pi-codebuddy-sdk 0.1.19 → 0.3.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/src/config.ts CHANGED
@@ -1,46 +1,50 @@
1
- // User-facing extension config. Loaded once at extension registration from
2
- // ~/.pi/agent/codebuddy-sdk.json and the project Pi config directory, project
3
- // overriding global. Missing or unparseable files are ignored.
4
-
1
+ import type { SettingSource } from "./index-types.js";
5
2
  import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
6
3
  import { existsSync, readFileSync } from "fs";
7
4
  import { homedir } from "os";
8
5
  import { join } from "path";
9
6
 
7
+ function redactPath(path: string): string {
8
+ const home = homedir();
9
+ return home && path.startsWith(home) ? "~" + path.slice(home.length) : path;
10
+ }
11
+
12
+ export const CONFIG_BASENAME = "codebuddy-sdk.json";
13
+
10
14
  export interface Config {
11
- askCodebuddy?: {
12
- enabled?: boolean;
13
- name?: string;
14
- label?: string;
15
- description?: string;
16
- defaultMode?: "full" | "read" | "none";
17
- defaultIsolated?: boolean;
18
- allowFullMode?: boolean;
19
- appendSkills?: boolean;
20
- };
21
- provider?: {
22
- appendSystemPrompt?: boolean;
23
- pathToCodebuddyExecutable?: string;
24
- plan?: "pro" | "max";
25
- longContextExtraUsage?: boolean;
26
- };
15
+ askCodebuddy?: {
16
+ enabled?: boolean;
17
+ name?: string;
18
+ label?: string;
19
+ description?: string;
20
+ defaultMode?: "full" | "read" | "none";
21
+ defaultIsolated?: boolean;
22
+ allowFullMode?: boolean;
23
+ appendSkills?: boolean;
24
+ };
25
+ provider?: {
26
+ appendSystemPrompt?: boolean;
27
+ settingSources?: SettingSource[];
28
+ strictMcpConfig?: boolean;
29
+ pathToCodebuddyCode?: string;
30
+ };
27
31
  }
28
32
 
29
33
  export function tryParseJson(path: string): Partial<Config> {
30
- if (!existsSync(path)) return {};
31
- try {
32
- return JSON.parse(readFileSync(path, "utf-8"));
33
- } catch (e) {
34
- console.error(`codebuddy-sdk: failed to parse ${path}: ${e}`);
35
- return {};
36
- }
34
+ if (!existsSync(path)) return {};
35
+ try {
36
+ return JSON.parse(readFileSync(path, "utf-8"));
37
+ } catch (e) {
38
+ console.error(`codebuddy-sdk: failed to parse ${redactPath(path)}: ${e}`);
39
+ return {};
40
+ }
37
41
  }
38
42
 
39
43
  export function loadConfig(cwd: string): Config {
40
- const global = tryParseJson(join(homedir(), ".pi", "agent", "codebuddy-sdk.json"));
41
- const project = tryParseJson(join(cwd, CONFIG_DIR_NAME, "codebuddy-sdk.json"));
42
- return {
43
- askCodebuddy: { ...global.askCodebuddy, ...project.askCodebuddy },
44
- provider: { ...global.provider, ...project.provider },
45
- };
44
+ const global = tryParseJson(join(homedir(), ".pi", "agent", CONFIG_BASENAME));
45
+ const project = tryParseJson(join(cwd, CONFIG_DIR_NAME, CONFIG_BASENAME));
46
+ return {
47
+ askCodebuddy: { ...global.askCodebuddy, ...project.askCodebuddy },
48
+ provider: { ...global.provider, ...project.provider },
49
+ };
46
50
  }
package/src/convert.ts CHANGED
@@ -1,105 +1,101 @@
1
- // Pure pi -> SDK message conversion helpers.
1
+ // Pi → CodeBuddy message conversion helpers.
2
2
 
3
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";
4
6
 
5
7
  export const PROVIDER_ID = "codebuddy";
6
8
 
7
9
  export const PI_TO_SDK_TOOL_NAME: Record<string, string> = {
8
- read: "Read", write: "Write", edit: "Edit", bash: "Bash",
10
+ read: "Read", write: "Write", edit: "Edit", bash: "Bash",
9
11
  };
10
12
 
11
13
  export function sanitizeToolId(id: string, cache: Map<string, string>): string {
12
- const existing = cache.get(id);
13
- if (existing) return existing;
14
- const clean = id.replace(/[^a-zA-Z0-9_-]/g, "_");
15
- cache.set(id, clean);
16
- return clean;
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;
17
19
  }
18
20
 
19
21
  export function mapPiToolNameToSdk(name: string, customToolNameToSdk?: Map<string, string>): string {
20
- if (!name) return "";
21
- const normalized = name.toLowerCase();
22
- if (customToolNameToSdk) {
23
- const mapped = customToolNameToSdk.get(name) ?? customToolNameToSdk.get(normalized);
24
- if (mapped) return mapped;
25
- }
26
- if (PI_TO_SDK_TOOL_NAME[normalized]) return PI_TO_SDK_TOOL_NAME[normalized];
27
- // ponytail: simple PascalCase fallback instead of change-case dep
28
- return name.charAt(0).toUpperCase() + name.slice(1);
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);
29
30
  }
30
31
 
31
32
  export function messageContentToText(
32
- content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
33
+ content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
33
34
  ): string {
34
- if (typeof content === "string") return content;
35
- if (!Array.isArray(content)) return "";
36
- const parts = [];
37
- let hasText = false;
38
- for (const block of content) {
39
- if (block.type === "text" && block.text) { parts.push(block.text); hasText = true; }
40
- else if (block.type !== "text" && block.type !== "image") { parts.push(`[${block.type}]`); }
41
- }
42
- return hasText ? parts.join("\n") : "";
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") : "";
43
44
  }
44
45
 
45
- // SessionMessage is an internal type used by the provider's session persistence.
46
- // It mirrors the Anthropic API message format.
47
- export interface SessionMessage {
48
- role: "user" | "assistant";
49
- content: string | Array<Record<string, unknown>>;
50
- }
51
-
52
- /** Convert pi message array to SDK-compatible format. */
46
+ /** Convert pi message array to session-import format. */
53
47
  export function convertPiMessages(
54
- messages: PiMessage[],
55
- customToolNameToSdk?: Map<string, string>,
56
- ): { anthropicMessages: SessionMessage[]; sanitizedIds: Map<string, string> } {
57
- const anthropicMessages: SessionMessage[] = [];
58
- const sanitizedIds = new Map<string, string>();
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>();
59
53
 
60
- for (const msg of messages) {
61
- if (msg.role === "user") {
62
- if (typeof msg.content === "string") {
63
- anthropicMessages.push({ role: "user", content: msg.content || "[empty]" });
64
- } else if (Array.isArray(msg.content)) {
65
- const parts: Array<Record<string, unknown>> = [];
66
- for (const block of msg.content) {
67
- if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
68
- else if (block.type === "image" && (block as any).data && (block as any).mimeType) {
69
- parts.push({ type: "image", source: { type: "base64", media_type: (block as any).mimeType, data: (block as any).data } });
70
- }
71
- }
72
- anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
73
- } else {
74
- anthropicMessages.push({ role: "user", content: "[empty]" });
75
- }
76
- } else if (msg.role === "assistant") {
77
- const content = Array.isArray(msg.content) ? msg.content : [];
78
- const blocks: Array<Record<string, unknown>> = [];
79
- for (const block of content) {
80
- if (block.type === "text" && block.text) {
81
- blocks.push({ type: "text", text: block.text });
82
- } else if (block.type === "thinking") {
83
- const sig = (block as any).thinkingSignature;
84
- const isCodebuddyProvider = msg.provider === PROVIDER_ID || msg.api === "codebuddy-sdk";
85
- if (isCodebuddyProvider && sig) {
86
- blocks.push({ type: "thinking", thinking: block.thinking ?? "", signature: sig });
87
- }
88
- } else if (block.type === "toolCall") {
89
- const toolName = mapPiToolNameToSdk((block as any).name, customToolNameToSdk);
90
- blocks.push({ type: "tool_use", id: sanitizeToolId((block as any).id, sanitizedIds), name: toolName, input: (block as any).arguments ?? {} });
91
- }
92
- }
93
- if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
94
- anthropicMessages.push({ role: "assistant", content: blocks });
95
- } else if (msg.role === "toolResult") {
96
- const text = typeof msg.content === "string" ? msg.content : messageContentToText(msg.content);
97
- anthropicMessages.push({
98
- role: "user",
99
- content: [{ type: "tool_result", tool_use_id: sanitizeToolId((msg as any).toolCallId, sanitizedIds), content: text || "", is_error: (msg as any).isError }],
100
- });
101
- }
102
- }
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
+ }
103
97
 
104
- return { anthropicMessages, sanitizedIds };
98
+ return { anthropicMessages, sanitizedIds };
105
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";