Adding support for pi-local

This commit is contained in:
Richard Anaya
2026-03-06 18:29:38 -08:00
parent ffc59f5b08
commit eb7f690ceb
25 changed files with 1861 additions and 12 deletions

View File

@@ -0,0 +1,71 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
export function buildPiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
if (v.model) ac.model = v.model;
if (v.thinkingEffort) ac.thinking = v.thinkingEffort;
// Pi sessions can run until the CLI exits naturally; keep timeout disabled (0)
ac.timeoutSec = 0;
ac.graceSec = 20;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
if (Object.keys(env).length > 0) ac.env = env;
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = v.extraArgs;
if (v.args) ac.args = v.args;
return ac;
}

View File

@@ -0,0 +1,2 @@
export { parsePiStdoutLine } from "./parse-stdout.js";
export { buildPiLocalConfig } from "./build-config.js";

View File

@@ -0,0 +1,142 @@
import type { TranscriptEntry } from "@paperclipai/adapter-utils";
function safeJsonParse(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function extractTextContent(content: string | Array<{ type: string; text?: string }>): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.filter((c) => c.type === "text" && c.text)
.map((c) => c.text!)
.join("");
}
export function parsePiStdoutLine(line: string, ts: string): TranscriptEntry[] {
const parsed = asRecord(safeJsonParse(line));
if (!parsed) {
return [{ kind: "stdout", ts, text: line }];
}
const type = asString(parsed.type);
// Agent lifecycle
if (type === "agent_start") {
return [{ kind: "system", ts, text: "Pi agent started" }];
}
if (type === "agent_end") {
return [{ kind: "system", ts, text: "Pi agent finished" }];
}
// Turn lifecycle
if (type === "turn_start") {
return [{ kind: "system", ts, text: "Turn started" }];
}
if (type === "turn_end") {
const message = asRecord(parsed.message);
const toolResults = parsed.toolResults as Array<Record<string, unknown>> | undefined;
const entries: TranscriptEntry[] = [];
if (message) {
const content = message.content as string | Array<{ type: string; text?: string }>;
const text = extractTextContent(content);
if (text) {
entries.push({ kind: "assistant", ts, text });
}
}
// Process tool results
if (toolResults) {
for (const tr of toolResults) {
const content = tr.content;
const isError = tr.isError === true;
const contentStr = typeof content === "string" ? content : JSON.stringify(content);
entries.push({
kind: "tool_result",
ts,
toolUseId: asString(tr.toolCallId, "unknown"),
content: contentStr,
isError,
});
}
}
return entries.length > 0 ? entries : [{ kind: "system", ts, text: "Turn ended" }];
}
// Message streaming
if (type === "message_start") {
return [];
}
if (type === "message_update") {
const assistantEvent = asRecord(parsed.assistantMessageEvent);
if (assistantEvent) {
const msgType = asString(assistantEvent.type);
if (msgType === "text_delta") {
const delta = asString(assistantEvent.delta);
if (delta) {
return [{ kind: "assistant", ts, text: delta, delta: true }];
}
}
}
return [];
}
if (type === "message_end") {
return [];
}
// Tool execution
if (type === "tool_execution_start") {
const toolName = asString(parsed.toolName);
const args = parsed.args;
if (toolName) {
return [{
kind: "tool_call",
ts,
name: toolName,
input: args,
}];
}
return [{ kind: "system", ts, text: `Tool started` }];
}
if (type === "tool_execution_update") {
return [];
}
if (type === "tool_execution_end") {
const toolCallId = asString(parsed.toolCallId);
const result = parsed.result;
const isError = parsed.isError === true;
const contentStr = typeof result === "string" ? result : JSON.stringify(result);
return [{
kind: "tool_result",
ts,
toolUseId: toolCallId || "unknown",
content: contentStr,
isError,
}];
}
return [{ kind: "stdout", ts, text: line }];
}