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/LICENSE +21 -0
- package/README.md +87 -61
- package/package.json +30 -23
- package/src/agents-md.ts +39 -24
- package/src/askcodebuddy-ui.ts +3 -3
- package/src/cb-session-io.ts +205 -0
- package/src/config.ts +37 -33
- package/src/convert.ts +79 -83
- package/src/extract-tool-results.ts +47 -0
- package/src/index-types.ts +1 -0
- package/src/index.ts +273 -227
- package/src/models.ts +64 -11
- package/src/query-state.ts +48 -48
- package/src/sdk-gate.ts +17 -0
- package/src/session-verify.ts +38 -0
- package/src/skills.ts +65 -13
- package/src/typebox-to-zod.ts +42 -0
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,104 +1,130 @@
|
|
|
1
1
|
# pi-codebuddy-sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Install
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
|
-
|
|
13
|
-
cd ~/Desktop/usaslahser/pi-codebuddy-sdk && npm install
|
|
16
|
+
pi install npm:pi-codebuddy-sdk
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Restart `pi` if it was already running.
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
## Requirements
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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/...`
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
Optional: set `defaultProvider` / `defaultModel` in `~/.pi/agent/settings.json` so you skip `/model` each time.
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
The first query after startup may take a few seconds while models are discovered from the SDK.
|
|
27
37
|
|
|
28
|
-
|
|
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)
|
|
29
43
|
|
|
30
44
|
```bash
|
|
31
|
-
# 如果已登录 CodeBuddy CLI,SDK 自动复用凭证
|
|
32
45
|
codebuddy login
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
Complete login in the browser. Credentials stay on your machine; Pi reuses them automatically.
|
|
49
|
+
|
|
50
|
+
### 2. API key
|
|
36
51
|
|
|
37
52
|
```bash
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
export CODEBUDDY_API_KEY="your-key"
|
|
43
|
-
export CODEBUDDY_INTERNET_ENVIRONMENT=internal
|
|
58
|
+
### 3. Tencent iOA (internal network)
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
export CODEBUDDY_API_KEY="your-key"
|
|
60
|
+
```bash
|
|
47
61
|
export CODEBUDDY_INTERNET_ENVIRONMENT=ioa
|
|
62
|
+
codebuddy login
|
|
48
63
|
```
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
Use this when CodeBuddy must run on the iOA network. Add `CODEBUDDY_API_KEY` as well if your environment requires it.
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|------|------|
|
|
54
|
-
| 国际版 | https://www.codebuddy.ai/profile/keys |
|
|
55
|
-
| 中国版 | https://copilot.tencent.com/profile/ |
|
|
56
|
-
| iOA | https://tencent.sso.copilot.tencent.com/profile/keys |
|
|
67
|
+
## Usage
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
```text
|
|
70
|
+
pi
|
|
71
|
+
/model
|
|
72
|
+
```
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
Pick any entry prefixed with `codebuddy/` (for example `codebuddy/hy3-preview-agent-ioa`).
|
|
75
|
+
|
|
76
|
+
Tools, skills, extensions, `/compact`, and steer behave the same as with other Pi providers.
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
pi --provider codebuddy --model claude-sonnet-4.6
|
|
78
|
+
## Configuration
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
**Optional.** Defaults work for most users.
|
|
81
|
+
|
|
82
|
+
File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json` in a project.
|
|
83
|
+
|
|
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
|
+
}
|
|
69
96
|
```
|
|
70
97
|
|
|
71
|
-
|
|
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
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| `hy3-preview-agent-ioa` | 混元 3 Preview,iOA 内免费无限额度 |
|
|
77
|
-
| `deepseek-v4-pro-ioa` | DeepSeek V4 Pro |
|
|
78
|
-
| `glm-5.2-ioa` | GLM 5.2 |
|
|
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.
|
|
79
111
|
|
|
80
|
-
##
|
|
112
|
+
## Troubleshooting
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 一致) |
|
|
90
|
-
| **Stateless** | 每次调用独立 query(),无 session 复用,简单可靠 |
|
|
114
|
+
```bash
|
|
115
|
+
export CODEBUDDY_SDK_DEBUG=1
|
|
116
|
+
```
|
|
91
117
|
|
|
92
|
-
|
|
118
|
+
Default log: `~/.pi/agent/codebuddy-sdk.log`. See [CONTRIBUTING.md](CONTRIBUTING.md) for maintainer details.
|
|
93
119
|
|
|
94
|
-
|
|
120
|
+
## Development
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
Maintainers: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
97
123
|
|
|
98
|
-
|
|
124
|
+
## License
|
|
99
125
|
|
|
100
|
-
|
|
126
|
+
MIT
|
|
101
127
|
|
|
102
|
-
|
|
128
|
+
## Inspiration
|
|
103
129
|
|
|
104
|
-
|
|
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
|
|
4
|
-
"description": "CodeBuddy Agent SDK
|
|
3
|
+
"version": "0.3.1",
|
|
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":
|
|
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,35 +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
|
-
"
|
|
40
|
+
"@earendil-works/pi-ai": ">=0.83.0",
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=0.83.0",
|
|
42
|
+
"@earendil-works/pi-tui": ">=0.83.0"
|
|
28
43
|
},
|
|
29
|
-
"
|
|
30
|
-
"@earendil-works/pi-ai":
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"@
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"typebox": {
|
|
37
|
-
"optional": true
|
|
38
|
-
}
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@earendil-works/pi-ai": "^0.83.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
47
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
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
|
-
"@earendil-works/pi-tui": "^0.80.2"
|
|
49
56
|
}
|
|
50
57
|
}
|
package/src/agents-md.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
// AGENTS.md discovery for forwarding to
|
|
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.
|
|
2
8
|
|
|
3
9
|
import { existsSync, readFileSync } from "fs";
|
|
4
10
|
import { homedir } from "os";
|
|
@@ -7,33 +13,42 @@ import { dirname, join, resolve } from "path";
|
|
|
7
13
|
const GLOBAL_AGENTS_PATH = join(homedir(), ".pi", "agent", "AGENTS.md");
|
|
8
14
|
|
|
9
15
|
export function resolveAgentsMdPath(): string | undefined {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
const fromCwd = findAgentsMdInParents(process.cwd());
|
|
17
|
+
if (fromCwd) return fromCwd;
|
|
18
|
+
if (existsSync(GLOBAL_AGENTS_PATH)) return GLOBAL_AGENTS_PATH;
|
|
19
|
+
return undefined;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export function findAgentsMdInParents(startDir: string): string | undefined {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export function extractAgentsAppend(): string | undefined {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
54
|
}
|
package/src/askcodebuddy-ui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Status-line rendering helpers for the
|
|
1
|
+
// Status-line rendering helpers for the AskCodebuddy tool.
|
|
2
2
|
//
|
|
3
|
-
// While
|
|
3
|
+
// While CodeBuddy runs inside an AskCodebuddy call, the pi TUI can't surface
|
|
4
4
|
// each tool_use individually — there's only one status row for the whole
|
|
5
5
|
// delegation. These helpers shape a tool_use record into a short, path-aware
|
|
6
6
|
// label (e.g. "Read(src/foo.ts)", "Bash(git log --oneline…)") and collapse
|
|
@@ -65,7 +65,7 @@ export function formatToolAction(tc: ToolCallState): string | undefined {
|
|
|
65
65
|
const label = current ? String(current.content ?? "").slice(0, 40) : "";
|
|
66
66
|
return label || undefined;
|
|
67
67
|
} else if (verb === "askclaude") {
|
|
68
|
-
// Recursive — don't show
|
|
68
|
+
// Recursive — don't show AskCodebuddy in its own action summary
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
71
71
|
return tc.name;
|
|
@@ -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
|
+
}
|