@vanillagreen/pi-claude-bridge 1.0.6 → 1.1.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/README.md +1 -1
- package/bundle/index.js +215 -23
- package/package.json +1 -1
- package/src/convert.ts +43 -5
- package/src/index.ts +150 -5
- package/src/prompt-context.ts +1 -5
- package/src/typebox-to-zod.ts +19 -5
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Restart Pi after installation.
|
|
|
27
27
|
|
|
28
28
|
- `claude-bridge/claude-opus-4-7`, Sonnet, and Haiku models in `/model`.
|
|
29
29
|
- Pi tool calls bridged to Claude Code through a local MCP server.
|
|
30
|
-
- Session reuse/rebuild so Claude Code follows Pi history across normal turns, `/compact`,
|
|
30
|
+
- Session reuse/rebuild so Claude Code follows Pi history across normal turns, `/compact`, tree navigation, and abort recovery. Forks rebuild on the first turn so the fork never inherits the parent's external Claude jsonl.
|
|
31
31
|
- Thinking-level forwarding, summarized Opus thinking display, MCP isolation, and Claude cloud-MCP suppression to reduce token overhead.
|
|
32
32
|
- Optional forwarding of Pi-only context that upstream does not pass to Claude Code.
|
|
33
33
|
|
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,9 +19160,33 @@ 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";
|
|
19189
|
+
import { resolve as pathResolve } from "path";
|
|
19153
19190
|
import { homedir as homedir5 } from "os";
|
|
19154
19191
|
import { delimiter, dirname as dirname5, join as join5 } from "path";
|
|
19155
19192
|
|
|
@@ -19275,6 +19312,38 @@ function messageContentToText(content) {
|
|
|
19275
19312
|
}
|
|
19276
19313
|
return hasText ? parts.join("\n") : "";
|
|
19277
19314
|
}
|
|
19315
|
+
function imageBlockToAnthropic(block) {
|
|
19316
|
+
if (!block.data || !block.mimeType) return void 0;
|
|
19317
|
+
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } };
|
|
19318
|
+
}
|
|
19319
|
+
function toolResultContentToAnthropic(content) {
|
|
19320
|
+
if (typeof content === "string") return content;
|
|
19321
|
+
if (!Array.isArray(content)) return "";
|
|
19322
|
+
const blocks = [];
|
|
19323
|
+
for (const block of content) {
|
|
19324
|
+
if (block.type === "text" && block.text) {
|
|
19325
|
+
blocks.push({ type: "text", text: block.text });
|
|
19326
|
+
} else if (block.type === "image") {
|
|
19327
|
+
const image = imageBlockToAnthropic(block);
|
|
19328
|
+
if (image) blocks.push(image);
|
|
19329
|
+
} else if (block.type) {
|
|
19330
|
+
blocks.push({ type: "text", text: `[${block.type}]` });
|
|
19331
|
+
}
|
|
19332
|
+
}
|
|
19333
|
+
if (blocks.length === 0) return "";
|
|
19334
|
+
if (blocks.every((block) => block.type === "text")) return blocks.map((block) => block.text).join("\n");
|
|
19335
|
+
return blocks;
|
|
19336
|
+
}
|
|
19337
|
+
function assistantProvenancePrefix(msg) {
|
|
19338
|
+
if (msg.role !== "assistant") return void 0;
|
|
19339
|
+
const provider = typeof msg.provider === "string" ? msg.provider : void 0;
|
|
19340
|
+
const model = typeof msg.model === "string" ? msg.model : void 0;
|
|
19341
|
+
const api = typeof msg.api === "string" ? msg.api : void 0;
|
|
19342
|
+
if (!provider && !model && !api) return void 0;
|
|
19343
|
+
if (provider === PROVIDER_ID || api === "anthropic") return void 0;
|
|
19344
|
+
return `[Prior Pi assistant response from ${provider ?? api ?? "unknown-provider"}${model ? `/${model}` : ""}]
|
|
19345
|
+
`;
|
|
19346
|
+
}
|
|
19278
19347
|
function convertPiMessages(messages, customToolNameToSdk) {
|
|
19279
19348
|
const anthropicMessages = [];
|
|
19280
19349
|
const sanitizedIds = /* @__PURE__ */ new Map();
|
|
@@ -19287,16 +19356,18 @@ function convertPiMessages(messages, customToolNameToSdk) {
|
|
|
19287
19356
|
for (const block of msg.content) {
|
|
19288
19357
|
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
19289
19358
|
else if (block.type === "image" && block.data && block.mimeType) {
|
|
19290
|
-
parts.push(
|
|
19359
|
+
parts.push(imageBlockToAnthropic(block));
|
|
19291
19360
|
}
|
|
19292
19361
|
}
|
|
19293
|
-
anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
|
|
19362
|
+
anthropicMessages.push({ role: "user", content: parts.filter(Boolean).length ? parts.filter(Boolean) : "[image]" });
|
|
19294
19363
|
} else {
|
|
19295
19364
|
anthropicMessages.push({ role: "user", content: "[empty]" });
|
|
19296
19365
|
}
|
|
19297
19366
|
} else if (msg.role === "assistant") {
|
|
19298
19367
|
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
19299
19368
|
const blocks = [];
|
|
19369
|
+
const provenance = assistantProvenancePrefix(msg);
|
|
19370
|
+
if (provenance) blocks.push({ type: "text", text: provenance });
|
|
19300
19371
|
for (const block of content) {
|
|
19301
19372
|
if (block.type === "text" && block.text) {
|
|
19302
19373
|
blocks.push({ type: "text", text: block.text });
|
|
@@ -19314,10 +19385,10 @@ function convertPiMessages(messages, customToolNameToSdk) {
|
|
|
19314
19385
|
if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
|
|
19315
19386
|
anthropicMessages.push({ role: "assistant", content: blocks });
|
|
19316
19387
|
} else if (msg.role === "toolResult") {
|
|
19317
|
-
const
|
|
19388
|
+
const content = toolResultContentToAnthropic(msg.content);
|
|
19318
19389
|
anthropicMessages.push({
|
|
19319
19390
|
role: "user",
|
|
19320
|
-
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content:
|
|
19391
|
+
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content: content || "", is_error: msg.isError }]
|
|
19321
19392
|
});
|
|
19322
19393
|
}
|
|
19323
19394
|
}
|
|
@@ -19360,7 +19431,7 @@ function rewriteSkillsBlock(skillsBlock) {
|
|
|
19360
19431
|
}
|
|
19361
19432
|
|
|
19362
19433
|
// src/session-verify.ts
|
|
19363
|
-
import { statSync as statSync2, readFileSync as
|
|
19434
|
+
import { statSync as statSync2, readFileSync as readFileSync3 } from "fs";
|
|
19364
19435
|
function verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount) {
|
|
19365
19436
|
const warnings = [];
|
|
19366
19437
|
let st;
|
|
@@ -19372,7 +19443,7 @@ function verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount)
|
|
|
19372
19443
|
}
|
|
19373
19444
|
let content;
|
|
19374
19445
|
try {
|
|
19375
|
-
content =
|
|
19446
|
+
content = readFileSync3(jsonlPath, "utf8");
|
|
19376
19447
|
} catch (e2) {
|
|
19377
19448
|
warnings.push(`file unreadable \u2014 path=${jsonlPath} size=${st.size} err=${e2.message}`);
|
|
19378
19449
|
return warnings;
|
|
@@ -19484,7 +19555,7 @@ function popContext() {
|
|
|
19484
19555
|
}
|
|
19485
19556
|
|
|
19486
19557
|
// src/config.ts
|
|
19487
|
-
import { existsSync as existsSync3, readFileSync as
|
|
19558
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
19488
19559
|
import { homedir as homedir2 } from "os";
|
|
19489
19560
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
19490
19561
|
var PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
|
|
@@ -19525,7 +19596,7 @@ function settingsPaths(cwd) {
|
|
|
19525
19596
|
function tryParseJson(path) {
|
|
19526
19597
|
if (!existsSync3(path)) return {};
|
|
19527
19598
|
try {
|
|
19528
|
-
return JSON.parse(
|
|
19599
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
19529
19600
|
} catch (e2) {
|
|
19530
19601
|
console.error(`claude-bridge: failed to parse ${path}: ${e2}`);
|
|
19531
19602
|
return {};
|
|
@@ -19536,7 +19607,7 @@ function readManagerConfig(cwd) {
|
|
|
19536
19607
|
for (const path of settingsPaths(cwd)) {
|
|
19537
19608
|
if (!existsSync3(path)) continue;
|
|
19538
19609
|
try {
|
|
19539
|
-
const parsed = JSON.parse(
|
|
19610
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
19540
19611
|
const configRoot = asRecord(asRecord(asRecord(parsed?.vstack)?.extensionManager)?.config);
|
|
19541
19612
|
const config2 = asRecord(configRoot?.[PACKAGE_ID]);
|
|
19542
19613
|
if (config2) mergeDeep(merged, config2);
|
|
@@ -19587,7 +19658,7 @@ function loadConfig(cwd) {
|
|
|
19587
19658
|
}
|
|
19588
19659
|
|
|
19589
19660
|
// src/agents-md.ts
|
|
19590
|
-
import { existsSync as existsSync4, readFileSync as
|
|
19661
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
19591
19662
|
import { homedir as homedir3 } from "os";
|
|
19592
19663
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
19593
19664
|
var GLOBAL_AGENTS_PATH = join3(homedir3(), ".pi", "agent", "AGENTS.md");
|
|
@@ -19612,7 +19683,7 @@ function extractAgentsAppend() {
|
|
|
19612
19683
|
const agentsPath = resolveAgentsMdPath();
|
|
19613
19684
|
if (!agentsPath) return void 0;
|
|
19614
19685
|
try {
|
|
19615
|
-
const content =
|
|
19686
|
+
const content = readFileSync5(agentsPath, "utf-8").trim();
|
|
19616
19687
|
if (!content) return void 0;
|
|
19617
19688
|
const sanitized = sanitizeAgentsContent(content);
|
|
19618
19689
|
return sanitized.length > 0 ? `# CLAUDE.md
|
|
@@ -19632,7 +19703,7 @@ function sanitizeAgentsContent(content) {
|
|
|
19632
19703
|
}
|
|
19633
19704
|
|
|
19634
19705
|
// src/prompt-context.ts
|
|
19635
|
-
import { existsSync as existsSync5, readFileSync as
|
|
19706
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
19636
19707
|
import { homedir as homedir4 } from "os";
|
|
19637
19708
|
import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
|
|
19638
19709
|
function piUserDir2() {
|
|
@@ -19643,7 +19714,7 @@ function piUserDir2() {
|
|
|
19643
19714
|
function readTrimmed(path) {
|
|
19644
19715
|
try {
|
|
19645
19716
|
if (!existsSync5(path)) return void 0;
|
|
19646
|
-
const content =
|
|
19717
|
+
const content = readFileSync6(path, "utf8").trim();
|
|
19647
19718
|
return content.length > 0 ? content : void 0;
|
|
19648
19719
|
} catch {
|
|
19649
19720
|
return void 0;
|
|
@@ -19727,11 +19798,7 @@ ${taskReminder}`);
|
|
|
19727
19798
|
}
|
|
19728
19799
|
}
|
|
19729
19800
|
if (settings.includeCavemanHook) {
|
|
19730
|
-
const caveman = extractBlockByMarkers(systemPrompt, [
|
|
19731
|
-
/^Caveman communication mode active/m,
|
|
19732
|
-
/^Token efficiency mode: terse smart caveman/m,
|
|
19733
|
-
/^Caveman mode is active/m
|
|
19734
|
-
]);
|
|
19801
|
+
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
19735
19802
|
if (caveman) {
|
|
19736
19803
|
parts.push(`### before_agent_start: caveman
|
|
19737
19804
|
|
|
@@ -34267,7 +34334,7 @@ config(en_default());
|
|
|
34267
34334
|
// src/typebox-to-zod.ts
|
|
34268
34335
|
function jsonSchemaPropertyToZod(prop) {
|
|
34269
34336
|
let base;
|
|
34270
|
-
if (Array.isArray(prop.enum)) base = external_exports.enum(prop.enum);
|
|
34337
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) base = external_exports.enum(prop.enum);
|
|
34271
34338
|
else switch (prop.type) {
|
|
34272
34339
|
case "string":
|
|
34273
34340
|
base = external_exports.string();
|
|
@@ -34279,12 +34346,22 @@ function jsonSchemaPropertyToZod(prop) {
|
|
|
34279
34346
|
case "boolean":
|
|
34280
34347
|
base = external_exports.boolean();
|
|
34281
34348
|
break;
|
|
34282
|
-
case "array":
|
|
34349
|
+
case "array": {
|
|
34283
34350
|
base = prop.items ? external_exports.array(jsonSchemaPropertyToZod(prop.items)) : external_exports.array(external_exports.unknown());
|
|
34351
|
+
const minItems = typeof prop.minItems === "number" ? prop.minItems : void 0;
|
|
34352
|
+
if (minItems !== void 0) base = base.min(minItems);
|
|
34284
34353
|
break;
|
|
34285
|
-
|
|
34286
|
-
|
|
34354
|
+
}
|
|
34355
|
+
case "object": {
|
|
34356
|
+
if (prop.properties && typeof prop.properties === "object" && !Array.isArray(prop.properties)) {
|
|
34357
|
+
base = external_exports.object(jsonSchemaToZodShape(prop));
|
|
34358
|
+
if (prop.additionalProperties === false) base = base.strict();
|
|
34359
|
+
else if (prop.additionalProperties === true) base = base.passthrough();
|
|
34360
|
+
} else {
|
|
34361
|
+
base = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
34362
|
+
}
|
|
34287
34363
|
break;
|
|
34364
|
+
}
|
|
34288
34365
|
default:
|
|
34289
34366
|
base = external_exports.unknown();
|
|
34290
34367
|
}
|
|
@@ -34423,6 +34500,113 @@ var CLAUDE_BRIDGE_TOOL_ISOLATION = {
|
|
|
34423
34500
|
allowedTools: [`mcp__${MCP_SERVER_NAME}__*`]
|
|
34424
34501
|
};
|
|
34425
34502
|
var sharedSession = null;
|
|
34503
|
+
var extensionApi;
|
|
34504
|
+
var BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
|
|
34505
|
+
function fingerprintMessages(messages) {
|
|
34506
|
+
const normalized = messages.map((message) => {
|
|
34507
|
+
if (message.role === "assistant") {
|
|
34508
|
+
return {
|
|
34509
|
+
role: message.role,
|
|
34510
|
+
provider: message.provider,
|
|
34511
|
+
model: message.model,
|
|
34512
|
+
content: message.content
|
|
34513
|
+
};
|
|
34514
|
+
}
|
|
34515
|
+
return message;
|
|
34516
|
+
});
|
|
34517
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
34518
|
+
}
|
|
34519
|
+
function readBuiltSessionContext(sessionManager) {
|
|
34520
|
+
const built = typeof sessionManager?.buildSessionContext === "function" ? sessionManager.buildSessionContext() : void 0;
|
|
34521
|
+
return Array.isArray(built?.messages) ? built : void 0;
|
|
34522
|
+
}
|
|
34523
|
+
function latestPersistedBridgeSession(sessionManager) {
|
|
34524
|
+
const entries = typeof sessionManager?.getEntries === "function" ? sessionManager.getEntries() : [];
|
|
34525
|
+
if (!Array.isArray(entries)) return void 0;
|
|
34526
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
34527
|
+
const entry = entries[i2];
|
|
34528
|
+
if (entry?.type !== "custom" || entry.customType !== BRIDGE_SESSION_CUSTOM_TYPE) continue;
|
|
34529
|
+
const data = entry.data;
|
|
34530
|
+
if (!data || typeof data.sessionId !== "string" || typeof data.cursor !== "number" || typeof data.cwd !== "string" || typeof data.fingerprint !== "string") continue;
|
|
34531
|
+
return data;
|
|
34532
|
+
}
|
|
34533
|
+
return void 0;
|
|
34534
|
+
}
|
|
34535
|
+
function claudeSessionExists(sessionId, cwd) {
|
|
34536
|
+
try {
|
|
34537
|
+
const session = openSession({ sessionId, projectPath: cwd, claudeDir: process.env.CLAUDE_CONFIG_DIR });
|
|
34538
|
+
statSync3(session.jsonlPath);
|
|
34539
|
+
return true;
|
|
34540
|
+
} catch {
|
|
34541
|
+
return false;
|
|
34542
|
+
}
|
|
34543
|
+
}
|
|
34544
|
+
function canonicalize(p4) {
|
|
34545
|
+
if (!p4) return void 0;
|
|
34546
|
+
try {
|
|
34547
|
+
return realpathSync3.native(p4);
|
|
34548
|
+
} catch {
|
|
34549
|
+
return pathResolve(p4);
|
|
34550
|
+
}
|
|
34551
|
+
}
|
|
34552
|
+
function shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd) {
|
|
34553
|
+
if (!persisted.piSessionId) return "missing piSessionId";
|
|
34554
|
+
if (currentPiSessionId && persisted.piSessionId !== currentPiSessionId) {
|
|
34555
|
+
return `piSessionId mismatch (persisted=${persisted.piSessionId} current=${currentPiSessionId})`;
|
|
34556
|
+
}
|
|
34557
|
+
if (currentCwd && canonicalize(persisted.cwd) !== canonicalize(currentCwd)) {
|
|
34558
|
+
return `cwd mismatch (persisted=${persisted.cwd} current=${currentCwd})`;
|
|
34559
|
+
}
|
|
34560
|
+
return void 0;
|
|
34561
|
+
}
|
|
34562
|
+
function restoreSharedSessionFromPi(ctx2) {
|
|
34563
|
+
const persisted = latestPersistedBridgeSession(ctx2.sessionManager);
|
|
34564
|
+
if (!persisted) return;
|
|
34565
|
+
const currentPiSessionId = typeof ctx2.sessionManager?.getSessionId === "function" ? ctx2.sessionManager.getSessionId() : void 0;
|
|
34566
|
+
const currentCwd = typeof ctx2.sessionManager?.getCwd === "function" ? ctx2.sessionManager.getCwd() : ctx2.cwd;
|
|
34567
|
+
const rejection = shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd);
|
|
34568
|
+
if (rejection) {
|
|
34569
|
+
debug(`restoreSharedSession: ${rejection} \u2014 forcing rebuild`);
|
|
34570
|
+
return;
|
|
34571
|
+
}
|
|
34572
|
+
const built = readBuiltSessionContext(ctx2.sessionManager);
|
|
34573
|
+
if (!built) return;
|
|
34574
|
+
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
34575
|
+
const fingerprint = fingerprintMessages(built.messages.slice(0, cursor));
|
|
34576
|
+
if (fingerprint !== persisted.fingerprint) {
|
|
34577
|
+
debug(`restoreSharedSession: fingerprint mismatch for ${persisted.sessionId.slice(0, 8)}`);
|
|
34578
|
+
return;
|
|
34579
|
+
}
|
|
34580
|
+
if (!claudeSessionExists(persisted.sessionId, persisted.cwd)) {
|
|
34581
|
+
debug(`restoreSharedSession: Claude session missing for ${persisted.sessionId.slice(0, 8)}`);
|
|
34582
|
+
return;
|
|
34583
|
+
}
|
|
34584
|
+
sharedSession = { sessionId: persisted.sessionId, cursor, cwd: persisted.cwd };
|
|
34585
|
+
debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
34586
|
+
}
|
|
34587
|
+
function schedulePersistSharedSession(ctxLike) {
|
|
34588
|
+
if (!extensionApi || !sharedSession || !ctxLike?.sessionManager) return;
|
|
34589
|
+
const snapshot = { ...sharedSession };
|
|
34590
|
+
const timer = setTimeout(() => {
|
|
34591
|
+
try {
|
|
34592
|
+
const built = readBuiltSessionContext(ctxLike.sessionManager);
|
|
34593
|
+
if (!built) return;
|
|
34594
|
+
const cursor = Math.max(0, Math.min(snapshot.cursor, built.messages.length));
|
|
34595
|
+
const data = {
|
|
34596
|
+
...snapshot,
|
|
34597
|
+
cursor,
|
|
34598
|
+
fingerprint: fingerprintMessages(built.messages.slice(0, cursor)),
|
|
34599
|
+
piSessionId: typeof ctxLike.sessionManager?.getSessionId === "function" ? ctxLike.sessionManager.getSessionId() : void 0,
|
|
34600
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34601
|
+
};
|
|
34602
|
+
extensionApi?.appendEntry(BRIDGE_SESSION_CUSTOM_TYPE, data);
|
|
34603
|
+
debug(`persistSharedSession: saved ${data.sessionId.slice(0, 8)}, cursor=${data.cursor}`);
|
|
34604
|
+
} catch (error51) {
|
|
34605
|
+
debug("persistSharedSession failed:", error51);
|
|
34606
|
+
}
|
|
34607
|
+
}, 0);
|
|
34608
|
+
timer.unref?.();
|
|
34609
|
+
}
|
|
34426
34610
|
function convertAndImportMessages(session, messages, customToolNameToSdk) {
|
|
34427
34611
|
const { anthropicMessages, sanitizedIds } = convertPiMessages(messages, customToolNameToSdk);
|
|
34428
34612
|
debug(`convertAndImportMessages: ${messages.length} pi msgs \u2192 ${anthropicMessages.length} anthropic msgs`);
|
|
@@ -35138,6 +35322,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35138
35322
|
return stream;
|
|
35139
35323
|
}
|
|
35140
35324
|
function index_default(pi) {
|
|
35325
|
+
extensionApi = pi;
|
|
35141
35326
|
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
35142
35327
|
const config2 = loadConfig(process.cwd());
|
|
35143
35328
|
debug("loadConfig:", JSON.stringify(config2));
|
|
@@ -35159,8 +35344,13 @@ function index_default(pi) {
|
|
|
35159
35344
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
35160
35345
|
clearSession(`session_start:${event.reason}`);
|
|
35161
35346
|
}
|
|
35347
|
+
if (event.reason === "startup" || event.reason === "resume") restoreSharedSessionFromPi(ctx2);
|
|
35162
35348
|
});
|
|
35163
35349
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
35350
|
+
pi.on("message_end", (event, ctx2) => {
|
|
35351
|
+
const message = event.message;
|
|
35352
|
+
if (message?.role === "assistant" && message.provider === PROVIDER_ID) schedulePersistSharedSession(ctx2);
|
|
35353
|
+
});
|
|
35164
35354
|
const markRebuild = (event) => {
|
|
35165
35355
|
if (sharedSession) {
|
|
35166
35356
|
debug(`${event}: marking needsRebuild on session ${sharedSession.sessionId.slice(0, 8)}`);
|
|
@@ -35188,5 +35378,7 @@ export {
|
|
|
35188
35378
|
CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
35189
35379
|
DISALLOWED_BUILTIN_TOOLS,
|
|
35190
35380
|
index_default as default,
|
|
35191
|
-
mapToolName
|
|
35381
|
+
mapToolName,
|
|
35382
|
+
restoreSharedSessionFromPi,
|
|
35383
|
+
shouldRestorePersistedBridgeEntry
|
|
35192
35384
|
};
|
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,8 +3,10 @@ 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";
|
|
9
|
+
import { resolve as pathResolve } from "path";
|
|
8
10
|
import { homedir } from "os";
|
|
9
11
|
import { delimiter, dirname, join } from "path";
|
|
10
12
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
@@ -184,12 +186,145 @@ interface SessionState {
|
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
let sharedSession: SessionState | null = null;
|
|
189
|
+
let extensionApi: ExtensionAPI | undefined;
|
|
190
|
+
|
|
191
|
+
const BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
|
|
192
|
+
|
|
193
|
+
interface PersistedBridgeSessionState extends SessionState {
|
|
194
|
+
fingerprint: string;
|
|
195
|
+
piSessionId?: string;
|
|
196
|
+
updatedAt: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function fingerprintMessages(messages: Context["messages"]): string {
|
|
200
|
+
const normalized = messages.map((message) => {
|
|
201
|
+
if (message.role === "assistant") {
|
|
202
|
+
return {
|
|
203
|
+
role: message.role,
|
|
204
|
+
provider: (message as AssistantMessage).provider,
|
|
205
|
+
model: (message as AssistantMessage).model,
|
|
206
|
+
content: (message as AssistantMessage).content,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return message;
|
|
210
|
+
});
|
|
211
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readBuiltSessionContext(sessionManager: unknown): { messages: Context["messages"] } | undefined {
|
|
215
|
+
const built = typeof (sessionManager as any)?.buildSessionContext === "function" ? (sessionManager as any).buildSessionContext() : undefined;
|
|
216
|
+
return Array.isArray(built?.messages) ? built as { messages: Context["messages"] } : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function latestPersistedBridgeSession(sessionManager: unknown): PersistedBridgeSessionState | undefined {
|
|
220
|
+
const entries = typeof (sessionManager as any)?.getEntries === "function" ? (sessionManager as any).getEntries() : [];
|
|
221
|
+
if (!Array.isArray(entries)) return undefined;
|
|
222
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
223
|
+
const entry = entries[i];
|
|
224
|
+
if (entry?.type !== "custom" || entry.customType !== BRIDGE_SESSION_CUSTOM_TYPE) continue;
|
|
225
|
+
const data = entry.data as Partial<PersistedBridgeSessionState> | undefined;
|
|
226
|
+
if (!data || typeof data.sessionId !== "string" || typeof data.cursor !== "number" || typeof data.cwd !== "string" || typeof data.fingerprint !== "string") continue;
|
|
227
|
+
return data as PersistedBridgeSessionState;
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function claudeSessionExists(sessionId: string, cwd: string): boolean {
|
|
233
|
+
try {
|
|
234
|
+
const session = openSession({ sessionId, projectPath: cwd, claudeDir: process.env.CLAUDE_CONFIG_DIR });
|
|
235
|
+
statSync(session.jsonlPath);
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function canonicalize(p: string | undefined): string | undefined {
|
|
243
|
+
if (!p) return undefined;
|
|
244
|
+
try { return realpathSync.native(p); } catch { return pathResolve(p); }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Decides whether a persisted bridge-session marker is safe to restore.
|
|
248
|
+
//
|
|
249
|
+
// The fork case is the load-bearing one: pi/core's createBranchedSession copies
|
|
250
|
+
// every non-label entry from root→leaf into the new session file. That includes
|
|
251
|
+
// our claude-bridge-session markers from the parent. Restoring from them would
|
|
252
|
+
// --resume parent's Claude jsonl on the fork's first turn, leaking conversation
|
|
253
|
+
// past the fork point.
|
|
254
|
+
//
|
|
255
|
+
// Returns undefined when the entry is safe to use, or a short rejection reason
|
|
256
|
+
// for diagnostic logging. Old entries without piSessionId always reject, which
|
|
257
|
+
// degrades safely to the rebuild path.
|
|
258
|
+
export function shouldRestorePersistedBridgeEntry(
|
|
259
|
+
persisted: { piSessionId?: string; cwd: string },
|
|
260
|
+
currentPiSessionId: string | undefined,
|
|
261
|
+
currentCwd: string | undefined,
|
|
262
|
+
): string | undefined {
|
|
263
|
+
if (!persisted.piSessionId) return "missing piSessionId";
|
|
264
|
+
if (currentPiSessionId && persisted.piSessionId !== currentPiSessionId) {
|
|
265
|
+
return `piSessionId mismatch (persisted=${persisted.piSessionId} current=${currentPiSessionId})`;
|
|
266
|
+
}
|
|
267
|
+
if (currentCwd && canonicalize(persisted.cwd) !== canonicalize(currentCwd)) {
|
|
268
|
+
return `cwd mismatch (persisted=${persisted.cwd} current=${currentCwd})`;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function restoreSharedSessionFromPi(ctx: { sessionManager?: unknown; cwd?: string }): void {
|
|
274
|
+
const persisted = latestPersistedBridgeSession(ctx.sessionManager);
|
|
275
|
+
if (!persisted) return;
|
|
276
|
+
const currentPiSessionId = typeof (ctx.sessionManager as any)?.getSessionId === "function" ? (ctx.sessionManager as any).getSessionId() : undefined;
|
|
277
|
+
const currentCwd = typeof (ctx.sessionManager as any)?.getCwd === "function" ? (ctx.sessionManager as any).getCwd() : ctx.cwd;
|
|
278
|
+
const rejection = shouldRestorePersistedBridgeEntry(persisted, currentPiSessionId, currentCwd);
|
|
279
|
+
if (rejection) {
|
|
280
|
+
debug(`restoreSharedSession: ${rejection} — forcing rebuild`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const built = readBuiltSessionContext(ctx.sessionManager);
|
|
284
|
+
if (!built) return;
|
|
285
|
+
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
286
|
+
const fingerprint = fingerprintMessages(built.messages.slice(0, cursor));
|
|
287
|
+
if (fingerprint !== persisted.fingerprint) {
|
|
288
|
+
debug(`restoreSharedSession: fingerprint mismatch for ${persisted.sessionId.slice(0, 8)}`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!claudeSessionExists(persisted.sessionId, persisted.cwd)) {
|
|
292
|
+
debug(`restoreSharedSession: Claude session missing for ${persisted.sessionId.slice(0, 8)}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
sharedSession = { sessionId: persisted.sessionId, cursor, cwd: persisted.cwd };
|
|
296
|
+
debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function schedulePersistSharedSession(ctxLike?: { sessionManager?: unknown }): void {
|
|
300
|
+
if (!extensionApi || !sharedSession || !ctxLike?.sessionManager) return;
|
|
301
|
+
const snapshot = { ...sharedSession };
|
|
302
|
+
const timer = setTimeout(() => {
|
|
303
|
+
try {
|
|
304
|
+
const built = readBuiltSessionContext(ctxLike.sessionManager);
|
|
305
|
+
if (!built) return;
|
|
306
|
+
const cursor = Math.max(0, Math.min(snapshot.cursor, built.messages.length));
|
|
307
|
+
const data: PersistedBridgeSessionState = {
|
|
308
|
+
...snapshot,
|
|
309
|
+
cursor,
|
|
310
|
+
fingerprint: fingerprintMessages(built.messages.slice(0, cursor)),
|
|
311
|
+
piSessionId: typeof (ctxLike.sessionManager as any)?.getSessionId === "function" ? (ctxLike.sessionManager as any).getSessionId() : undefined,
|
|
312
|
+
updatedAt: new Date().toISOString(),
|
|
313
|
+
};
|
|
314
|
+
extensionApi?.appendEntry(BRIDGE_SESSION_CUSTOM_TYPE, data);
|
|
315
|
+
debug(`persistSharedSession: saved ${data.sessionId.slice(0, 8)}, cursor=${data.cursor}`);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
debug("persistSharedSession failed:", error);
|
|
318
|
+
}
|
|
319
|
+
}, 0);
|
|
320
|
+
timer.unref?.();
|
|
321
|
+
}
|
|
187
322
|
|
|
188
323
|
// Convert pi messages to Anthropic API format for session import.
|
|
189
|
-
// Lossy: non-Anthropic thinking blocks are dropped (no valid signature)
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
324
|
+
// Lossy: non-Anthropic thinking blocks are dropped (no valid signature). User and
|
|
325
|
+
// tool-result image blocks are preserved when possible. If assistant blocks are
|
|
326
|
+
// otherwise incompatible, convertPiMessages emits a text placeholder so the record
|
|
327
|
+
// sequence stays valid before repairToolPairing runs.
|
|
193
328
|
function convertAndImportMessages(
|
|
194
329
|
session: ReturnType<typeof createSession>,
|
|
195
330
|
messages: Context["messages"],
|
|
@@ -1166,6 +1301,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1166
1301
|
// --- Extension registration ---
|
|
1167
1302
|
|
|
1168
1303
|
export default function (pi: ExtensionAPI) {
|
|
1304
|
+
extensionApi = pi;
|
|
1169
1305
|
// Disable non-essential Claude Code traffic (update checks, MCP registry, telemetry)
|
|
1170
1306
|
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
1171
1307
|
|
|
@@ -1195,8 +1331,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1195
1331
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
1196
1332
|
clearSession(`session_start:${event.reason}`);
|
|
1197
1333
|
}
|
|
1334
|
+
// Note: "fork" intentionally omitted from restoration. createBranchedSession
|
|
1335
|
+
// copies the parent's persisted bridge entries into the fork; restoring from
|
|
1336
|
+
// them would --resume the parent's Claude jsonl and leak conversation past the
|
|
1337
|
+
// fork point. Letting the first fork turn rebuild is the correct path.
|
|
1338
|
+
if (event.reason === "startup" || event.reason === "resume") restoreSharedSessionFromPi(ctx);
|
|
1198
1339
|
});
|
|
1199
1340
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
1341
|
+
pi.on("message_end", (event, ctx) => {
|
|
1342
|
+
const message = (event as { message?: AssistantMessage }).message;
|
|
1343
|
+
if (message?.role === "assistant" && message.provider === PROVIDER_ID) schedulePersistSharedSession(ctx);
|
|
1344
|
+
});
|
|
1200
1345
|
|
|
1201
1346
|
// pi /compact and session-tree navigation (rewind / fork-at-point /
|
|
1202
1347
|
// branch switch) both mutate pi's messages array out from under the
|
package/src/prompt-context.ts
CHANGED
|
@@ -115,11 +115,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (settings.includeCavemanHook) {
|
|
118
|
-
const caveman = extractBlockByMarkers(systemPrompt, [
|
|
119
|
-
/^Caveman communication mode active/m,
|
|
120
|
-
/^Token efficiency mode: terse smart caveman/m,
|
|
121
|
-
/^Caveman mode is active/m,
|
|
122
|
-
]);
|
|
118
|
+
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
123
119
|
if (caveman) {
|
|
124
120
|
parts.push(`### before_agent_start: caveman\n\n${caveman}`);
|
|
125
121
|
labels.push("caveman hook");
|
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);
|