@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/LICENSE +21 -0
- package/README.md +135 -0
- package/package.json +65 -0
- package/src/agents-md.ts +54 -0
- package/src/askcodebuddy-ui.ts +90 -0
- package/src/cb-session-io.ts +205 -0
- package/src/config.ts +50 -0
- package/src/convert.ts +101 -0
- package/src/extract-tool-results.ts +47 -0
- package/src/index-types.ts +1 -0
- package/src/index.ts +1979 -0
- package/src/model-calibration.ts +129 -0
- package/src/models.ts +79 -0
- package/src/query-state.ts +80 -0
- package/src/sdk-gate.ts +17 -0
- package/src/session-verify.ts +38 -0
- package/src/skills.ts +155 -0
- package/src/typebox-to-zod.ts +160 -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
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @raoxxxwq/pi-codebuddy-sdk
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
- Repository: `https://github.com/MuJianxuan/pi-codebuddy-sdk`
|
|
6
|
+
- Author: `raoxxxwq`
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- Exposes CodeBuddy models in Pi (`codebuddy/...` in `/model`)
|
|
11
|
+
- Bridges Pi tools to the SDK (Pi still executes tools; CodeBuddy only plans and calls them)
|
|
12
|
+
- Forwards Pi's system prompt, skills, and project `AGENTS.md` so the model acts as Pi—not as standalone CodeBuddy Code
|
|
13
|
+
- Supports session resume, compaction, streaming, thinking levels, and images
|
|
14
|
+
- Learns runtime-served context windows over time and keeps Pi's registered model metadata conservatively aligned with observed reality
|
|
15
|
+
- Optional **AskCodebuddy** tool to delegate a focused sub-task to another CodeBuddy call
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@raoxxxwq/pi-codebuddy-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Restart `pi` if it was already running.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- **`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)).
|
|
28
|
+
- **CodeBuddy auth already working** — if `codebuddy` works in your terminal, you do not need extra setup for this extension.
|
|
29
|
+
|
|
30
|
+
## Quick start (already using CodeBuddy)
|
|
31
|
+
|
|
32
|
+
No `codebuddy-sdk.json` and no plugin-specific env vars are required.
|
|
33
|
+
|
|
34
|
+
1. `pi install npm:@raoxxxwq/pi-codebuddy-sdk`
|
|
35
|
+
2. Restart `pi`
|
|
36
|
+
3. `/model` → pick `codebuddy/...`
|
|
37
|
+
|
|
38
|
+
Optional: set `defaultProvider` / `defaultModel` in `~/.pi/agent/settings.json` so you skip `/model` each time.
|
|
39
|
+
|
|
40
|
+
The first query after startup may take a few seconds while models are discovered from the SDK.
|
|
41
|
+
|
|
42
|
+
## Auth
|
|
43
|
+
|
|
44
|
+
The extension does not store credentials. The CodeBuddy CLI reads them from your machine. Use **one** of the following:
|
|
45
|
+
|
|
46
|
+
### 1. CLI login (recommended)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
codebuddy login
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Complete login in the browser. Credentials stay on your machine; Pi reuses them automatically.
|
|
53
|
+
|
|
54
|
+
### 2. API key
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
export CODEBUDDY_API_KEY="your-api-key"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Obtain the key from your CodeBuddy account. Do not commit it or store it in repo files.
|
|
61
|
+
|
|
62
|
+
### 3. Tencent iOA (internal network)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export CODEBUDDY_INTERNET_ENVIRONMENT=ioa
|
|
66
|
+
codebuddy login
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use this when CodeBuddy must run on the iOA network. Add `CODEBUDDY_API_KEY` as well if your environment requires it.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
pi
|
|
75
|
+
/model
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Pick any entry prefixed with `codebuddy/` (for example `codebuddy/hy3-preview-agent-ioa`).
|
|
79
|
+
|
|
80
|
+
Tools, skills, extensions, `/compact`, and steer behave the same as with other Pi providers.
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
**Optional.** Defaults work for most users.
|
|
85
|
+
|
|
86
|
+
File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json` in a project.
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"askCodebuddy": {
|
|
91
|
+
"enabled": true,
|
|
92
|
+
"allowFullMode": true
|
|
93
|
+
},
|
|
94
|
+
"provider": {
|
|
95
|
+
"appendSystemPrompt": true,
|
|
96
|
+
"strictMcpConfig": true,
|
|
97
|
+
"pathToCodebuddyCode": "/path/to/codebuddy"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Option | Default | Meaning |
|
|
103
|
+
|--------|---------|---------|
|
|
104
|
+
| `askCodebuddy.enabled` | `true` | Register the AskCodebuddy delegation tool |
|
|
105
|
+
| `askCodebuddy.allowFullMode` | `true` | Allow write-capable delegation mode |
|
|
106
|
+
| `provider.appendSystemPrompt` | `true` | Use Pi's system prompt and Pi Tool Bridge guidance instead of CodeBuddy's default identity; disable only for debugging/compatibility |
|
|
107
|
+
| `provider.strictMcpConfig` | `true` | Use only Pi-bridged MCP tools so Pi remains the tool execution boundary; disable only for debugging/compatibility |
|
|
108
|
+
| `provider.pathToCodebuddyCode` | auto | Path to `codebuddy` when it is **not** on `PATH` |
|
|
109
|
+
|
|
110
|
+
## Privacy
|
|
111
|
+
|
|
112
|
+
- The extension does **not** send conversation data to this repository or any third-party telemetry endpoint.
|
|
113
|
+
- Credentials are handled entirely by the CodeBuddy CLI on your machine.
|
|
114
|
+
- 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.
|
|
115
|
+
|
|
116
|
+
## Troubleshooting
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
export CODEBUDDY_SDK_DEBUG=1
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Default log: `~/.pi/agent/codebuddy-sdk.log`. See [CONTRIBUTING.md](CONTRIBUTING.md) for maintainer details.
|
|
123
|
+
Runtime calibration cache: `~/.pi/agent/codebuddy-sdk-model-calibration.json` stores observed model capability floors per runtime environment.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
Maintainers: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
132
|
+
|
|
133
|
+
## Inspiration
|
|
134
|
+
|
|
135
|
+
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
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raoxxxwq/pi-codebuddy-sdk",
|
|
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
|
+
"author": "raoxxxwq",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MuJianxuan/pi-codebuddy-sdk.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/MuJianxuan/pi-codebuddy-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/MuJianxuan/pi-codebuddy-sdk#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi",
|
|
25
|
+
"pi-coding-agent",
|
|
26
|
+
"extension",
|
|
27
|
+
"codebuddy",
|
|
28
|
+
"agent-sdk",
|
|
29
|
+
"provider"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test:unit": "node --import tsx --test tests/unit-*.mjs",
|
|
38
|
+
"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",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@tencent-ai/agent-sdk": "^0.3.0",
|
|
43
|
+
"change-case": "^5.4.4",
|
|
44
|
+
"typebox": "^1.3.0",
|
|
45
|
+
"zod": "^4.4.3"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-ai": ">=0.80.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0",
|
|
50
|
+
"@earendil-works/pi-tui": ">=0.80.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@earendil-works/pi-ai": "^0.80.2",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.80.2",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.80.2",
|
|
56
|
+
"@types/node": "^24.13.2",
|
|
57
|
+
"tsx": "^4.22.4",
|
|
58
|
+
"typescript": "^6.0.3"
|
|
59
|
+
},
|
|
60
|
+
"pi": {
|
|
61
|
+
"extensions": [
|
|
62
|
+
"./src/index.ts"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/agents-md.ts
ADDED
|
@@ -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
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SettingSource } from "./index-types.js";
|
|
2
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
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
|
+
|
|
14
|
+
export interface Config {
|
|
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
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function tryParseJson(path: string): Partial<Config> {
|
|
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
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadConfig(cwd: string): Config {
|
|
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
|
+
};
|
|
50
|
+
}
|