pi-codebuddy-sdk 0.1.18 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eli Dickinson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,107 +1,130 @@
1
1
  # pi-codebuddy-sdk
2
2
 
3
- 回退到 `@tencent-ai/agent-sdk` 的 Pi provider 扩展。无反向代理,直接使用本地 CodeBuddy CLI,合规安全。
3
+ Pi extension that registers **CodeBuddy** as a model provider. You keep using Pi for the TUI, tools, skills, and extensions; CodeBuddy Agent SDK runs inference locally via the `codebuddy` CLI. No HTTP proxy and no changes to how you work in Pi beyond picking a model.
4
4
 
5
- ## 安装
5
+ ## What it does
6
6
 
7
- ### 方式 1:全局安装(日常使用)
7
+ - Exposes CodeBuddy models in Pi (`codebuddy/...` in `/model`)
8
+ - Bridges Pi tools to the SDK (Pi still executes tools; CodeBuddy only plans and calls them)
9
+ - Forwards Pi's system prompt, skills, and project `AGENTS.md` so the model acts as Pi—not as standalone CodeBuddy Code
10
+ - Supports session resume, compaction, streaming, thinking levels, and images
11
+ - Optional **AskCodebuddy** tool to delegate a focused sub-task to another CodeBuddy call
12
+
13
+ ## Install
8
14
 
9
15
  ```bash
10
- ln -s ~/Desktop/pi-codebuddy/src ~/.pi/agent/extensions/pi-codebuddy
11
- cd ~/Desktop/pi-codebuddy && npm install
16
+ pi install npm:pi-codebuddy-sdk
12
17
  ```
13
18
 
14
- 重启 Pi 即可自动发现。
19
+ Restart `pi` if it was already running.
15
20
 
16
- ### 方式 2:临时加载(测试)
21
+ ## Requirements
17
22
 
18
- ```bash
19
- pi -e ~/Desktop/pi-codebuddy/src/index.ts --provider codebuddy --model <model>
20
- ```
23
+ - **`codebuddy` on your `PATH`** — the extension spawns the same CLI you use standalone. If `which codebuddy` fails, either add it to `PATH` or set `pathToCodebuddyCode` in `codebuddy-sdk.json` (see [Configuration](#configuration)).
24
+ - **CodeBuddy auth already working** — if `codebuddy` works in your terminal, you do not need extra setup for this extension.
25
+
26
+ ## Quick start (already using CodeBuddy)
27
+
28
+ No `codebuddy-sdk.json` and no plugin-specific env vars are required.
29
+
30
+ 1. `pi install npm:pi-codebuddy-sdk`
31
+ 2. Restart `pi`
32
+ 3. `/model` → pick `codebuddy/...`
21
33
 
22
- ## 认证
34
+ Optional: set `defaultProvider` / `defaultModel` in `~/.pi/agent/settings.json` so you skip `/model` each time.
23
35
 
24
- CodeBuddy SDK 从本地环境读取凭证,优先级:
36
+ The first query after startup may take a few seconds while models are discovered from the SDK.
25
37
 
26
- ### 1. 已通过 CLI 登录(自动,无需配置)
38
+ ## Auth
39
+
40
+ The extension does not store credentials. The CodeBuddy CLI reads them from your machine. Use **one** of the following:
41
+
42
+ ### 1. CLI login (recommended)
27
43
 
28
44
  ```bash
29
- # 如果已登录 CodeBuddy CLI,SDK 自动复用凭证
30
45
  codebuddy login
31
46
  ```
32
47
 
33
- ### 2. API Key(手动设置)
48
+ Complete login in the browser. Credentials stay on your machine; Pi reuses them automatically.
49
+
50
+ ### 2. API key
34
51
 
35
52
  ```bash
36
- # 国际版
37
- export CODEBUDDY_API_KEY="your-key"
53
+ export CODEBUDDY_API_KEY="your-api-key"
54
+ ```
55
+
56
+ Obtain the key from your CodeBuddy account. Do not commit it or store it in repo files.
38
57
 
39
- # 中国版
40
- export CODEBUDDY_API_KEY="your-key"
41
- export CODEBUDDY_INTERNET_ENVIRONMENT=internal
58
+ ### 3. Tencent iOA (internal network)
42
59
 
43
- # iOA 版(企业内)
44
- export CODEBUDDY_API_KEY="your-key"
60
+ ```bash
45
61
  export CODEBUDDY_INTERNET_ENVIRONMENT=ioa
62
+ codebuddy login
46
63
  ```
47
64
 
48
- **获取 API Key:**
65
+ Use this when CodeBuddy must run on the iOA network. Add `CODEBUDDY_API_KEY` as well if your environment requires it.
49
66
 
50
- | 版本 | 地址 |
51
- |------|------|
52
- | 国际版 | https://www.codebuddy.ai/profile/keys |
53
- | 中国版 | https://copilot.tencent.com/profile/ |
54
- | iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
67
+ ## Usage
55
68
 
56
- ## 使用
69
+ ```text
70
+ pi
71
+ /model
72
+ ```
57
73
 
58
- ```bash
59
- # 列出 codebuddy 所有模型
60
- pi --list-models
74
+ Pick any entry prefixed with `codebuddy/` (for example `codebuddy/hy3-preview-agent-ioa`).
61
75
 
62
- # 使用指定模型
63
- pi --provider codebuddy --model claude-sonnet-4.6
76
+ Tools, skills, extensions, `/compact`, and steer behave the same as with other Pi providers.
64
77
 
65
- # 或进入 Pi 后切换
66
- /model codebuddy/claude-sonnet-4.6
67
- ```
78
+ ## Configuration
68
79
 
69
- ### 推荐模型
80
+ **Optional.** Defaults work for most users.
70
81
 
71
- | 模型 | 说明 |
72
- |------|------|
73
- | `claude-sonnet-4.6` | Claude Sonnet 4.6,支持 thinking |
74
- | `claude-opus-4.8` | Claude Opus 4.8,支持 thinking + image |
75
- | `gemini-3.1-pro` | Gemini 3.1 Pro,支持 thinking + image |
76
- | `gpt-5.5` | GPT-5.5,支持 thinking + image |
77
- | `hy3-preview-agent-ioa` | 混元 3 Preview,iOA 内免费无限额度 |
82
+ File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json` in a project.
78
83
 
79
- ## 特性
84
+ ```json
85
+ {
86
+ "askCodebuddy": {
87
+ "enabled": true,
88
+ "allowFullMode": true
89
+ },
90
+ "provider": {
91
+ "appendSystemPrompt": true,
92
+ "strictMcpConfig": true,
93
+ "pathToCodebuddyCode": "/path/to/codebuddy"
94
+ }
95
+ }
96
+ ```
97
+
98
+ | Option | Default | Meaning |
99
+ |--------|---------|---------|
100
+ | `askCodebuddy.enabled` | `true` | Register the AskCodebuddy delegation tool |
101
+ | `askCodebuddy.allowFullMode` | `true` | Allow write-capable delegation mode |
102
+ | `provider.appendSystemPrompt` | `true` | Use Pi's system prompt instead of CodeBuddy's default identity |
103
+ | `provider.strictMcpConfig` | `true` | Use only Pi-bridged MCP tools |
104
+ | `provider.pathToCodebuddyCode` | auto | Path to `codebuddy` when it is **not** on `PATH` |
105
+
106
+ ## Privacy
80
107
 
81
- | 特性 | 说明 |
82
- |------|------|
83
- | **动态模型发现** | 启动时从 CodeBuddy SDK 拉取 29 个模型,自动标注 thinking/image 能力 |
84
- | **Session 池复用** | 多轮对话复用同一 CodeBuddy CLI 进程,首轮后每轮省 ~6s 启动 |
85
- | **Thinking 控制** | Pi 的 thinking level 映射到 CodeBuddy 的 thinking/effort 参数 |
86
- | **Image 输入** | 原生支持 Claude/Gemini/GPT 的多模态图片输入 |
87
- | **Abort 处理** | Esc 取消 → CodeBuddy `interrupt()`,干净终止 |
88
- | **Token 统计** | 包含 cache_read / cache_write / total_cost_usd |
89
- | **YOLO 模式** | `bypassPermissions`,所有工具自动批准(与 pi-cursor-sdk 一致) |
108
+ - The extension does **not** send conversation data to this repository or any third-party telemetry endpoint.
109
+ - Credentials are handled entirely by the CodeBuddy CLI on your machine.
110
+ - Optional debug mode (`CODEBUDDY_SDK_DEBUG=1`) writes **local** logs under `~/.pi/agent/`; paths are redacted, prompts and tool payloads are not logged. Delete logs when finished.
90
111
 
91
- ## FAQ
112
+ ## Troubleshooting
92
113
 
93
- **Q: 和 pi-cursor-sdk 有什么区别?**
114
+ ```bash
115
+ export CODEBUDDY_SDK_DEBUG=1
116
+ ```
94
117
 
95
- pi-cursor-sdk 需要自建 MCP bridge 桥接 Cursor SDK 工具,pi-codebuddy SDK 原生处理工具执行,代码量减少 60%+。
118
+ Default log: `~/.pi/agent/codebuddy-sdk.log`. See [CONTRIBUTING.md](CONTRIBUTING.md) for maintainer details.
96
119
 
97
- **Q: 怎么查看当前用了多少 tokens?**
120
+ ## Development
98
121
 
99
- 每次回复后 Pi 会显示 usage,包含 input/output/cache/total 及费用。
122
+ Maintainers: [CONTRIBUTING.md](CONTRIBUTING.md).
100
123
 
101
- **Q: maxTurns 限制?**
124
+ ## License
102
125
 
103
- 100(安全上限)。CodeBuddy agent 完成任务会自动停止,不会循环满。
126
+ MIT
104
127
 
105
- **Q: 支持哪些 CodeBuddy 版本?**
128
+ ## Inspiration
106
129
 
107
- CodeBuddy CLI ≥ 2.x(`@tencent-ai/agent-sdk` ≥ 0.3.0)。
130
+ Early MCP bridge patterns were inspired by [pi-claude-bridge](https://github.com/elidickinson/pi-claude-bridge). This package is a separate codebase on [@tencent-ai/agent-sdk](https://www.npmjs.com/package/@tencent-ai/agent-sdk).
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.1.18",
4
- "description": "CodeBuddy Agent SDK provider for Pi — native tool execution, 29+ models, thinking controls, session pool",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension that uses CodeBuddy (via Agent SDK) as a model provider and adds an AskCodebuddy tool.",
5
5
  "author": "slahser",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
- "repository": "github:RealAlexandreAI/pi-codebuddy-sdk",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/RealAlexandreAI/pi-codebuddy-sdk.git"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
9
15
  "keywords": [
10
16
  "pi-package",
11
17
  "pi",
@@ -16,34 +22,36 @@
16
22
  "provider"
17
23
  ],
18
24
  "files": [
19
- "src"
25
+ "src",
26
+ "README.md",
27
+ "LICENSE"
20
28
  ],
29
+ "scripts": {
30
+ "test:unit": "node --import tsx --test tests/unit-*.mjs",
31
+ "test": "set -a && [ -f .env.test ] && . .env.test; set +a && npm run test:unit && tests/int-smoke.sh && tests/int-multi-turn.sh && tests/int-cache.sh && node --import tsx --test tests/int-*.mjs",
32
+ "typecheck": "tsc --noEmit"
33
+ },
21
34
  "dependencies": {
22
- "@tencent-ai/agent-sdk": "^0.3.0"
35
+ "@tencent-ai/agent-sdk": "^0.3.0",
36
+ "change-case": "^5.4.4",
37
+ "typebox": "^1.3.0"
23
38
  },
24
39
  "peerDependencies": {
25
- "@earendil-works/pi-ai": "*",
26
- "@earendil-works/pi-coding-agent": "*",
27
- "typebox": "*"
40
+ "@earendil-works/pi-ai": ">=0.80.0",
41
+ "@earendil-works/pi-coding-agent": ">=0.80.0",
42
+ "@earendil-works/pi-tui": ">=0.80.0"
28
43
  },
29
- "peerDependenciesMeta": {
30
- "@earendil-works/pi-ai": {
31
- "optional": true
32
- },
33
- "@earendil-works/pi-coding-agent": {
34
- "optional": true
35
- },
36
- "typebox": {
37
- "optional": true
38
- }
44
+ "devDependencies": {
45
+ "@earendil-works/pi-ai": "^0.80.2",
46
+ "@earendil-works/pi-coding-agent": "^0.80.2",
47
+ "@earendil-works/pi-tui": "^0.80.2",
48
+ "@types/node": "^24.13.2",
49
+ "tsx": "^4.22.4",
50
+ "typescript": "^6.0.3"
39
51
  },
40
52
  "pi": {
41
53
  "extensions": [
42
54
  "./src/index.ts"
43
55
  ]
44
- },
45
- "devDependencies": {
46
- "@earendil-works/pi-ai": "^0.80.2",
47
- "@earendil-works/pi-coding-agent": "^0.80.2"
48
56
  }
49
57
  }
@@ -0,0 +1,54 @@
1
+ // AGENTS.md discovery and sanitization for forwarding to Claude Code.
2
+ //
3
+ // Pi uses AGENTS.md for long-lived instructions; Claude Code reads the same
4
+ // content under "# CLAUDE.md". We walk up from cwd looking for AGENTS.md,
5
+ // fall back to ~/.pi/agent/AGENTS.md, and rewrite pi-specific references
6
+ // (~/.pi, .pi/, .pi, pi) to their Claude Code equivalents so any paths or
7
+ // references in the file still resolve inside the CC subprocess.
8
+
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { dirname, join, resolve } from "path";
12
+
13
+ const GLOBAL_AGENTS_PATH = join(homedir(), ".pi", "agent", "AGENTS.md");
14
+
15
+ export function resolveAgentsMdPath(): string | undefined {
16
+ const fromCwd = findAgentsMdInParents(process.cwd());
17
+ if (fromCwd) return fromCwd;
18
+ if (existsSync(GLOBAL_AGENTS_PATH)) return GLOBAL_AGENTS_PATH;
19
+ return undefined;
20
+ }
21
+
22
+ export function findAgentsMdInParents(startDir: string): string | undefined {
23
+ let current = resolve(startDir);
24
+ while (true) {
25
+ const candidate = join(current, "AGENTS.md");
26
+ if (existsSync(candidate)) return candidate;
27
+ const parent = dirname(current);
28
+ if (parent === current) break;
29
+ current = parent;
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ export function extractAgentsAppend(): string | undefined {
35
+ const agentsPath = resolveAgentsMdPath();
36
+ if (!agentsPath) return undefined;
37
+ try {
38
+ const content = readFileSync(agentsPath, "utf-8").trim();
39
+ if (!content) return undefined;
40
+ const sanitized = sanitizeAgentsContent(content);
41
+ return sanitized.length > 0 ? `# CLAUDE.md\n\n${sanitized}` : undefined;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ export function sanitizeAgentsContent(content: string): string {
48
+ let sanitized = content;
49
+ sanitized = sanitized.replace(/~\/\.pi\b/gi, "~/.claude");
50
+ sanitized = sanitized.replace(/(^|[\s'"`])\.pi\//g, "$1.claude/");
51
+ sanitized = sanitized.replace(/\b\.pi\b/gi, ".claude");
52
+ sanitized = sanitized.replace(/\bpi\b/gi, "environment");
53
+ return sanitized;
54
+ }
@@ -0,0 +1,90 @@
1
+ // Status-line rendering helpers for the AskCodebuddy tool.
2
+ //
3
+ // While CodeBuddy runs inside an AskCodebuddy call, the pi TUI can't surface
4
+ // each tool_use individually — there's only one status row for the whole
5
+ // delegation. These helpers shape a tool_use record into a short, path-aware
6
+ // label (e.g. "Read(src/foo.ts)", "Bash(git log --oneline…)") and collapse
7
+ // runs of the same tool so the line doesn't flicker. Used only by
8
+ // promptAndWait; the provider path exposes tools directly through pi's TUI
9
+ // and doesn't need this.
10
+
11
+ export interface ToolCallState {
12
+ name: string;
13
+ status: string;
14
+ rawInput?: unknown;
15
+ }
16
+
17
+ export function extractPath(rawInput: unknown): string | undefined {
18
+ if (!rawInput || typeof rawInput !== "object") return undefined;
19
+ const input = rawInput as Record<string, unknown>;
20
+ if (typeof input.file_path === "string") return input.file_path;
21
+ if (typeof input.path === "string") return input.path;
22
+ if (typeof input.command === "string") return input.command.substring(0, 80);
23
+ return undefined;
24
+ }
25
+
26
+ export function shortPath(p: string): string {
27
+ const cwd = process.cwd();
28
+ if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
29
+ if (p.startsWith("/")) {
30
+ const parts = p.split("/");
31
+ if (parts.length > 3) return parts.slice(-2).join("/");
32
+ }
33
+ return p;
34
+ }
35
+
36
+ export function formatToolAction(tc: ToolCallState): string | undefined {
37
+ const path = extractPath(tc.rawInput);
38
+ const verb = tc.name.toLowerCase().split(/\s/)[0];
39
+ if (verb === "read" || verb === "readfile") {
40
+ return path ? `Read(${shortPath(path)})` : "Read";
41
+ } else if (verb === "glob") {
42
+ const input = tc.rawInput as Record<string, unknown> | undefined;
43
+ const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
44
+ return pat ? `Glob(${pat})` : "Glob";
45
+ } else if (verb === "edit" || verb === "write" || verb === "writefile" || verb === "multiedit") {
46
+ return path ? `Edit(${shortPath(path)})` : "Edit";
47
+ } else if (verb === "bashoutput") {
48
+ return undefined; // redundant with preceding Bash call
49
+ } else if (verb === "bash" || verb === "terminal") {
50
+ return path ? `Bash(${path})` : "Bash";
51
+ } else if (verb === "agent") {
52
+ const input = tc.rawInput as Record<string, unknown> | undefined;
53
+ return `Agent(${String(input?.description ?? "").slice(0, 40)})`;
54
+ } else if (verb === "grep") {
55
+ const input = tc.rawInput as Record<string, unknown> | undefined;
56
+ const pat = typeof input?.pattern === "string" ? input.pattern.slice(0, 40) : "";
57
+ return pat ? `Grep(${pat})` : "Grep";
58
+ } else if (verb === "skill") {
59
+ const input = tc.rawInput as Record<string, unknown> | undefined;
60
+ const name = typeof input?.skill === "string" ? input.skill.slice(0, 40) : "";
61
+ return name ? `Skill(${name})` : "Skill";
62
+ } else if (verb === "todowrite" || verb === "taskcreate" || verb === "taskupdate") {
63
+ const todos = Array.isArray((tc.rawInput as any)?.todos) ? (tc.rawInput as any).todos : [];
64
+ const current = todos.find((t: any) => t.status === "in_progress") ?? todos.find((t: any) => t.status === "pending");
65
+ const label = current ? String(current.content ?? "").slice(0, 40) : "";
66
+ return label || undefined;
67
+ } else if (verb === "askclaude") {
68
+ // Recursive — don't show AskCodebuddy in its own action summary
69
+ return undefined;
70
+ }
71
+ return tc.name;
72
+ }
73
+
74
+ export function buildActionSummary(calls: Map<string, ToolCallState>): string {
75
+ const parts: string[] = [];
76
+ let prevVerb = "";
77
+ for (const [, tc] of calls) {
78
+ const action = formatToolAction(tc);
79
+ if (!action) continue;
80
+ const verb = tc.name.toLowerCase().split(/\s/)[0];
81
+ // Collapse consecutive calls to the same tool — keep only the latest
82
+ if (verb === prevVerb) {
83
+ parts[parts.length - 1] = action;
84
+ } else {
85
+ parts.push(action);
86
+ }
87
+ prevVerb = verb;
88
+ }
89
+ return parts.join("; ");
90
+ }
@@ -0,0 +1,205 @@
1
+ // CodeBuddy session JSONL read/write for pi session sync (replaces cc-session-io).
2
+ // CodeBuddy stores sessions at ~/.codebuddy/projects/<sanitized-path>/<sessionId>.jsonl
3
+
4
+ import { randomUUID } from "crypto";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import type { Message as PiMessage } from "@earendil-works/pi-ai";
9
+ import { convertPiMessages, messageContentToText } from "./convert.js";
10
+
11
+ export interface CbMessage {
12
+ role: "user" | "assistant";
13
+ content: string;
14
+ }
15
+
16
+ export function getCodebuddyDir(codebuddyDir?: string): string {
17
+ return codebuddyDir ?? process.env.CODEBUDDY_CONFIG_DIR ?? join(homedir(), ".codebuddy");
18
+ }
19
+
20
+ export function normalizeProjectPath(projectPath: string): string {
21
+ return projectPath.replace(/\/+$/, "") || projectPath;
22
+ }
23
+
24
+ export function projectPathToHash(projectPath: string): string {
25
+ return normalizeProjectPath(projectPath).replace(/^\//, "").replace(/\//g, "-");
26
+ }
27
+
28
+ export function getProjectDir(projectPath: string, codebuddyDir?: string): string {
29
+ return join(getCodebuddyDir(codebuddyDir), "projects", projectPathToHash(projectPath));
30
+ }
31
+
32
+ export function getSessionPath(sessionId: string, projectPath: string, codebuddyDir?: string): string {
33
+ return join(getProjectDir(projectPath, codebuddyDir), `${sessionId}.jsonl`);
34
+ }
35
+
36
+ export function deleteSession(sessionId: string, projectPath: string, codebuddyDir?: string): void {
37
+ const jsonlPath = getSessionPath(sessionId, projectPath, codebuddyDir);
38
+ const dir = join(getProjectDir(projectPath, codebuddyDir), sessionId);
39
+ try { rmSync(jsonlPath, { force: true }); } catch { /* ignore */ }
40
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
41
+ }
42
+
43
+ function piToCbMessages(messages: PiMessage[]): CbMessage[] {
44
+ const { anthropicMessages } = convertPiMessages(messages);
45
+ const out: CbMessage[] = [];
46
+ for (const msg of anthropicMessages) {
47
+ if (msg.role === "user") {
48
+ const text = typeof msg.content === "string"
49
+ ? msg.content
50
+ : msg.content.map((b: any) => b.type === "text" ? b.text : b.type === "tool_result" ? `[tool_result:${b.tool_use_id}]` : `[${b.type}]`).join("\n");
51
+ out.push({ role: "user", content: text });
52
+ } else if (msg.role === "assistant") {
53
+ const text = typeof msg.content === "string"
54
+ ? msg.content
55
+ : msg.content.map((b: any) => b.type === "text" ? b.text : b.type === "tool_use" ? `[tool:${b.name}]` : `[${b.type}]`).join("\n");
56
+ out.push({ role: "assistant", content: text });
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function writeCbJsonl(sessionId: string, cwd: string, messages: CbMessage[]): string {
63
+ const jsonlPath = getSessionPath(sessionId, cwd);
64
+ mkdirSync(join(jsonlPath, ".."), { recursive: true });
65
+ const lines: string[] = [];
66
+ let parentId: string | undefined;
67
+ const ts = Date.now();
68
+ for (const msg of messages) {
69
+ const id = randomUUID();
70
+ if (msg.role === "user") {
71
+ lines.push(JSON.stringify({
72
+ id, timestamp: ts, type: "message", role: "user",
73
+ content: [{ type: "input_text", text: msg.content }],
74
+ providerData: { agent: "sdk" }, sessionId, cwd,
75
+ }));
76
+ parentId = id;
77
+ } else {
78
+ lines.push(JSON.stringify({
79
+ id, parentId, timestamp: ts, type: "message", role: "assistant",
80
+ status: "completed",
81
+ content: [{ type: "output_text", text: msg.content }],
82
+ providerData: { agent: "sdk" }, sessionId, cwd,
83
+ }));
84
+ parentId = id;
85
+ }
86
+ }
87
+ writeFileSync(jsonlPath, lines.join("\n") + (lines.length ? "\n" : ""));
88
+ return jsonlPath;
89
+ }
90
+
91
+ export class Session {
92
+ readonly sessionId: string;
93
+ readonly projectPath: string;
94
+ readonly codebuddyDir?: string;
95
+ readonly jsonlPath: string;
96
+ messages: CbMessage[] = [];
97
+
98
+ constructor(sessionId: string, projectPath: string, codebuddyDir?: string) {
99
+ this.sessionId = sessionId;
100
+ this.projectPath = projectPath;
101
+ this.codebuddyDir = codebuddyDir;
102
+ this.jsonlPath = getSessionPath(sessionId, projectPath, codebuddyDir);
103
+ }
104
+
105
+ importPiMessages(messages: PiMessage[]): void {
106
+ this.messages = piToCbMessages(messages);
107
+ }
108
+
109
+ save(): void {
110
+ writeCbJsonl(this.sessionId, this.projectPath, this.messages);
111
+ }
112
+ }
113
+
114
+ export interface CreateSessionOptions {
115
+ projectPath: string;
116
+ sessionId?: string;
117
+ model?: string;
118
+ codebuddyDir?: string;
119
+ }
120
+
121
+ export function createSession(opts: CreateSessionOptions): Session {
122
+ const sessionId = opts.sessionId ?? randomUUID();
123
+ return new Session(sessionId, opts.projectPath, opts.codebuddyDir);
124
+ }
125
+
126
+ export function repairToolPairing(messages: Array<{ role: string; content: unknown }>): Array<{ role: string; content: unknown }> {
127
+ const result: Array<{ role: string; content: unknown }> = [];
128
+ let pending: Set<string> | null = null;
129
+ const synthetic = (id: string) => ({
130
+ type: "tool_result",
131
+ tool_use_id: id,
132
+ content: "[no tool result recorded]",
133
+ is_error: true,
134
+ });
135
+ const flushPending = () => {
136
+ if (pending && pending.size > 0) {
137
+ result.push({ role: "user", content: [...pending].map(synthetic) });
138
+ }
139
+ pending = null;
140
+ };
141
+ for (const msg of messages) {
142
+ if (msg.role === "assistant") {
143
+ flushPending();
144
+ const ids = new Set<string>();
145
+ if (Array.isArray(msg.content)) {
146
+ for (const b of msg.content as Array<{ type?: string; id?: string }>) {
147
+ if (b.type === "tool_use" && typeof b.id === "string") ids.add(b.id);
148
+ }
149
+ }
150
+ result.push(msg);
151
+ pending = ids.size > 0 ? ids : null;
152
+ continue;
153
+ }
154
+ const blocks = Array.isArray(msg.content) ? msg.content as Array<{ type?: string; tool_use_id?: string }> : null;
155
+ const hasToolResults = blocks?.some((b) => b.type === "tool_result") ?? false;
156
+ if (!pending && !hasToolResults) {
157
+ result.push(msg);
158
+ continue;
159
+ }
160
+ const input = blocks ?? (typeof msg.content === "string" && msg.content ? [{ type: "text", text: msg.content }] : []);
161
+ const provided = new Set<string>();
162
+ const kept = input.filter((b: { type?: string; tool_use_id?: string }) => {
163
+ if (b.type !== "tool_result") return true;
164
+ if (b.tool_use_id && pending?.has(b.tool_use_id)) {
165
+ provided.add(b.tool_use_id);
166
+ return true;
167
+ }
168
+ return false;
169
+ });
170
+ if (pending) {
171
+ const missing = [...pending].filter((id) => !provided.has(id)).map(synthetic);
172
+ kept.unshift(...missing);
173
+ pending = null;
174
+ }
175
+ if (kept.length === 0) {
176
+ if (result.length === 0) {
177
+ result.push({ role: "user", content: [{ type: "text", text: "[orphaned tool result removed]" }] });
178
+ }
179
+ continue;
180
+ }
181
+ result.push({ ...msg, content: kept });
182
+ }
183
+ flushPending();
184
+ return result;
185
+ }
186
+
187
+ export function readSession(sessionId: string, projectPath: string, codebuddyDir?: string): Session | null {
188
+ const jsonlPath = getSessionPath(sessionId, projectPath, codebuddyDir);
189
+ if (!existsSync(jsonlPath)) return null;
190
+ const session = new Session(sessionId, projectPath, codebuddyDir);
191
+ const lines = readFileSync(jsonlPath, "utf-8").split("\n").filter(Boolean);
192
+ for (const line of lines) {
193
+ try {
194
+ const rec = JSON.parse(line);
195
+ if (rec.type === "message" && rec.role === "user") {
196
+ const text = rec.content?.find((b: any) => b.type === "input_text")?.text ?? messageContentToText(rec.content);
197
+ session.messages.push({ role: "user", content: text });
198
+ } else if (rec.type === "message" && rec.role === "assistant") {
199
+ const text = rec.content?.find((b: any) => b.type === "output_text")?.text ?? messageContentToText(rec.content);
200
+ session.messages.push({ role: "assistant", content: text });
201
+ }
202
+ } catch { /* skip malformed */ }
203
+ }
204
+ return session;
205
+ }