@raoxxxwq/pi-codebuddy-sdk 0.3.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/src/convert.ts ADDED
@@ -0,0 +1,101 @@
1
+ // Pi → CodeBuddy message conversion helpers.
2
+
3
+ import type { Message as PiMessage } from "@earendil-works/pi-ai";
4
+ import type { CbMessage } from "./cb-session-io.js";
5
+ import { pascalCase } from "change-case";
6
+
7
+ export const PROVIDER_ID = "codebuddy";
8
+
9
+ export const PI_TO_SDK_TOOL_NAME: Record<string, string> = {
10
+ read: "Read", write: "Write", edit: "Edit", bash: "Bash",
11
+ };
12
+
13
+ export function sanitizeToolId(id: string, cache: Map<string, string>): string {
14
+ const existing = cache.get(id);
15
+ if (existing) return existing;
16
+ const clean = id.replace(/[^a-zA-Z0-9_-]/g, "_");
17
+ cache.set(id, clean);
18
+ return clean;
19
+ }
20
+
21
+ export function mapPiToolNameToSdk(name: string, customToolNameToSdk?: Map<string, string>): string {
22
+ if (!name) return "";
23
+ const normalized = name.toLowerCase();
24
+ if (customToolNameToSdk) {
25
+ const mapped = customToolNameToSdk.get(name) ?? customToolNameToSdk.get(normalized);
26
+ if (mapped) return mapped;
27
+ }
28
+ if (PI_TO_SDK_TOOL_NAME[normalized]) return PI_TO_SDK_TOOL_NAME[normalized];
29
+ return pascalCase(name);
30
+ }
31
+
32
+ export function messageContentToText(
33
+ content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
34
+ ): string {
35
+ if (typeof content === "string") return content;
36
+ if (!Array.isArray(content)) return "";
37
+ const parts = [];
38
+ let hasText = false;
39
+ for (const block of content) {
40
+ if (block.type === "text" && block.text) { parts.push(block.text); hasText = true; }
41
+ else if (block.type !== "text" && block.type !== "image") { parts.push(`[${block.type}]`); }
42
+ }
43
+ return hasText ? parts.join("\n") : "";
44
+ }
45
+
46
+ /** Convert pi message array to session-import format. */
47
+ export function convertPiMessages(
48
+ messages: PiMessage[],
49
+ customToolNameToSdk?: Map<string, string>,
50
+ ): { anthropicMessages: Array<{ role: "user" | "assistant"; content: string | Array<unknown> }>; sanitizedIds: Map<string, string> } {
51
+ const anthropicMessages: Array<{ role: "user" | "assistant"; content: string | Array<unknown> }> = [];
52
+ const sanitizedIds = new Map<string, string>();
53
+
54
+ for (const msg of messages) {
55
+ if (msg.role === "user") {
56
+ if (typeof msg.content === "string") {
57
+ anthropicMessages.push({ role: "user", content: msg.content || "[empty]" });
58
+ } else if (Array.isArray(msg.content)) {
59
+ const parts = [];
60
+ for (const block of msg.content) {
61
+ if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
62
+ else if (block.type === "image" && block.data && block.mimeType) {
63
+ parts.push({ type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } });
64
+ }
65
+ }
66
+ anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
67
+ } else {
68
+ anthropicMessages.push({ role: "user", content: "[empty]" });
69
+ }
70
+ } else if (msg.role === "assistant") {
71
+ const content = Array.isArray(msg.content) ? msg.content : [];
72
+ const blocks = [];
73
+ for (const block of content) {
74
+ if (block.type === "text" && block.text) {
75
+ blocks.push({ type: "text", text: block.text });
76
+ } else if (block.type === "thinking") {
77
+ const sig = block.thinkingSignature;
78
+ const isCodebuddyProvider = msg.provider === PROVIDER_ID || msg.provider === "codebuddy-sdk" || msg.api === "codebuddy-sdk";
79
+ if (isCodebuddyProvider && sig) {
80
+ blocks.push({ type: "thinking", thinking: block.thinking ?? "", signature: sig });
81
+ }
82
+ } else if (block.type === "toolCall") {
83
+ const toolName = mapPiToolNameToSdk(block.name, customToolNameToSdk);
84
+ blocks.push({ type: "tool_use", id: sanitizeToolId(block.id, sanitizedIds), name: toolName, input: block.arguments ?? {} });
85
+ }
86
+ }
87
+ if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
88
+ anthropicMessages.push({ role: "assistant", content: blocks });
89
+ } else if (msg.role === "toolResult") {
90
+ const text = typeof msg.content === "string" ? msg.content : messageContentToText(msg.content);
91
+ anthropicMessages.push({
92
+ role: "user",
93
+ content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content: text || "", is_error: msg.isError }],
94
+ });
95
+ }
96
+ }
97
+
98
+ return { anthropicMessages, sanitizedIds };
99
+ }
100
+
101
+ export type SessionMessage = CbMessage;
@@ -0,0 +1,47 @@
1
+ // Tool-result extraction: walks the context tail to collect this turn's
2
+ // tool results. Pi appends results to context and calls the provider again;
3
+ // this scrapes them back out. Walks past user messages (steer/followUp) that
4
+ // pi may inject between toolResults. Stops at the nearest assistant message
5
+ // (turn boundary).
6
+ // Extracted from index.ts so tests can import without activating the extension.
7
+
8
+ export type McpContent = Array<
9
+ | { type: "text"; text: string }
10
+ | { type: "image"; data: string; mimeType: string }
11
+ >;
12
+
13
+ export interface McpResult {
14
+ content: McpContent;
15
+ isError?: boolean;
16
+ toolCallId?: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export function toolResultToMcpContent(
21
+ content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
22
+ ): McpContent {
23
+ if (typeof content === "string") return [{ type: "text", text: content || "" }];
24
+ if (!Array.isArray(content)) return [{ type: "text", text: "" }];
25
+ const blocks: McpContent = [];
26
+ for (const block of content) {
27
+ if (block.type === "text" && block.text) blocks.push({ type: "text", text: block.text });
28
+ else if (block.type === "image" && block.data && block.mimeType) blocks.push({ type: "image", data: block.data, mimeType: block.mimeType });
29
+ }
30
+ return blocks.length ? blocks : [{ type: "text", text: "" }];
31
+ }
32
+
33
+ // Returns { results, stopIdx } so callers can log the walk boundary.
34
+ export function extractAllToolResults(
35
+ messages: Array<{ role: string; content?: unknown; toolCallId?: string; isError?: boolean; [key: string]: unknown }>,
36
+ ): { results: McpResult[]; stopIdx: number } {
37
+ const results: McpResult[] = [];
38
+ let stopIdx = -1;
39
+ for (let i = messages.length - 1; i >= 0; i--) {
40
+ const msg = messages[i];
41
+ if (msg.role === "toolResult") {
42
+ results.unshift({ content: toolResultToMcpContent(msg.content as string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>), isError: msg.isError, toolCallId: msg.toolCallId });
43
+ } else if (msg.role === "assistant") { stopIdx = i; break; }
44
+ // user messages: skip (steer/followUp injected mid-tool-execution)
45
+ }
46
+ return { results, stopIdx };
47
+ }
@@ -0,0 +1 @@
1
+ export type SettingSource = "user" | "project" | "local";