Skip to content

Commit 19c295e

Browse files
authored
Merge pull request #183 from richardanaya/master
Adding support for pi-local
2 parents fd0799f + a6b5f12 commit 19c295e

28 files changed

Lines changed: 2061 additions & 12 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@paperclipai/shared": minor
3+
---
4+
5+
Add support for Pi local adapter in constants and onboarding UI.

cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@paperclipai/adapter-codex-local": "workspace:*",
3939
"@paperclipai/adapter-cursor-local": "workspace:*",
4040
"@paperclipai/adapter-opencode-local": "workspace:*",
41+
"@paperclipai/adapter-pi-local": "workspace:*",
4142
"@paperclipai/adapter-openclaw": "workspace:*",
4243
"@paperclipai/adapter-utils": "workspace:*",
4344
"@paperclipai/db": "workspace:*",

cli/src/adapters/registry.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli";
33
import { printCodexStreamEvent } from "@paperclipai/adapter-codex-local/cli";
44
import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
55
import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli";
6+
import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli";
67
import { printOpenClawStreamEvent } from "@paperclipai/adapter-openclaw/cli";
78
import { processCLIAdapter } from "./process/index.js";
89
import { httpCLIAdapter } from "./http/index.js";
@@ -22,6 +23,11 @@ const openCodeLocalCLIAdapter: CLIAdapterModule = {
2223
formatStdoutEvent: printOpenCodeStreamEvent,
2324
};
2425

26+
const piLocalCLIAdapter: CLIAdapterModule = {
27+
type: "pi_local",
28+
formatStdoutEvent: printPiStreamEvent,
29+
};
30+
2531
const cursorLocalCLIAdapter: CLIAdapterModule = {
2632
type: "cursor",
2733
formatStdoutEvent: printCursorStreamEvent,
@@ -33,7 +39,7 @@ const openclawCLIAdapter: CLIAdapterModule = {
3339
};
3440

3541
const adaptersByType = new Map<string, CLIAdapterModule>(
36-
[claudeLocalCLIAdapter, codexLocalCLIAdapter, openCodeLocalCLIAdapter, cursorLocalCLIAdapter, openclawCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
42+
[claudeLocalCLIAdapter, codexLocalCLIAdapter, openCodeLocalCLIAdapter, piLocalCLIAdapter, cursorLocalCLIAdapter, openclawCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
3743
);
3844

3945
export function getCLIAdapter(type: string): CLIAdapterModule {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@paperclipai/adapter-pi-local",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"exports": {
6+
".": "./src/index.ts",
7+
"./server": "./src/server/index.ts",
8+
"./ui": "./src/ui/index.ts",
9+
"./cli": "./src/cli/index.ts"
10+
},
11+
"publishConfig": {
12+
"access": "public",
13+
"exports": {
14+
".": {
15+
"types": "./dist/index.d.ts",
16+
"import": "./dist/index.js"
17+
},
18+
"./server": {
19+
"types": "./dist/server/index.d.ts",
20+
"import": "./dist/server/index.js"
21+
},
22+
"./ui": {
23+
"types": "./dist/ui/index.d.ts",
24+
"import": "./dist/ui/index.js"
25+
},
26+
"./cli": {
27+
"types": "./dist/cli/index.d.ts",
28+
"import": "./dist/cli/index.js"
29+
}
30+
},
31+
"main": "./dist/index.js",
32+
"types": "./dist/index.d.ts"
33+
},
34+
"files": [
35+
"dist"
36+
],
37+
"scripts": {
38+
"build": "tsc",
39+
"clean": "rm -rf dist",
40+
"typecheck": "tsc --noEmit"
41+
},
42+
"dependencies": {
43+
"@paperclipai/adapter-utils": "workspace:*",
44+
"picocolors": "^1.1.1"
45+
},
46+
"devDependencies": {
47+
"@types/node": "^24.6.0",
48+
"typescript": "^5.7.3"
49+
}
50+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import pc from "picocolors";
2+
3+
function safeJsonParse(text: string): unknown {
4+
try {
5+
return JSON.parse(text);
6+
} catch {
7+
return null;
8+
}
9+
}
10+
11+
function asRecord(value: unknown): Record<string, unknown> | null {
12+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
13+
return value as Record<string, unknown>;
14+
}
15+
16+
function asString(value: unknown, fallback = ""): string {
17+
return typeof value === "string" ? value : fallback;
18+
}
19+
20+
function extractTextContent(content: string | Array<{ type: string; text?: string }>): string {
21+
if (typeof content === "string") return content;
22+
if (!Array.isArray(content)) return "";
23+
return content
24+
.filter((c) => c.type === "text" && c.text)
25+
.map((c) => c.text!)
26+
.join("");
27+
}
28+
29+
export function printPiStreamEvent(raw: string, _debug: boolean): void {
30+
const line = raw.trim();
31+
if (!line) return;
32+
33+
const parsed = asRecord(safeJsonParse(line));
34+
if (!parsed) {
35+
console.log(line);
36+
return;
37+
}
38+
39+
const type = asString(parsed.type);
40+
41+
if (type === "agent_start") {
42+
console.log(pc.blue("Pi agent started"));
43+
return;
44+
}
45+
46+
if (type === "agent_end") {
47+
console.log(pc.blue("Pi agent finished"));
48+
return;
49+
}
50+
51+
if (type === "turn_start") {
52+
console.log(pc.blue("Turn started"));
53+
return;
54+
}
55+
56+
if (type === "turn_end") {
57+
const message = asRecord(parsed.message);
58+
if (message) {
59+
const content = message.content as string | Array<{ type: string; text?: string }>;
60+
const text = extractTextContent(content);
61+
if (text) {
62+
console.log(pc.green(`assistant: ${text}`));
63+
}
64+
}
65+
return;
66+
}
67+
68+
if (type === "message_update") {
69+
const assistantEvent = asRecord(parsed.assistantMessageEvent);
70+
if (assistantEvent) {
71+
const msgType = asString(assistantEvent.type);
72+
if (msgType === "text_delta") {
73+
const delta = asString(assistantEvent.delta);
74+
if (delta) {
75+
console.log(pc.green(delta));
76+
}
77+
}
78+
}
79+
return;
80+
}
81+
82+
if (type === "tool_execution_start") {
83+
const toolName = asString(parsed.toolName);
84+
const args = parsed.args;
85+
console.log(pc.yellow(`tool_start: ${toolName}`));
86+
if (args !== undefined) {
87+
try {
88+
console.log(pc.gray(JSON.stringify(args, null, 2)));
89+
} catch {
90+
console.log(pc.gray(String(args)));
91+
}
92+
}
93+
return;
94+
}
95+
96+
if (type === "tool_execution_end") {
97+
const result = parsed.result;
98+
const isError = parsed.isError === true;
99+
const output = typeof result === "string" ? result : JSON.stringify(result);
100+
if (output) {
101+
console.log((isError ? pc.red : pc.gray)(output));
102+
}
103+
return;
104+
}
105+
106+
console.log(line);
107+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { printPiStreamEvent } from "./format-event.js";
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export const type = "pi_local";
2+
export const label = "Pi (local)";
3+
4+
export const models: Array<{ id: string; label: string }> = [];
5+
6+
export const agentConfigurationDoc = `# pi_local agent configuration
7+
8+
Adapter: pi_local
9+
10+
Use when:
11+
- You want Paperclip to run Pi (the AI coding agent) locally as the agent runtime
12+
- You want provider/model routing in Pi format (--provider <name> --model <id>)
13+
- You want Pi session resume across heartbeats via --session
14+
- You need Pi's tool set (read, bash, edit, write, grep, find, ls)
15+
16+
Don't use when:
17+
- You need webhook-style external invocation (use openclaw or http)
18+
- You only need one-shot shell commands (use process)
19+
- Pi CLI is not installed on the machine
20+
21+
Core fields:
22+
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
23+
- instructionsFilePath (string, optional): absolute path to a markdown instructions file appended to system prompt via --append-system-prompt
24+
- promptTemplate (string, optional): user prompt template passed via -p flag
25+
- model (string, required): Pi model id in provider/model format (for example xai/grok-4)
26+
- thinking (string, optional): thinking level (off, minimal, low, medium, high, xhigh)
27+
- command (string, optional): defaults to "pi"
28+
- env (object, optional): KEY=VALUE environment variables
29+
30+
Operational fields:
31+
- timeoutSec (number, optional): run timeout in seconds
32+
- graceSec (number, optional): SIGTERM grace period in seconds
33+
34+
Notes:
35+
- Pi supports multiple providers and models. Use \`pi --list-models\` to list available options.
36+
- Paperclip requires an explicit \`model\` value for \`pi_local\` agents.
37+
- Sessions are stored in ~/.pi/paperclips/ and resumed with --session.
38+
- All tools (read, bash, edit, write, grep, find, ls) are enabled by default.
39+
- Agent instructions are appended to Pi's system prompt via --append-system-prompt, while the user task is sent via -p.
40+
`;

0 commit comments

Comments
 (0)