UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

195 lines • 7.74 kB
import { spawn, spawnSync } from "node:child_process"; import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { DEFAULT_RADIUS_GATEWAY } from "@earendil-works/pi-ai/providers/radius-config"; import { hyperlink } from "@earendil-works/pi-tui"; import { getAuthCredential } from "../../cli/auth-command.js"; import { getShareViewerUrl } from "../../config.js"; import { exportSessionToJsonl } from "../../core/session-export.js"; import { BorderedLoader } from "./components/bordered-loader.js"; import { theme } from "./theme/theme.js"; /** Trailing `pi.share` entry carrying the system prompt and tool schemas for the session viewer. */ export function createShareTrailingEntries(session, parentId, timestamp) { return [ { type: "custom", customType: "pi.share", id: crypto.randomUUID().slice(0, 8), parentId, timestamp, data: { systemPrompt: session.state.systemPrompt, tools: session.state.tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters, })), }, }, ]; } /** Export the current branch with presentation metadata for Radius. */ export function exportSessionForShare(filePath, session) { exportSessionToJsonl(session.sessionManager, filePath, (parentId, timestamp) => createShareTrailingEntries(session, parentId, timestamp)); } /** Share the current session through Radius, falling back to a private gist. */ export async function shareSession(context) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-share-")); const jsonlFile = path.join(tempDir, "session.jsonl"); const htmlFile = path.join(tempDir, "session.html"); try { try { exportSessionForShare(jsonlFile, context.session); } catch (error) { context.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); return; } const destination = context.session.settingsManager.getSessionShareDestination(); if (destination !== "gist") { if (await tryShareViaRadius(jsonlFile, context)) return; if (destination === "radius") { context.showError("Radius authentication is not configured. Run '/login radius' first."); return; } } try { const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" }); if (authResult.status !== 0) { context.showError("GitHub CLI is not logged in. Run 'gh auth login' first."); return; } } catch { context.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"); return; } try { await context.session.exportToHtml(htmlFile, { themeName: theme.name }); } catch (error) { context.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`); return; } await shareViaGist(htmlFile, context); } finally { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { // Ignore cleanup errors } } } async function tryShareViaRadius(tmpFile, context) { const provider = context.session.modelRuntime.getProvider("radius"); if (!provider) return false; const token = getAuthCredential(await context.session.modelRuntime.getAuth("radius", { minOAuthValidityMs: 5 * 60_000 })); if (!token) return false; const loader = new BorderedLoader(context.ui, theme, "Uploading to Radius..."); context.editorContainer.clear(); context.editorContainer.addChild(loader); context.ui.setFocus(loader); context.ui.requestRender(); loader.onAbort = () => { restoreEditor(loader, context); context.showStatus("Share cancelled"); }; try { const body = fs.readFileSync(tmpFile); const url = new URL("/v1/artifacts", DEFAULT_RADIUS_GATEWAY); url.searchParams.set("visibility", "organization"); url.searchParams.set("title", "Pi session"); const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson", "Content-Length": String(body.byteLength), }, body, signal: loader.signal, }); if (loader.signal.aborted) return true; const json = (await response.json().catch(() => null)); if (loader.signal.aborted) return true; restoreEditor(loader, context); if (!response.ok || !json?.artifact) { context.showError(`Failed to upload Radius artifact: ${json?.error || response.statusText || response.status}`); return true; } const shareUrl = json.artifact.canonical_url; context.showStatus(`Share URL: ${hyperlink(shareUrl, shareUrl)}`); return true; } catch (error) { if (!loader.signal.aborted) { restoreEditor(loader, context); context.showError(`Failed to upload Radius artifact: ${error instanceof Error ? error.message : "Unknown error"}`); } return true; } } async function shareViaGist(tmpFile, context) { const loader = new BorderedLoader(context.ui, theme, "Creating gist..."); context.editorContainer.clear(); context.editorContainer.addChild(loader); context.ui.setFocus(loader); context.ui.requestRender(); let proc = null; loader.onAbort = () => { proc?.kill(); restoreEditor(loader, context); context.showStatus("Share cancelled"); }; try { const result = await new Promise((resolve) => { proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]); let stdout = ""; let stderr = ""; proc.stdout?.on("data", (data) => { stdout += data.toString(); }); proc.stderr?.on("data", (data) => { stderr += data.toString(); }); proc.on("close", (code) => resolve({ stdout, stderr, code })); }); if (loader.signal.aborted) return; restoreEditor(loader, context); if (result.code !== 0) { context.showError(`Failed to create gist: ${result.stderr?.trim() || "Unknown error"}`); return; } const gistUrl = result.stdout?.trim(); const gistId = gistUrl?.split("/").pop(); if (!gistId) { context.showError("Failed to parse gist ID from gh output"); return; } const previewUrl = getShareViewerUrl(gistId); context.showStatus(`Share URL: ${hyperlink(previewUrl, previewUrl)}\nGist: ${hyperlink(gistUrl, gistUrl)}`); } catch (error) { if (!loader.signal.aborted) { restoreEditor(loader, context); context.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`); } } } function restoreEditor(loader, context) { loader.dispose(); context.editorContainer.clear(); context.editorContainer.addChild(context.editor); context.ui.setFocus(context.editor); } //# sourceMappingURL=session-share.js.map