diff --git a/.changeset/add-pi-adapter-support.md b/.changeset/add-pi-adapter-support.md new file mode 100644 index 00000000..97005a39 --- /dev/null +++ b/.changeset/add-pi-adapter-support.md @@ -0,0 +1,5 @@ +--- +"@paperclipai/shared": minor +--- + +Add support for Pi local adapter in constants and onboarding UI. \ No newline at end of file diff --git a/.github/workflows/pr-policy.yml b/.github/workflows/pr-policy.yml new file mode 100644 index 00000000..eb515eda --- /dev/null +++ b/.github/workflows/pr-policy.yml @@ -0,0 +1,48 @@ +name: PR Policy + +on: + pull_request: + branches: + - master + +concurrency: + group: pr-policy-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + policy: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.4 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Block manual lockfile edits + run: | + changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")" + if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then + echo "Do not commit pnpm-lock.yaml in pull requests. CI owns lockfile updates." + exit 1 + fi + + - name: Validate dependency resolution when manifests change + run: | + changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")" + manifest_pattern='(^|/)package\.json$|^pnpm-workspace\.yaml$|^\.npmrc$|^pnpmfile\.(cjs|js|mjs)$' + if printf '%s\n' "$changed" | grep -Eq "$manifest_pattern"; then + pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/pr-verify.yml similarity index 82% rename from .github/workflows/ci.yml rename to .github/workflows/pr-verify.yml index 4a44003c..e84e448a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/pr-verify.yml @@ -1,15 +1,12 @@ -name: CI +name: PR Verify on: pull_request: branches: - master - push: - branches: - - master concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: pr-verify-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: @@ -33,7 +30,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install --frozen-lockfile + run: pnpm install --no-frozen-lockfile - name: Typecheck run: pnpm -r typecheck diff --git a/.github/workflows/refresh-lockfile.yml b/.github/workflows/refresh-lockfile.yml new file mode 100644 index 00000000..079fdd4e --- /dev/null +++ b/.github/workflows/refresh-lockfile.yml @@ -0,0 +1,76 @@ +name: Refresh Lockfile + +on: + push: + branches: + - master + workflow_dispatch: + +concurrency: + group: refresh-lockfile-master + cancel-in-progress: false + +jobs: + refresh_and_verify: + runs-on: ubuntu-latest + timeout-minutes: 25 + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.4 + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Refresh pnpm lockfile + run: pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile + + - name: Fail on unexpected file changes + run: | + changed="$(git status --porcelain)" + if [ -z "$changed" ]; then + exit 0 + fi + if printf '%s\n' "$changed" | grep -Fvq ' pnpm-lock.yaml'; then + echo "Unexpected files changed during lockfile refresh:" + echo "$changed" + exit 1 + fi + + - name: Commit refreshed lockfile + run: | + if git diff --quiet -- pnpm-lock.yaml; then + exit 0 + fi + git config user.name "lockfile-bot" + git config user.email "lockfile-bot@users.noreply.github.com" + git add pnpm-lock.yaml + git commit -m "chore(lockfile): refresh pnpm-lock.yaml" + git push || { + echo "Push failed because master moved during lockfile refresh." + echo "A later refresh run should recompute the lockfile from the newer master state." + exit 1 + } + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm -r typecheck + + - name: Run tests + run: pnpm test:run + + - name: Build + run: pnpm build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ab420a24 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing Guide + +Thanks for wanting to contribute! + +We really appreciate both small fixes and thoughtful larger changes. + +## Two Paths to Get Your Pull Request Accepted + +### Path 1: Small, Focused Changes (Fastest way to get merged) +- Pick **one** clear thing to fix/improve +- Touch the **smallest possible number of files** +- Make sure the change is very targeted and easy to review +- All automated checks pass (including Greptile comments) +- No new lint/test failures + +These almost always get merged quickly when they're clean. + +### Path 2: Bigger or Impactful Changes +- **First** talk about it in Discord → #dev channel + → Describe what you're trying to solve + → Share rough ideas / approach +- Once there's rough agreement, build it +- In your PR include: + - Before / After screenshots (or short video if UI/behavior change) + - Clear description of what & why + - Proof it works (manual testing notes) + - All tests passing + - All Greptile + other PR comments addressed + +PRs that follow this path are **much** more likely to be accepted, even when they're large. + +## General Rules (both paths) +- Write clear commit messages +- Keep PR title + description meaningful +- One PR = one logical change (unless it's a small related group) +- Run tests locally first +- Be kind in discussions 😄 + +Questions? Just ask in #dev — we're happy to help. + +Happy hacking! diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a63594a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Paperclip AI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cli/package.json b/cli/package.json index bd0ac340..1bddae42 100644 --- a/cli/package.json +++ b/cli/package.json @@ -38,6 +38,7 @@ "@paperclipai/adapter-codex-local": "workspace:*", "@paperclipai/adapter-cursor-local": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", + "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-utils": "workspace:*", diff --git a/cli/src/adapters/registry.ts b/cli/src/adapters/registry.ts index 82762ab3..41d95f77 100644 --- a/cli/src/adapters/registry.ts +++ b/cli/src/adapters/registry.ts @@ -3,6 +3,7 @@ import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli"; import { printCodexStreamEvent } from "@paperclipai/adapter-codex-local/cli"; import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli"; import { printOpenCodeStreamEvent } from "@paperclipai/adapter-opencode-local/cli"; +import { printPiStreamEvent } from "@paperclipai/adapter-pi-local/cli"; import { printOpenClawStreamEvent } from "@paperclipai/adapter-openclaw/cli"; import { printOpenClawGatewayStreamEvent } from "@paperclipai/adapter-openclaw-gateway/cli"; import { processCLIAdapter } from "./process/index.js"; @@ -23,6 +24,11 @@ const openCodeLocalCLIAdapter: CLIAdapterModule = { formatStdoutEvent: printOpenCodeStreamEvent, }; +const piLocalCLIAdapter: CLIAdapterModule = { + type: "pi_local", + formatStdoutEvent: printPiStreamEvent, +}; + const cursorLocalCLIAdapter: CLIAdapterModule = { type: "cursor", formatStdoutEvent: printCursorStreamEvent, @@ -43,6 +49,7 @@ const adaptersByType = new Map( claudeLocalCLIAdapter, codexLocalCLIAdapter, openCodeLocalCLIAdapter, + piLocalCLIAdapter, cursorLocalCLIAdapter, openclawCLIAdapter, openclawGatewayCLIAdapter, diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index bb287623..d3362600 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -15,6 +15,14 @@ Current implementation status: - Node.js 20+ - pnpm 9+ +## Dependency Lockfile Policy + +GitHub Actions owns `pnpm-lock.yaml`. + +- Do not commit `pnpm-lock.yaml` in pull requests. +- Pull request CI validates dependency resolution when manifests change. +- Pushes to `master` regenerate `pnpm-lock.yaml` with `pnpm install --lockfile-only --no-frozen-lockfile`, commit it back if needed, and then run verification with `--frozen-lockfile`. + ## Start Dev From repo root: diff --git a/packages/adapters/opencode-local/src/cli/format-event.ts b/packages/adapters/opencode-local/src/cli/format-event.ts index 00d0ec76..58d2038d 100644 --- a/packages/adapters/opencode-local/src/cli/format-event.ts +++ b/packages/adapters/opencode-local/src/cli/format-event.ts @@ -74,20 +74,25 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void { if (type === "tool_use") { const part = asRecord(parsed.part); const tool = asString(part?.tool, "tool"); + const callID = asString(part?.callID); const state = asRecord(part?.state); const status = asString(state?.status); - const summary = `tool_${status || "event"}: ${tool}`; const isError = status === "error"; - console.log((isError ? pc.red : pc.yellow)(summary)); - const input = state?.input; - if (input !== undefined) { - try { - console.log(pc.gray(JSON.stringify(input, null, 2))); - } catch { - console.log(pc.gray(String(input))); + const metadata = asRecord(state?.metadata); + + console.log(pc.yellow(`tool_call: ${tool}${callID ? ` (${callID})` : ""}`)); + + if (status) { + const metaParts = [`status=${status}`]; + if (metadata) { + for (const [key, value] of Object.entries(metadata)) { + if (value !== undefined && value !== null) metaParts.push(`${key}=${value}`); + } } + console.log((isError ? pc.red : pc.gray)(`tool_result ${metaParts.join(" ")}`)); } - const output = asString(state?.output) || asString(state?.error); + + const output = (asString(state?.output) || asString(state?.error)).trim(); if (output) console.log((isError ? pc.red : pc.gray)(output)); return; } @@ -101,7 +106,8 @@ export function printOpenCodeStreamEvent(raw: string, _debug: boolean): void { const cached = asNumber(cache?.read, 0); const cost = asNumber(part?.cost, 0); const reason = asString(part?.reason, "step"); - console.log(pc.blue(`step finished (${reason}) tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`)); + console.log(pc.blue(`step finished: reason=${reason}`)); + console.log(pc.blue(`tokens: in=${input} out=${output} cached=${cached} cost=$${cost.toFixed(6)}`)); return; } diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 338646b3..970896af 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -340,7 +340,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise { inputTokens: 120, cachedInputTokens: 20, outputTokens: 50, - costUsd: 0.0025, }); + expect(parsed.costUsd).toBeCloseTo(0.0025, 6); expect(parsed.errorMessage).toContain("model unavailable"); }); diff --git a/packages/adapters/opencode-local/src/server/parse.ts b/packages/adapters/opencode-local/src/server/parse.ts index 5cbfa46c..96af0ed1 100644 --- a/packages/adapters/opencode-local/src/server/parse.ts +++ b/packages/adapters/opencode-local/src/server/parse.ts @@ -27,8 +27,8 @@ export function parseOpenCodeJsonl(stdout: string) { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, - costUsd: 0, }; + let costUsd = 0; for (const rawLine of stdout.split(/\r?\n/)) { const line = rawLine.trim(); @@ -56,7 +56,7 @@ export function parseOpenCodeJsonl(stdout: string) { usage.inputTokens += asNumber(tokens.input, 0); usage.cachedInputTokens += asNumber(cache.read, 0); usage.outputTokens += asNumber(tokens.output, 0) + asNumber(tokens.reasoning, 0); - usage.costUsd += asNumber(part.cost, 0); + costUsd += asNumber(part.cost, 0); continue; } @@ -81,6 +81,7 @@ export function parseOpenCodeJsonl(stdout: string) { sessionId, summary: messages.join("\n\n").trim(), usage, + costUsd, errorMessage: errors.length > 0 ? errors.join("\n") : null, }; } @@ -92,7 +93,7 @@ export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): b .filter(Boolean) .join("\n"); - return /unknown\s+session|session\s+.*\s+not\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test( + return /unknown\s+session|session\b.*\bnot\s+found|resource\s+not\s+found:.*[\\/]session[\\/].*\.json|notfounderror|no session/i.test( haystack, ); } diff --git a/packages/adapters/opencode-local/src/server/test.ts b/packages/adapters/opencode-local/src/server/test.ts index 569f0d75..5bb7aa36 100644 --- a/packages/adapters/opencode-local/src/server/test.ts +++ b/packages/adapters/opencode-local/src/server/test.ts @@ -79,6 +79,17 @@ export async function testEnvironment( for (const [key, value] of Object.entries(envConfig)) { if (typeof value === "string") env[key] = value; } + + const openaiKeyOverride = "OPENAI_API_KEY" in envConfig ? asString(envConfig.OPENAI_API_KEY, "") : null; + if (openaiKeyOverride !== null && openaiKeyOverride.trim() === "") { + checks.push({ + code: "opencode_openai_api_key_missing", + level: "warn", + message: "OPENAI_API_KEY override is empty.", + hint: "The OPENAI_API_KEY override is empty. Set a valid key or remove the override.", + }); + } + const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env })); const cwdInvalid = checks.some((check) => check.code === "opencode_cwd_invalid"); @@ -111,7 +122,9 @@ export async function testEnvironment( checks.every((check) => check.code !== "opencode_cwd_invalid" && check.code !== "opencode_command_unresolvable"); let modelValidationPassed = false; - if (canRunProbe) { + const configuredModel = asString(config.model, "").trim(); + + if (canRunProbe && configuredModel) { try { const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv }); if (discovered.length > 0) { @@ -129,24 +142,59 @@ export async function testEnvironment( }); } } catch (err) { - checks.push({ - code: "opencode_models_discovery_failed", - level: "error", - message: err instanceof Error ? err.message : "OpenCode model discovery failed.", - hint: "Run `opencode models` manually to verify provider auth and config.", - }); + const errMsg = err instanceof Error ? err.message : String(err); + if (/ProviderModelNotFoundError/i.test(errMsg)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + detail: errMsg, + hint: "Run `opencode models` and choose an available provider/model ID.", + }); + } else { + checks.push({ + code: "opencode_models_discovery_failed", + level: "error", + message: errMsg || "OpenCode model discovery failed.", + hint: "Run `opencode models` manually to verify provider auth and config.", + }); + } + } + } else if (canRunProbe && !configuredModel) { + try { + const discovered = await discoverOpenCodeModels({ command, cwd, env: runtimeEnv }); + if (discovered.length > 0) { + checks.push({ + code: "opencode_models_discovered", + level: "info", + message: `Discovered ${discovered.length} model(s) from OpenCode providers.`, + }); + } + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + if (/ProviderModelNotFoundError/i.test(errMsg)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + detail: errMsg, + hint: "Run `opencode models` and choose an available provider/model ID.", + }); + } else { + checks.push({ + code: "opencode_models_discovery_failed", + level: "warn", + message: errMsg || "OpenCode model discovery failed (best-effort, no model configured).", + hint: "Run `opencode models` manually to verify provider auth and config.", + }); + } } } - const configuredModel = asString(config.model, "").trim(); - if (!configuredModel) { - checks.push({ - code: "opencode_model_required", - level: "error", - message: "OpenCode requires a configured model in provider/model format.", - hint: "Set adapterConfig.model using an ID from `opencode models`.", - }); - } else if (canRunProbe) { + const modelUnavailable = checks.some((check) => check.code === "opencode_hello_probe_model_unavailable"); + if (!configuredModel && !modelUnavailable) { + // No model configured – skip model requirement if no model-related checks exist + } else if (configuredModel && canRunProbe) { try { await ensureOpenCodeModelConfiguredAndAvailable({ model: configuredModel, @@ -226,6 +274,14 @@ export async function testEnvironment( hint: "Run `opencode run --format json` manually and prompt `Respond with hello` to inspect output.", }), }); + } else if (/ProviderModelNotFoundError/i.test(authEvidence)) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: "The configured model was not found by the provider.", + ...(detail ? { detail } : {}), + hint: "Run `opencode models` and choose an available provider/model ID.", + }); } else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) { checks.push({ code: "opencode_hello_probe_auth_required", diff --git a/packages/adapters/opencode-local/src/ui/parse-stdout.ts b/packages/adapters/opencode-local/src/ui/parse-stdout.ts index dc48e5d1..2060125a 100644 --- a/packages/adapters/opencode-local/src/ui/parse-stdout.ts +++ b/packages/adapters/opencode-local/src/ui/parse-stdout.ts @@ -56,19 +56,28 @@ function parseToolUse(parsed: Record, ts: string): TranscriptEn const status = asString(state?.status); if (status !== "completed" && status !== "error") return [callEntry]; - const output = + const rawOutput = asString(state?.output) || asString(state?.error) || asString(part.title) || `${toolName} ${status}`; + const metadata = asRecord(state?.metadata); + const headerParts: string[] = [`status: ${status}`]; + if (metadata) { + for (const [key, value] of Object.entries(metadata)) { + if (value !== undefined && value !== null) headerParts.push(`${key}: ${value}`); + } + } + const content = `${headerParts.join("\n")}\n\n${rawOutput}`.trim(); + return [ callEntry, { kind: "tool_result", ts, - toolUseId: asString(part.id, toolName), - content: output, + toolUseId: asString(part.callID) || asString(part.id, toolName), + content, isError: status === "error", }, ]; diff --git a/packages/adapters/pi-local/package.json b/packages/adapters/pi-local/package.json new file mode 100644 index 00000000..1184c1ca --- /dev/null +++ b/packages/adapters/pi-local/package.json @@ -0,0 +1,50 @@ +{ + "name": "@paperclipai/adapter-pi-local", + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./server": "./src/server/index.ts", + "./ui": "./src/ui/index.ts", + "./cli": "./src/cli/index.ts" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "import": "./dist/server/index.js" + }, + "./ui": { + "types": "./dist/ui/index.d.ts", + "import": "./dist/ui/index.js" + }, + "./cli": { + "types": "./dist/cli/index.d.ts", + "import": "./dist/cli/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paperclipai/adapter-utils": "workspace:*", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "@types/node": "^24.6.0", + "typescript": "^5.7.3" + } +} diff --git a/packages/adapters/pi-local/src/cli/format-event.ts b/packages/adapters/pi-local/src/cli/format-event.ts new file mode 100644 index 00000000..e93319a6 --- /dev/null +++ b/packages/adapters/pi-local/src/cli/format-event.ts @@ -0,0 +1,107 @@ +import pc from "picocolors"; + +function safeJsonParse(text: string): unknown { + try { + return JSON.parse(text); + } catch { + return null; + } +} + +function asRecord(value: unknown): Record | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + return value as Record; +} + +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 printPiStreamEvent(raw: string, _debug: boolean): void { + const line = raw.trim(); + if (!line) return; + + const parsed = asRecord(safeJsonParse(line)); + if (!parsed) { + console.log(line); + return; + } + + const type = asString(parsed.type); + + if (type === "agent_start") { + console.log(pc.blue("Pi agent started")); + return; + } + + if (type === "agent_end") { + console.log(pc.blue("Pi agent finished")); + return; + } + + if (type === "turn_start") { + console.log(pc.blue("Turn started")); + return; + } + + if (type === "turn_end") { + const message = asRecord(parsed.message); + if (message) { + const content = message.content as string | Array<{ type: string; text?: string }>; + const text = extractTextContent(content); + if (text) { + console.log(pc.green(`assistant: ${text}`)); + } + } + 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) { + console.log(pc.green(delta)); + } + } + } + return; + } + + if (type === "tool_execution_start") { + const toolName = asString(parsed.toolName); + const args = parsed.args; + console.log(pc.yellow(`tool_start: ${toolName}`)); + if (args !== undefined) { + try { + console.log(pc.gray(JSON.stringify(args, null, 2))); + } catch { + console.log(pc.gray(String(args))); + } + } + return; + } + + if (type === "tool_execution_end") { + const result = parsed.result; + const isError = parsed.isError === true; + const output = typeof result === "string" ? result : JSON.stringify(result); + if (output) { + console.log((isError ? pc.red : pc.gray)(output)); + } + return; + } + + console.log(line); +} diff --git a/packages/adapters/pi-local/src/cli/index.ts b/packages/adapters/pi-local/src/cli/index.ts new file mode 100644 index 00000000..94d5961a --- /dev/null +++ b/packages/adapters/pi-local/src/cli/index.ts @@ -0,0 +1 @@ +export { printPiStreamEvent } from "./format-event.js"; diff --git a/packages/adapters/pi-local/src/index.ts b/packages/adapters/pi-local/src/index.ts new file mode 100644 index 00000000..3794426f --- /dev/null +++ b/packages/adapters/pi-local/src/index.ts @@ -0,0 +1,40 @@ +export const type = "pi_local"; +export const label = "Pi (local)"; + +export const models: Array<{ id: string; label: string }> = []; + +export const agentConfigurationDoc = `# pi_local agent configuration + +Adapter: pi_local + +Use when: +- You want Paperclip to run Pi (the AI coding agent) locally as the agent runtime +- You want provider/model routing in Pi format (--provider --model ) +- You want Pi session resume across heartbeats via --session +- You need Pi's tool set (read, bash, edit, write, grep, find, ls) + +Don't use when: +- You need webhook-style external invocation (use openclaw or http) +- You only need one-shot shell commands (use process) +- Pi CLI is not installed on the machine + +Core fields: +- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible) +- instructionsFilePath (string, optional): absolute path to a markdown instructions file appended to system prompt via --append-system-prompt +- promptTemplate (string, optional): user prompt template passed via -p flag +- model (string, required): Pi model id in provider/model format (for example xai/grok-4) +- thinking (string, optional): thinking level (off, minimal, low, medium, high, xhigh) +- command (string, optional): defaults to "pi" +- env (object, optional): KEY=VALUE environment variables + +Operational fields: +- timeoutSec (number, optional): run timeout in seconds +- graceSec (number, optional): SIGTERM grace period in seconds + +Notes: +- Pi supports multiple providers and models. Use \`pi --list-models\` to list available options. +- Paperclip requires an explicit \`model\` value for \`pi_local\` agents. +- Sessions are stored in ~/.pi/paperclips/ and resumed with --session. +- All tools (read, bash, edit, write, grep, find, ls) are enabled by default. +- Agent instructions are appended to Pi's system prompt via --append-system-prompt, while the user task is sent via -p. +`; diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts new file mode 100644 index 00000000..23cad28b --- /dev/null +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -0,0 +1,478 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils"; +import { + asString, + asNumber, + asStringArray, + parseObject, + buildPaperclipEnv, + redactEnvForLogs, + ensureAbsoluteDirectory, + ensureCommandResolvable, + ensurePathInEnv, + renderTemplate, + runChildProcess, +} from "@paperclipai/adapter-utils/server-utils"; +import { isPiUnknownSessionError, parsePiJsonl } from "./parse.js"; +import { ensurePiModelConfiguredAndAvailable } from "./models.js"; + +const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); +const PAPERCLIP_SKILLS_CANDIDATES = [ + path.resolve(__moduleDir, "../../skills"), + path.resolve(__moduleDir, "../../../../../skills"), +]; + +const PAPERCLIP_SESSIONS_DIR = path.join(os.homedir(), ".pi", "paperclips"); + +function firstNonEmptyLine(text: string): string { + return ( + text + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean) ?? "" + ); +} + +function parseModelProvider(model: string | null): string | null { + if (!model) return null; + const trimmed = model.trim(); + if (!trimmed.includes("/")) return null; + return trimmed.slice(0, trimmed.indexOf("/")).trim() || null; +} + +function parseModelId(model: string | null): string | null { + if (!model) return null; + const trimmed = model.trim(); + if (!trimmed.includes("/")) return trimmed || null; + return trimmed.slice(trimmed.indexOf("/") + 1).trim() || null; +} + +async function resolvePaperclipSkillsDir(): Promise { + for (const candidate of PAPERCLIP_SKILLS_CANDIDATES) { + const isDir = await fs.stat(candidate).then((s) => s.isDirectory()).catch(() => false); + if (isDir) return candidate; + } + return null; +} + +async function ensurePiSkillsInjected(onLog: AdapterExecutionContext["onLog"]) { + const skillsDir = await resolvePaperclipSkillsDir(); + if (!skillsDir) return; + + const piSkillsHome = path.join(os.homedir(), ".pi", "agent", "skills"); + await fs.mkdir(piSkillsHome, { recursive: true }); + + const entries = await fs.readdir(skillsDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const source = path.join(skillsDir, entry.name); + const target = path.join(piSkillsHome, entry.name); + const existing = await fs.lstat(target).catch(() => null); + if (existing) continue; + + try { + await fs.symlink(source, target); + await onLog( + "stderr", + `[paperclip] Injected Pi skill "${entry.name}" into ${piSkillsHome}\n`, + ); + } catch (err) { + await onLog( + "stderr", + `[paperclip] Failed to inject Pi skill "${entry.name}" into ${piSkillsHome}: ${err instanceof Error ? err.message : String(err)}\n`, + ); + } + } +} + +async function ensureSessionsDir(): Promise { + await fs.mkdir(PAPERCLIP_SESSIONS_DIR, { recursive: true }); + return PAPERCLIP_SESSIONS_DIR; +} + +function buildSessionPath(agentId: string, timestamp: string): string { + const safeTimestamp = timestamp.replace(/[:.]/g, "-"); + return path.join(PAPERCLIP_SESSIONS_DIR, `${safeTimestamp}-${agentId}.jsonl`); +} + +export async function execute(ctx: AdapterExecutionContext): Promise { + const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx; + + const promptTemplate = asString( + config.promptTemplate, + "You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.", + ); + const command = asString(config.command, "pi"); + const model = asString(config.model, "").trim(); + const thinking = asString(config.thinking, "").trim(); + + // Parse model into provider and model id + const provider = parseModelProvider(model); + const modelId = parseModelId(model); + + const workspaceContext = parseObject(context.paperclipWorkspace); + const workspaceCwd = asString(workspaceContext.cwd, ""); + const workspaceSource = asString(workspaceContext.source, ""); + const workspaceId = asString(workspaceContext.workspaceId, ""); + const workspaceRepoUrl = asString(workspaceContext.repoUrl, ""); + const workspaceRepoRef = asString(workspaceContext.repoRef, ""); + const workspaceHints = Array.isArray(context.paperclipWorkspaces) + ? context.paperclipWorkspaces.filter( + (value): value is Record => typeof value === "object" && value !== null, + ) + : []; + const configuredCwd = asString(config.cwd, ""); + const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0; + const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd; + const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd(); + await ensureAbsoluteDirectory(cwd, { createIfMissing: true }); + + // Ensure sessions directory exists + await ensureSessionsDir(); + + // Inject skills + await ensurePiSkillsInjected(onLog); + + // Build environment + const envConfig = parseObject(config.env); + const hasExplicitApiKey = + typeof envConfig.PAPERCLIP_API_KEY === "string" && envConfig.PAPERCLIP_API_KEY.trim().length > 0; + const env: Record = { ...buildPaperclipEnv(agent) }; + env.PAPERCLIP_RUN_ID = runId; + + const wakeTaskId = + (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) || + (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) || + null; + const wakeReason = + typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0 + ? context.wakeReason.trim() + : null; + const wakeCommentId = + (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) || + (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) || + null; + const approvalId = + typeof context.approvalId === "string" && context.approvalId.trim().length > 0 + ? context.approvalId.trim() + : null; + const approvalStatus = + typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0 + ? context.approvalStatus.trim() + : null; + const linkedIssueIds = Array.isArray(context.issueIds) + ? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0) + : []; + + if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId; + if (wakeReason) env.PAPERCLIP_WAKE_REASON = wakeReason; + if (wakeCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId; + if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId; + if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus; + if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); + if (workspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd; + if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource; + if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId; + if (workspaceRepoUrl) env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl; + if (workspaceRepoRef) env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef; + if (workspaceHints.length > 0) env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); + + for (const [key, value] of Object.entries(envConfig)) { + if (typeof value === "string") env[key] = value; + } + if (!hasExplicitApiKey && authToken) { + env.PAPERCLIP_API_KEY = authToken; + } + + const runtimeEnv = Object.fromEntries( + Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + await ensureCommandResolvable(command, cwd, runtimeEnv); + + // Validate model is available before execution + await ensurePiModelConfiguredAndAvailable({ + model, + command, + cwd, + env: runtimeEnv, + }); + + const timeoutSec = asNumber(config.timeoutSec, 0); + const graceSec = asNumber(config.graceSec, 20); + const extraArgs = (() => { + const fromExtraArgs = asStringArray(config.extraArgs); + if (fromExtraArgs.length > 0) return fromExtraArgs; + return asStringArray(config.args); + })(); + + // Handle session + const runtimeSessionParams = parseObject(runtime.sessionParams); + const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? ""); + const runtimeSessionCwd = asString(runtimeSessionParams.cwd, ""); + const canResumeSession = + runtimeSessionId.length > 0 && + (runtimeSessionCwd.length === 0 || path.resolve(runtimeSessionCwd) === path.resolve(cwd)); + const sessionPath = canResumeSession ? runtimeSessionId : buildSessionPath(agent.id, new Date().toISOString()); + + if (runtimeSessionId && !canResumeSession) { + await onLog( + "stderr", + `[paperclip] Pi session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`, + ); + } + + // Ensure session file exists (Pi requires this on first run) + if (!canResumeSession) { + try { + await fs.writeFile(sessionPath, "", { flag: "wx" }); + } catch (err) { + // File may already exist, that's ok + if ((err as NodeJS.ErrnoException).code !== "EEXIST") { + throw err; + } + } + } + + // Handle instructions file and build system prompt extension + const instructionsFilePath = asString(config.instructionsFilePath, "").trim(); + const resolvedInstructionsFilePath = instructionsFilePath + ? path.resolve(cwd, instructionsFilePath) + : ""; + const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : ""; + + let systemPromptExtension = ""; + let instructionsReadFailed = false; + if (resolvedInstructionsFilePath) { + try { + const instructionsContents = await fs.readFile(resolvedInstructionsFilePath, "utf8"); + systemPromptExtension = + `${instructionsContents}\n\n` + + `The above agent instructions were loaded from ${resolvedInstructionsFilePath}. ` + + `Resolve any relative file references from ${instructionsFileDir}.\n\n` + + `You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.`; + await onLog( + "stderr", + `[paperclip] Loaded agent instructions file: ${resolvedInstructionsFilePath}\n`, + ); + } catch (err) { + instructionsReadFailed = true; + const reason = err instanceof Error ? err.message : String(err); + await onLog( + "stderr", + `[paperclip] Warning: could not read agent instructions file "${resolvedInstructionsFilePath}": ${reason}\n`, + ); + // Fall back to base prompt template + systemPromptExtension = promptTemplate; + } + } else { + systemPromptExtension = promptTemplate; + } + + const renderedSystemPromptExtension = renderTemplate(systemPromptExtension, { + agentId: agent.id, + companyId: agent.companyId, + runId, + company: { id: agent.companyId }, + agent, + run: { id: runId, source: "on_demand" }, + context, + }); + + // User prompt is simple - just the rendered prompt template without instructions + const userPrompt = renderTemplate(promptTemplate, { + agentId: agent.id, + companyId: agent.companyId, + runId, + company: { id: agent.companyId }, + agent, + run: { id: runId, source: "on_demand" }, + context, + }); + + const commandNotes = (() => { + if (!resolvedInstructionsFilePath) return [] as string[]; + if (instructionsReadFailed) { + return [ + `Configured instructionsFilePath ${resolvedInstructionsFilePath}, but file could not be read; continuing without injected instructions.`, + ]; + } + return [ + `Loaded agent instructions from ${resolvedInstructionsFilePath}`, + `Appended instructions + path directive to system prompt (relative references from ${instructionsFileDir}).`, + ]; + })(); + + const buildArgs = (sessionFile: string): string[] => { + const args: string[] = []; + + // Use RPC mode for proper lifecycle management (waits for agent completion) + args.push("--mode", "rpc"); + + // Use --append-system-prompt to extend Pi's default system prompt + args.push("--append-system-prompt", renderedSystemPromptExtension); + + if (provider) args.push("--provider", provider); + if (modelId) args.push("--model", modelId); + if (thinking) args.push("--thinking", thinking); + + args.push("--tools", "read,bash,edit,write,grep,find,ls"); + args.push("--session", sessionFile); + + if (extraArgs.length > 0) args.push(...extraArgs); + + return args; + }; + + const buildRpcStdin = (): string => { + // Send the prompt as an RPC command + const promptCommand = { + type: "prompt", + message: userPrompt, + }; + return JSON.stringify(promptCommand) + "\n"; + }; + + const runAttempt = async (sessionFile: string) => { + const args = buildArgs(sessionFile); + if (onMeta) { + await onMeta({ + adapterType: "pi_local", + command, + cwd, + commandNotes, + commandArgs: args, + env: redactEnvForLogs(env), + prompt: userPrompt, + context, + }); + } + + // Buffer stdout by lines to handle partial JSON chunks + let stdoutBuffer = ""; + const bufferedOnLog = async (stream: "stdout" | "stderr", chunk: string) => { + if (stream === "stderr") { + // Pass stderr through immediately (not JSONL) + await onLog(stream, chunk); + return; + } + + // Buffer stdout and emit only complete lines + stdoutBuffer += chunk; + const lines = stdoutBuffer.split("\n"); + // Keep the last (potentially incomplete) line in the buffer + stdoutBuffer = lines.pop() || ""; + + // Emit complete lines + for (const line of lines) { + if (line) { + await onLog(stream, line + "\n"); + } + } + }; + + const proc = await runChildProcess(runId, command, args, { + cwd, + env: runtimeEnv, + timeoutSec, + graceSec, + onLog: bufferedOnLog, + stdin: buildRpcStdin(), + }); + + // Flush any remaining buffer content + if (stdoutBuffer) { + await onLog("stdout", stdoutBuffer); + } + + return { + proc, + rawStderr: proc.stderr, + parsed: parsePiJsonl(proc.stdout), + }; + }; + + const toResult = ( + attempt: { + proc: { exitCode: number | null; signal: string | null; timedOut: boolean; stdout: string; stderr: string }; + rawStderr: string; + parsed: ReturnType; + }, + clearSessionOnMissingSession = false, + ): AdapterExecutionResult => { + if (attempt.proc.timedOut) { + return { + exitCode: attempt.proc.exitCode, + signal: attempt.proc.signal, + timedOut: true, + errorMessage: `Timed out after ${timeoutSec}s`, + clearSession: clearSessionOnMissingSession, + }; + } + + const resolvedSessionId = clearSessionOnMissingSession ? null : sessionPath; + const resolvedSessionParams = resolvedSessionId + ? { sessionId: resolvedSessionId, cwd } + : null; + + const stderrLine = firstNonEmptyLine(attempt.proc.stderr); + const rawExitCode = attempt.proc.exitCode; + const fallbackErrorMessage = stderrLine || `Pi exited with code ${rawExitCode ?? -1}`; + + return { + exitCode: rawExitCode, + signal: attempt.proc.signal, + timedOut: false, + errorMessage: (rawExitCode ?? 0) === 0 ? null : fallbackErrorMessage, + usage: { + inputTokens: attempt.parsed.usage.inputTokens, + outputTokens: attempt.parsed.usage.outputTokens, + cachedInputTokens: attempt.parsed.usage.cachedInputTokens, + }, + sessionId: resolvedSessionId, + sessionParams: resolvedSessionParams, + sessionDisplayId: resolvedSessionId, + provider: provider, + model: model, + billingType: "unknown", + costUsd: attempt.parsed.usage.costUsd, + resultJson: { + stdout: attempt.proc.stdout, + stderr: attempt.proc.stderr, + }, + summary: attempt.parsed.finalMessage ?? attempt.parsed.messages.join("\n\n").trim(), + clearSession: Boolean(clearSessionOnMissingSession), + }; + }; + + const initial = await runAttempt(sessionPath); + const initialFailed = + !initial.proc.timedOut && ((initial.proc.exitCode ?? 0) !== 0 || initial.parsed.errors.length > 0); + + if ( + canResumeSession && + initialFailed && + isPiUnknownSessionError(initial.proc.stdout, initial.rawStderr) + ) { + await onLog( + "stderr", + `[paperclip] Pi session "${runtimeSessionId}" is unavailable; retrying with a fresh session.\n`, + ); + const newSessionPath = buildSessionPath(agent.id, new Date().toISOString()); + try { + await fs.writeFile(newSessionPath, "", { flag: "wx" }); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "EEXIST") { + throw err; + } + } + const retry = await runAttempt(newSessionPath); + return toResult(retry, true); + } + + return toResult(initial); +} diff --git a/packages/adapters/pi-local/src/server/index.ts b/packages/adapters/pi-local/src/server/index.ts new file mode 100644 index 00000000..a18d5264 --- /dev/null +++ b/packages/adapters/pi-local/src/server/index.ts @@ -0,0 +1,60 @@ +import type { AdapterSessionCodec } from "@paperclipai/adapter-utils"; + +function readNonEmptyString(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +export const sessionCodec: AdapterSessionCodec = { + deserialize(raw: unknown) { + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null; + const record = raw as Record; + const sessionId = + readNonEmptyString(record.sessionId) ?? + readNonEmptyString(record.session_id) ?? + readNonEmptyString(record.session); + if (!sessionId) return null; + const cwd = + readNonEmptyString(record.cwd) ?? + readNonEmptyString(record.workdir) ?? + readNonEmptyString(record.folder); + return { + sessionId, + ...(cwd ? { cwd } : {}), + }; + }, + serialize(params: Record | null) { + if (!params) return null; + const sessionId = + readNonEmptyString(params.sessionId) ?? + readNonEmptyString(params.session_id) ?? + readNonEmptyString(params.session); + if (!sessionId) return null; + const cwd = + readNonEmptyString(params.cwd) ?? + readNonEmptyString(params.workdir) ?? + readNonEmptyString(params.folder); + return { + sessionId, + ...(cwd ? { cwd } : {}), + }; + }, + getDisplayId(params: Record | null) { + if (!params) return null; + return ( + readNonEmptyString(params.sessionId) ?? + readNonEmptyString(params.session_id) ?? + readNonEmptyString(params.session) + ); + }, +}; + +export { execute } from "./execute.js"; +export { testEnvironment } from "./test.js"; +export { + listPiModels, + discoverPiModels, + discoverPiModelsCached, + ensurePiModelConfiguredAndAvailable, + resetPiModelsCacheForTests, +} from "./models.js"; +export { parsePiJsonl, isPiUnknownSessionError } from "./parse.js"; diff --git a/packages/adapters/pi-local/src/server/models.test.ts b/packages/adapters/pi-local/src/server/models.test.ts new file mode 100644 index 00000000..df777544 --- /dev/null +++ b/packages/adapters/pi-local/src/server/models.test.ts @@ -0,0 +1,33 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + ensurePiModelConfiguredAndAvailable, + listPiModels, + resetPiModelsCacheForTests, +} from "./models.js"; + +describe("pi models", () => { + afterEach(() => { + delete process.env.PAPERCLIP_PI_COMMAND; + resetPiModelsCacheForTests(); + }); + + it("returns an empty list when discovery command is unavailable", async () => { + process.env.PAPERCLIP_PI_COMMAND = "__paperclip_missing_pi_command__"; + await expect(listPiModels()).resolves.toEqual([]); + }); + + it("rejects when model is missing", async () => { + await expect( + ensurePiModelConfiguredAndAvailable({ model: "" }), + ).rejects.toThrow("Pi requires `adapterConfig.model`"); + }); + + it("rejects when discovery cannot run for configured model", async () => { + process.env.PAPERCLIP_PI_COMMAND = "__paperclip_missing_pi_command__"; + await expect( + ensurePiModelConfiguredAndAvailable({ + model: "xai/grok-4", + }), + ).rejects.toThrow(); + }); +}); diff --git a/packages/adapters/pi-local/src/server/models.ts b/packages/adapters/pi-local/src/server/models.ts new file mode 100644 index 00000000..3212312a --- /dev/null +++ b/packages/adapters/pi-local/src/server/models.ts @@ -0,0 +1,208 @@ +import { createHash } from "node:crypto"; +import type { AdapterModel } from "@paperclipai/adapter-utils"; +import { asString, runChildProcess } from "@paperclipai/adapter-utils/server-utils"; + +const MODELS_CACHE_TTL_MS = 60_000; + +function firstNonEmptyLine(text: string): string { + return ( + text + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean) ?? "" + ); +} + +function parseModelsOutput(stdout: string): AdapterModel[] { + const parsed: AdapterModel[] = []; + const lines = stdout.split(/\r?\n/); + + // Skip header line if present + let startIndex = 0; + if (lines.length > 0 && (lines[0].includes("provider") || lines[0].includes("model"))) { + startIndex = 1; + } + + for (let i = startIndex; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + + // Parse format: "provider model context max-out thinking images" + // Split by 2+ spaces to handle the columnar format + const parts = line.split(/\s{2,}/); + if (parts.length < 2) continue; + + const provider = parts[0].trim(); + const model = parts[1].trim(); + + if (!provider || !model) continue; + if (provider === "provider" && model === "model") continue; // Skip header + + const id = `${provider}/${model}`; + parsed.push({ id, label: id }); + } + + return parsed; +} + +function dedupeModels(models: AdapterModel[]): AdapterModel[] { + const seen = new Set(); + const deduped: AdapterModel[] = []; + for (const model of models) { + const id = model.id.trim(); + if (!id || seen.has(id)) continue; + seen.add(id); + deduped.push({ id, label: model.label.trim() || id }); + } + return deduped; +} + +function sortModels(models: AdapterModel[]): AdapterModel[] { + return [...models].sort((a, b) => + a.id.localeCompare(b.id, "en", { numeric: true, sensitivity: "base" }), + ); +} + +function resolvePiCommand(input: unknown): string { + const envOverride = + typeof process.env.PAPERCLIP_PI_COMMAND === "string" && + process.env.PAPERCLIP_PI_COMMAND.trim().length > 0 + ? process.env.PAPERCLIP_PI_COMMAND.trim() + : "pi"; + return asString(input, envOverride); +} + +const discoveryCache = new Map(); +const VOLATILE_ENV_KEY_PREFIXES = ["PAPERCLIP_", "npm_", "NPM_"] as const; +const VOLATILE_ENV_KEY_EXACT = new Set(["PWD", "OLDPWD", "SHLVL", "_", "TERM_SESSION_ID"]); + +function isVolatileEnvKey(key: string): boolean { + if (VOLATILE_ENV_KEY_EXACT.has(key)) return true; + return VOLATILE_ENV_KEY_PREFIXES.some((prefix) => key.startsWith(prefix)); +} + +function hashValue(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +function discoveryCacheKey(command: string, cwd: string, env: Record) { + const envKey = Object.entries(env) + .filter(([key]) => !isVolatileEnvKey(key)) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}=${hashValue(value)}`) + .join("\n"); + return `${command}\n${cwd}\n${envKey}`; +} + +function pruneExpiredDiscoveryCache(now: number) { + for (const [key, value] of discoveryCache.entries()) { + if (value.expiresAt <= now) discoveryCache.delete(key); + } +} + +export async function discoverPiModels(input: { + command?: unknown; + cwd?: unknown; + env?: unknown; +} = {}): Promise { + const command = resolvePiCommand(input.command); + const cwd = asString(input.cwd, process.cwd()); + const env = normalizeEnv(input.env); + const runtimeEnv = normalizeEnv({ ...process.env, ...env }); + + const result = await runChildProcess( + `pi-models-${Date.now()}-${Math.random().toString(16).slice(2)}`, + command, + ["--list-models"], + { + cwd, + env: runtimeEnv, + timeoutSec: 20, + graceSec: 3, + onLog: async () => {}, + }, + ); + + if (result.timedOut) { + throw new Error("`pi --list-models` timed out."); + } + if ((result.exitCode ?? 1) !== 0) { + const detail = firstNonEmptyLine(result.stderr) || firstNonEmptyLine(result.stdout); + throw new Error(detail ? `\`pi --list-models\` failed: ${detail}` : "`pi --list-models` failed."); + } + + return sortModels(dedupeModels(parseModelsOutput(result.stdout))); +} + +function normalizeEnv(input: unknown): Record { + const envInput = typeof input === "object" && input !== null && !Array.isArray(input) + ? (input as Record) + : {}; + const env: Record = {}; + for (const [key, value] of Object.entries(envInput)) { + if (typeof value === "string") env[key] = value; + } + return env; +} + +export async function discoverPiModelsCached(input: { + command?: unknown; + cwd?: unknown; + env?: unknown; +} = {}): Promise { + const command = resolvePiCommand(input.command); + const cwd = asString(input.cwd, process.cwd()); + const env = normalizeEnv(input.env); + const key = discoveryCacheKey(command, cwd, env); + const now = Date.now(); + pruneExpiredDiscoveryCache(now); + const cached = discoveryCache.get(key); + if (cached && cached.expiresAt > now) return cached.models; + + const models = await discoverPiModels({ command, cwd, env }); + discoveryCache.set(key, { expiresAt: now + MODELS_CACHE_TTL_MS, models }); + return models; +} + +export async function ensurePiModelConfiguredAndAvailable(input: { + model?: unknown; + command?: unknown; + cwd?: unknown; + env?: unknown; +}): Promise { + const model = asString(input.model, "").trim(); + if (!model) { + throw new Error("Pi requires `adapterConfig.model` in provider/model format."); + } + + const models = await discoverPiModelsCached({ + command: input.command, + cwd: input.cwd, + env: input.env, + }); + + if (models.length === 0) { + throw new Error("Pi returned no models. Run `pi --list-models` and verify provider auth."); + } + + if (!models.some((entry) => entry.id === model)) { + const sample = models.slice(0, 12).map((entry) => entry.id).join(", "); + throw new Error( + `Configured Pi model is unavailable: ${model}. Available models: ${sample}${models.length > 12 ? ", ..." : ""}`, + ); + } + + return models; +} + +export async function listPiModels(): Promise { + try { + return await discoverPiModelsCached(); + } catch { + return []; + } +} + +export function resetPiModelsCacheForTests() { + discoveryCache.clear(); +} diff --git a/packages/adapters/pi-local/src/server/parse.test.ts b/packages/adapters/pi-local/src/server/parse.test.ts new file mode 100644 index 00000000..6a3eef4d --- /dev/null +++ b/packages/adapters/pi-local/src/server/parse.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, it } from "vitest"; +import { parsePiJsonl, isPiUnknownSessionError } from "./parse.js"; + +describe("parsePiJsonl", () => { + it("parses agent lifecycle and messages", () => { + const stdout = [ + JSON.stringify({ type: "agent_start" }), + JSON.stringify({ + type: "turn_end", + message: { + role: "assistant", + content: [{ type: "text", text: "Hello from Pi" }], + }, + }), + JSON.stringify({ type: "agent_end", messages: [] }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.messages).toContain("Hello from Pi"); + expect(parsed.finalMessage).toBe("Hello from Pi"); + }); + + it("parses streaming text deltas", () => { + const stdout = [ + JSON.stringify({ + type: "message_update", + assistantMessageEvent: { type: "text_delta", delta: "Hello " }, + }), + JSON.stringify({ + type: "message_update", + assistantMessageEvent: { type: "text_delta", delta: "World" }, + }), + JSON.stringify({ + type: "turn_end", + message: { + role: "assistant", + content: "Hello World", + }, + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.messages).toContain("Hello World"); + }); + + it("parses tool execution", () => { + const stdout = [ + JSON.stringify({ + type: "tool_execution_start", + toolCallId: "tool_1", + toolName: "read", + args: { path: "/tmp/test.txt" }, + }), + JSON.stringify({ + type: "tool_execution_end", + toolCallId: "tool_1", + toolName: "read", + result: "file contents", + isError: false, + }), + JSON.stringify({ + type: "turn_end", + message: { role: "assistant", content: "Done" }, + toolResults: [ + { + toolCallId: "tool_1", + content: "file contents", + isError: false, + }, + ], + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.toolCalls).toHaveLength(1); + expect(parsed.toolCalls[0].toolName).toBe("read"); + expect(parsed.toolCalls[0].result).toBe("file contents"); + expect(parsed.toolCalls[0].isError).toBe(false); + }); + + it("handles errors in tool execution", () => { + const stdout = [ + JSON.stringify({ + type: "tool_execution_start", + toolCallId: "tool_1", + toolName: "read", + args: { path: "/missing.txt" }, + }), + JSON.stringify({ + type: "tool_execution_end", + toolCallId: "tool_1", + toolName: "read", + result: "File not found", + isError: true, + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.toolCalls).toHaveLength(1); + expect(parsed.toolCalls[0].isError).toBe(true); + expect(parsed.toolCalls[0].result).toBe("File not found"); + }); + + it("extracts usage and cost from turn_end events", () => { + const stdout = [ + JSON.stringify({ + type: "turn_end", + message: { + role: "assistant", + content: "Response with usage", + usage: { + input: 100, + output: 50, + cacheRead: 20, + totalTokens: 170, + cost: { + input: 0.001, + output: 0.0015, + cacheRead: 0.0001, + cacheWrite: 0, + total: 0.0026, + }, + }, + }, + toolResults: [], + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.usage.inputTokens).toBe(100); + expect(parsed.usage.outputTokens).toBe(50); + expect(parsed.usage.cachedInputTokens).toBe(20); + expect(parsed.usage.costUsd).toBeCloseTo(0.0026, 4); + }); + + it("accumulates usage from multiple turns", () => { + const stdout = [ + JSON.stringify({ + type: "turn_end", + message: { + role: "assistant", + content: "First response", + usage: { + input: 50, + output: 25, + cacheRead: 0, + cost: { total: 0.001 }, + }, + }, + }), + JSON.stringify({ + type: "turn_end", + message: { + role: "assistant", + content: "Second response", + usage: { + input: 30, + output: 20, + cacheRead: 10, + cost: { total: 0.0015 }, + }, + }, + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.usage.inputTokens).toBe(80); + expect(parsed.usage.outputTokens).toBe(45); + expect(parsed.usage.cachedInputTokens).toBe(10); + expect(parsed.usage.costUsd).toBeCloseTo(0.0025, 4); + }); + + it("handles standalone usage events with Pi format", () => { + const stdout = [ + JSON.stringify({ + type: "usage", + usage: { + input: 200, + output: 100, + cacheRead: 50, + cost: { total: 0.005 }, + }, + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.usage.inputTokens).toBe(200); + expect(parsed.usage.outputTokens).toBe(100); + expect(parsed.usage.cachedInputTokens).toBe(50); + expect(parsed.usage.costUsd).toBe(0.005); + }); + + it("handles standalone usage events with generic format", () => { + const stdout = [ + JSON.stringify({ + type: "usage", + usage: { + inputTokens: 150, + outputTokens: 75, + cachedInputTokens: 25, + costUsd: 0.003, + }, + }), + ].join("\n"); + + const parsed = parsePiJsonl(stdout); + expect(parsed.usage.inputTokens).toBe(150); + expect(parsed.usage.outputTokens).toBe(75); + expect(parsed.usage.cachedInputTokens).toBe(25); + expect(parsed.usage.costUsd).toBe(0.003); + }); +}); + +describe("isPiUnknownSessionError", () => { + it("detects unknown session errors", () => { + expect(isPiUnknownSessionError("session not found: s_123", "")).toBe(true); + expect(isPiUnknownSessionError("", "unknown session id")).toBe(true); + expect(isPiUnknownSessionError("", "no session available")).toBe(true); + expect(isPiUnknownSessionError("all good", "")).toBe(false); + expect(isPiUnknownSessionError("working fine", "no errors")).toBe(false); + }); +}); diff --git a/packages/adapters/pi-local/src/server/parse.ts b/packages/adapters/pi-local/src/server/parse.ts new file mode 100644 index 00000000..3ba50d8b --- /dev/null +++ b/packages/adapters/pi-local/src/server/parse.ts @@ -0,0 +1,211 @@ +import { asNumber, asString, parseJson, parseObject } from "@paperclipai/adapter-utils/server-utils"; + +interface ParsedPiOutput { + sessionId: string | null; + messages: string[]; + errors: string[]; + usage: { + inputTokens: number; + outputTokens: number; + cachedInputTokens: number; + costUsd: number; + }; + finalMessage: string | null; + toolCalls: Array<{ toolCallId: string; toolName: string; args: unknown; result: string | null; isError: boolean }>; +} + +function asRecord(value: unknown): Record | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + return value as Record; +} + +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 parsePiJsonl(stdout: string): ParsedPiOutput { + const result: ParsedPiOutput = { + sessionId: null, + messages: [], + errors: [], + usage: { + inputTokens: 0, + outputTokens: 0, + cachedInputTokens: 0, + costUsd: 0, + }, + finalMessage: null, + toolCalls: [], + }; + + let currentToolCall: { toolCallId: string; toolName: string; args: unknown } | null = null; + + for (const rawLine of stdout.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line) continue; + + const event = parseJson(line); + if (!event) continue; + + const eventType = asString(event.type, ""); + + // RPC protocol messages - skip these (internal implementation detail) + if (eventType === "response" || eventType === "extension_ui_request" || eventType === "extension_ui_response" || eventType === "extension_error") { + continue; + } + + // Agent lifecycle + if (eventType === "agent_start") { + continue; + } + + if (eventType === "agent_end") { + const messages = event.messages as Array> | undefined; + if (messages && messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage?.role === "assistant") { + const content = lastMessage.content as string | Array<{ type: string; text?: string }>; + result.finalMessage = extractTextContent(content); + } + } + continue; + } + + // Turn lifecycle + if (eventType === "turn_start") { + continue; + } + + if (eventType === "turn_end") { + const message = asRecord(event.message); + if (message) { + const content = message.content as string | Array<{ type: string; text?: string }>; + const text = extractTextContent(content); + if (text) { + result.finalMessage = text; + result.messages.push(text); + } + + // Extract usage and cost from assistant message + const usage = asRecord(message.usage); + if (usage) { + result.usage.inputTokens += asNumber(usage.input, 0); + result.usage.outputTokens += asNumber(usage.output, 0); + result.usage.cachedInputTokens += asNumber(usage.cacheRead, 0); + + // Pi stores cost in usage.cost.total (and broken down in usage.cost.input, etc.) + const cost = asRecord(usage.cost); + if (cost) { + result.usage.costUsd += asNumber(cost.total, 0); + } + } + } + + // Tool results are in toolResults array + const toolResults = event.toolResults as Array> | undefined; + if (toolResults) { + for (const tr of toolResults) { + const toolCallId = asString(tr.toolCallId, ""); + const content = tr.content; + const isError = tr.isError === true; + + // Find matching tool call by toolCallId + const existingCall = result.toolCalls.find((tc) => tc.toolCallId === toolCallId); + if (existingCall) { + existingCall.result = typeof content === "string" ? content : JSON.stringify(content); + existingCall.isError = isError; + } + } + } + continue; + } + + // Message updates (streaming) + if (eventType === "message_update") { + const assistantEvent = asRecord(event.assistantMessageEvent); + if (assistantEvent) { + const msgType = asString(assistantEvent.type, ""); + if (msgType === "text_delta") { + const delta = asString(assistantEvent.delta, ""); + if (delta) { + // Append to last message or create new + if (result.messages.length === 0) { + result.messages.push(delta); + } else { + result.messages[result.messages.length - 1] += delta; + } + } + } + } + continue; + } + + // Tool execution + if (eventType === "tool_execution_start") { + const toolCallId = asString(event.toolCallId, ""); + const toolName = asString(event.toolName, ""); + const args = event.args; + currentToolCall = { toolCallId, toolName, args }; + result.toolCalls.push({ + toolCallId, + toolName, + args, + result: null, + isError: false, + }); + continue; + } + + if (eventType === "tool_execution_end") { + const toolCallId = asString(event.toolCallId, ""); + const toolName = asString(event.toolName, ""); + const toolResult = event.result; + const isError = event.isError === true; + + // Find the tool call by toolCallId (not toolName, to handle multiple calls to same tool) + const existingCall = result.toolCalls.find((tc) => tc.toolCallId === toolCallId); + if (existingCall) { + existingCall.result = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult); + existingCall.isError = isError; + } + currentToolCall = null; + continue; + } + + // Usage tracking if available in the event (fallback for standalone usage events) + if (eventType === "usage" || event.usage) { + const usage = asRecord(event.usage); + if (usage) { + // Support both Pi format (input/output/cacheRead) and generic format (inputTokens/outputTokens/cachedInputTokens) + result.usage.inputTokens += asNumber(usage.inputTokens ?? usage.input, 0); + result.usage.outputTokens += asNumber(usage.outputTokens ?? usage.output, 0); + result.usage.cachedInputTokens += asNumber(usage.cachedInputTokens ?? usage.cacheRead, 0); + + // Cost may be in usage.costUsd (direct) or usage.cost.total (Pi format) + const cost = asRecord(usage.cost); + if (cost) { + result.usage.costUsd += asNumber(cost.total ?? usage.costUsd, 0); + } else { + result.usage.costUsd += asNumber(usage.costUsd, 0); + } + } + } + } + + return result; +} + +export function isPiUnknownSessionError(stdout: string, stderr: string): boolean { + const haystack = `${stdout}\n${stderr}` + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .join("\n"); + + return /unknown\s+session|session\s+not\s+found|session\s+.*\s+not\s+found|no\s+session/i.test(haystack); +} diff --git a/packages/adapters/pi-local/src/server/test.ts b/packages/adapters/pi-local/src/server/test.ts new file mode 100644 index 00000000..cf8fa80a --- /dev/null +++ b/packages/adapters/pi-local/src/server/test.ts @@ -0,0 +1,276 @@ +import type { + AdapterEnvironmentCheck, + AdapterEnvironmentTestContext, + AdapterEnvironmentTestResult, +} from "@paperclipai/adapter-utils"; +import { + asString, + parseObject, + ensureAbsoluteDirectory, + ensureCommandResolvable, + ensurePathInEnv, + runChildProcess, +} from "@paperclipai/adapter-utils/server-utils"; +import { + asStringArray, +} from "@paperclipai/adapter-utils/server-utils"; +import { discoverPiModelsCached } from "./models.js"; +import { parsePiJsonl } from "./parse.js"; + +function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { + if (checks.some((check) => check.level === "error")) return "fail"; + if (checks.some((check) => check.level === "warn")) return "warn"; + return "pass"; +} + +function firstNonEmptyLine(text: string): string { + return ( + text + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean) ?? "" + ); +} + +function summarizeProbeDetail(stdout: string, stderr: string, parsedError: string | null): string | null { + const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout); + if (!raw) return null; + const clean = raw.replace(/\s+/g, " ").trim(); + const max = 240; + return clean.length > max ? `${clean.slice(0, max - 1)}...` : clean; +} + +function normalizeEnv(input: unknown): Record { + if (typeof input !== "object" || input === null || Array.isArray(input)) return {}; + const env: Record = {}; + for (const [key, value] of Object.entries(input as Record)) { + if (typeof value === "string") env[key] = value; + } + return env; +} + +const PI_AUTH_REQUIRED_RE = + /(?:auth(?:entication)?\s+required|api\s*key|invalid\s*api\s*key|not\s+logged\s+in|free\s+usage\s+exceeded)/i; + +export async function testEnvironment( + ctx: AdapterEnvironmentTestContext, +): Promise { + const checks: AdapterEnvironmentCheck[] = []; + const config = parseObject(ctx.config); + const command = asString(config.command, "pi"); + const cwd = asString(config.cwd, process.cwd()); + + try { + await ensureAbsoluteDirectory(cwd, { createIfMissing: false }); + checks.push({ + code: "pi_cwd_valid", + level: "info", + message: `Working directory is valid: ${cwd}`, + }); + } catch (err) { + checks.push({ + code: "pi_cwd_invalid", + level: "error", + message: err instanceof Error ? err.message : "Invalid working directory", + detail: cwd, + }); + } + + const envConfig = parseObject(config.env); + const env: Record = {}; + for (const [key, value] of Object.entries(envConfig)) { + if (typeof value === "string") env[key] = value; + } + const runtimeEnv = normalizeEnv(ensurePathInEnv({ ...process.env, ...env })); + + const cwdInvalid = checks.some((check) => check.code === "pi_cwd_invalid"); + if (cwdInvalid) { + checks.push({ + code: "pi_command_skipped", + level: "warn", + message: "Skipped command check because working directory validation failed.", + detail: command, + }); + } else { + try { + await ensureCommandResolvable(command, cwd, runtimeEnv); + checks.push({ + code: "pi_command_resolvable", + level: "info", + message: `Command is executable: ${command}`, + }); + } catch (err) { + checks.push({ + code: "pi_command_unresolvable", + level: "error", + message: err instanceof Error ? err.message : "Command is not executable", + detail: command, + }); + } + } + + const canRunProbe = + checks.every((check) => check.code !== "pi_cwd_invalid" && check.code !== "pi_command_unresolvable"); + + if (canRunProbe) { + try { + const discovered = await discoverPiModelsCached({ command, cwd, env: runtimeEnv }); + if (discovered.length > 0) { + checks.push({ + code: "pi_models_discovered", + level: "info", + message: `Discovered ${discovered.length} model(s) from Pi.`, + }); + } else { + checks.push({ + code: "pi_models_empty", + level: "warn", + message: "Pi returned no models.", + hint: "Run `pi --list-models` and verify provider authentication.", + }); + } + } catch (err) { + checks.push({ + code: "pi_models_discovery_failed", + level: "warn", + message: err instanceof Error ? err.message : "Pi model discovery failed.", + hint: "Run `pi --list-models` manually to verify provider auth and config.", + }); + } + } + + const configuredModel = asString(config.model, "").trim(); + if (!configuredModel) { + checks.push({ + code: "pi_model_required", + level: "error", + message: "Pi requires a configured model in provider/model format.", + hint: "Set adapterConfig.model using an ID from `pi --list-models`.", + }); + } else if (canRunProbe) { + // Verify model is in the list + try { + const discovered = await discoverPiModelsCached({ command, cwd, env: runtimeEnv }); + const modelExists = discovered.some((m: { id: string }) => m.id === configuredModel); + if (modelExists) { + checks.push({ + code: "pi_model_configured", + level: "info", + message: `Configured model: ${configuredModel}`, + }); + } else { + checks.push({ + code: "pi_model_not_found", + level: "warn", + message: `Configured model "${configuredModel}" not found in available models.`, + hint: "Run `pi --list-models` and choose a currently available provider/model ID.", + }); + } + } catch { + // If we can't verify, just note it + checks.push({ + code: "pi_model_configured", + level: "info", + message: `Configured model: ${configuredModel}`, + }); + } + } + + if (canRunProbe && configuredModel) { + // Parse model for probe + const provider = configuredModel.includes("/") + ? configuredModel.slice(0, configuredModel.indexOf("/")) + : ""; + const modelId = configuredModel.includes("/") + ? configuredModel.slice(configuredModel.indexOf("/") + 1) + : configuredModel; + const thinking = asString(config.thinking, "").trim(); + const extraArgs = (() => { + const fromExtraArgs = asStringArray(config.extraArgs); + if (fromExtraArgs.length > 0) return fromExtraArgs; + return asStringArray(config.args); + })(); + + const args = ["-p", "Respond with hello.", "--mode", "json"]; + if (provider) args.push("--provider", provider); + if (modelId) args.push("--model", modelId); + if (thinking) args.push("--thinking", thinking); + args.push("--tools", "read"); + if (extraArgs.length > 0) args.push(...extraArgs); + + try { + const probe = await runChildProcess( + `pi-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`, + command, + args, + { + cwd, + env: runtimeEnv, + timeoutSec: 60, + graceSec: 5, + onLog: async () => {}, + }, + ); + + const parsed = parsePiJsonl(probe.stdout); + const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errors[0] ?? null); + const authEvidence = `${parsed.errors.join("\n")}\n${probe.stdout}\n${probe.stderr}`.trim(); + + if (probe.timedOut) { + checks.push({ + code: "pi_hello_probe_timed_out", + level: "warn", + message: "Pi hello probe timed out.", + hint: "Retry the probe. If this persists, run Pi manually in this working directory.", + }); + } else if ((probe.exitCode ?? 1) === 0 && parsed.errors.length === 0) { + const summary = (parsed.finalMessage || parsed.messages.join(" ")).trim(); + const hasHello = /\bhello\b/i.test(summary); + checks.push({ + code: hasHello ? "pi_hello_probe_passed" : "pi_hello_probe_unexpected_output", + level: hasHello ? "info" : "warn", + message: hasHello + ? "Pi hello probe succeeded." + : "Pi probe ran but did not return `hello` as expected.", + ...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}), + ...(hasHello + ? {} + : { + hint: "Run `pi --mode json` manually and prompt `Respond with hello` to inspect output.", + }), + }); + } else if (PI_AUTH_REQUIRED_RE.test(authEvidence)) { + checks.push({ + code: "pi_hello_probe_auth_required", + level: "warn", + message: "Pi is installed, but provider authentication is not ready.", + ...(detail ? { detail } : {}), + hint: "Set provider API key environment variable (e.g., ANTHROPIC_API_KEY, XAI_API_KEY) and retry.", + }); + } else { + checks.push({ + code: "pi_hello_probe_failed", + level: "error", + message: "Pi hello probe failed.", + ...(detail ? { detail } : {}), + hint: "Run `pi --mode json` manually in this working directory to debug.", + }); + } + } catch (err) { + checks.push({ + code: "pi_hello_probe_failed", + level: "error", + message: "Pi hello probe failed.", + detail: err instanceof Error ? err.message : String(err), + hint: "Run `pi --mode json` manually in this working directory to debug.", + }); + } + } + + return { + adapterType: ctx.adapterType, + status: summarizeStatus(checks), + checks, + testedAt: new Date().toISOString(), + }; +} diff --git a/packages/adapters/pi-local/src/ui/build-config.ts b/packages/adapters/pi-local/src/ui/build-config.ts new file mode 100644 index 00000000..f871019d --- /dev/null +++ b/packages/adapters/pi-local/src/ui/build-config.ts @@ -0,0 +1,71 @@ +import type { CreateConfigValues } from "@paperclipai/adapter-utils"; + +function parseEnvVars(text: string): Record { + const env: Record = {}; + 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 { + if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {}; + const env: Record = {}; + 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; + 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 { + const ac: Record = {}; + 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; +} diff --git a/packages/adapters/pi-local/src/ui/index.ts b/packages/adapters/pi-local/src/ui/index.ts new file mode 100644 index 00000000..89b781a7 --- /dev/null +++ b/packages/adapters/pi-local/src/ui/index.ts @@ -0,0 +1,2 @@ +export { parsePiStdoutLine } from "./parse-stdout.js"; +export { buildPiLocalConfig } from "./build-config.js"; diff --git a/packages/adapters/pi-local/src/ui/parse-stdout.ts b/packages/adapters/pi-local/src/ui/parse-stdout.ts new file mode 100644 index 00000000..b80fe5f1 --- /dev/null +++ b/packages/adapters/pi-local/src/ui/parse-stdout.ts @@ -0,0 +1,147 @@ +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 | null { + if (typeof value !== "object" || value === null || Array.isArray(value)) return null; + return value as Record; +} + +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); + + // RPC protocol messages - filter these out (internal implementation detail) + if (type === "response" || type === "extension_ui_request" || type === "extension_ui_response" || type === "extension_error") { + return []; + } + + // 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> | 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 }]; +} diff --git a/packages/adapters/pi-local/tsconfig.json b/packages/adapters/pi-local/tsconfig.json new file mode 100644 index 00000000..2f355cfe --- /dev/null +++ b/packages/adapters/pi-local/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/adapters/pi-local/vitest.config.ts b/packages/adapters/pi-local/vitest.config.ts new file mode 100644 index 00000000..f624398e --- /dev/null +++ b/packages/adapters/pi-local/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +}); diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 5a35a78a..c7f85b57 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -27,6 +27,7 @@ export const AGENT_ADAPTER_TYPES = [ "claude_local", "codex_local", "opencode_local", + "pi_local", "cursor", "openclaw", "openclaw_gateway", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80ef3264..cb2d1d55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,12 +38,12 @@ importers: '@paperclipai/adapter-openclaw': specifier: workspace:* version: link:../packages/adapters/openclaw - '@paperclipai/adapter-openclaw-gateway': - specifier: workspace:* - version: link:../packages/adapters/openclaw-gateway '@paperclipai/adapter-opencode-local': specifier: workspace:* version: link:../packages/adapters/opencode-local + '@paperclipai/adapter-pi-local': + specifier: workspace:* + version: link:../packages/adapters/pi-local '@paperclipai/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils @@ -149,7 +149,7 @@ importers: specifier: ^5.7.3 version: 5.9.3 - packages/adapters/openclaw-gateway: + packages/adapters/opencode-local: dependencies: '@paperclipai/adapter-utils': specifier: workspace:* @@ -157,21 +157,15 @@ importers: picocolors: specifier: ^1.1.1 version: 1.1.1 - ws: - specifier: ^8.19.0 - version: 8.19.0 devDependencies: '@types/node': specifier: ^24.6.0 version: 24.12.0 - '@types/ws': - specifier: ^8.18.1 - version: 8.18.1 typescript: specifier: ^5.7.3 version: 5.9.3 - packages/adapters/opencode-local: + packages/adapters/pi-local: dependencies: '@paperclipai/adapter-utils': specifier: workspace:* @@ -242,12 +236,12 @@ importers: '@paperclipai/adapter-openclaw': specifier: workspace:* version: link:../packages/adapters/openclaw - '@paperclipai/adapter-openclaw-gateway': - specifier: workspace:* - version: link:../packages/adapters/openclaw-gateway '@paperclipai/adapter-opencode-local': specifier: workspace:* version: link:../packages/adapters/opencode-local + '@paperclipai/adapter-pi-local': + specifier: workspace:* + version: link:../packages/adapters/pi-local '@paperclipai/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils @@ -357,12 +351,12 @@ importers: '@paperclipai/adapter-openclaw': specifier: workspace:* version: link:../packages/adapters/openclaw - '@paperclipai/adapter-openclaw-gateway': - specifier: workspace:* - version: link:../packages/adapters/openclaw-gateway '@paperclipai/adapter-opencode-local': specifier: workspace:* version: link:../packages/adapters/opencode-local + '@paperclipai/adapter-pi-local': + specifier: workspace:* + version: link:../packages/adapters/pi-local '@paperclipai/adapter-utils': specifier: workspace:* version: link:../packages/adapter-utils @@ -390,9 +384,6 @@ importers: lucide-react: specifier: ^0.574.0 version: 0.574.0(react@19.2.4) - mermaid: - specifier: ^11.12.0 - version: 11.12.3 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -445,9 +436,6 @@ importers: packages: - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -723,9 +711,6 @@ packages: '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} - '@braintree/sanitize-url@7.1.2': - resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@changesets/apply-release-plan@7.1.0': resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} @@ -781,21 +766,6 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@chevrotain/cst-dts-gen@11.1.2': - resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} - - '@chevrotain/gast@11.1.2': - resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} - - '@chevrotain/regexp-to-ast@11.1.2': - resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} - - '@chevrotain/types@11.1.2': - resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} - - '@chevrotain/utils@11.1.2': - resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} - '@clack/core@0.4.2': resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} @@ -1456,12 +1426,6 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -1632,9 +1596,6 @@ packages: react: '>= 18 || >= 19' react-dom: '>= 18 || >= 19' - '@mermaid-js/parser@1.0.0': - resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} - '@noble/ciphers@2.1.1': resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} engines: {node: '>= 20.19.0'} @@ -2862,99 +2823,6 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.7': - resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} - - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.1.0': - resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2973,9 +2841,6 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/geojson@7946.0.16': - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -3032,9 +2897,6 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3305,14 +3167,6 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chevrotain-allstar@0.3.1: - resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} - peerDependencies: - chevrotain: ^11.0.0 - - chevrotain@11.1.2: - resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3357,14 +3211,6 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -3375,9 +3221,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -3404,12 +3247,6 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - - cose-base@2.2.0: - resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -3425,172 +3262,13 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - cytoscape-cose-bilkent@4.1.0: - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape-fcose@2.2.0: - resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape@3.33.1: - resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} - engines: {node: '>=0.10'} - - d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - - d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - - d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - - d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - - d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - - d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - - d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - - d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - - d3-sankey@0.12.3: - resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} - - d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - - d3-shape@1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 - - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - - d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} - d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} - dagre-d3-es@7.0.13: - resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} - dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3622,9 +3300,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -3667,10 +3342,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dompurify@3.3.2: - resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} - engines: {node: '>=20'} - dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -4030,9 +3701,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hachure-fill@0.5.2: - resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -4065,10 +3733,6 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -4086,13 +3750,6 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - intersection-observer@0.10.0: resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. @@ -4202,13 +3859,6 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - katex@0.16.37: - resolution: {integrity: sha512-TIGjO2cCGYono+uUzgkE7RFF329mLLWGuHUlSr6cwIVj9O8f0VQZ783rsanmJpFUo32vvtj7XT04NGRPh+SZFg==} - hasBin: true - - khroma@2.1.0: - resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -4217,16 +3867,6 @@ packages: resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} engines: {node: '>=20.0.0'} - langium@4.2.1: - resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - - layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - - layout-base@2.0.1: - resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - lexical@0.35.0: resolution: {integrity: sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==} @@ -4309,9 +3949,6 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -4343,11 +3980,6 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@16.4.2: - resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} - engines: {node: '>= 20'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4425,9 +4057,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.3: - resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -4578,9 +4207,6 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -4663,9 +4289,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -4673,9 +4296,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-data-parser@0.1.0: - resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4767,15 +4387,6 @@ packages: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - points-on-curve@0.2.0: - resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} - - points-on-path@0.2.1: - resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} @@ -4989,9 +4600,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rollup@4.57.1: resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5000,9 +4608,6 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} - roughjs@4.6.6: - resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -5014,9 +4619,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -5178,9 +4780,6 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - superagent@10.3.0: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} @@ -5215,10 +4814,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -5249,10 +4844,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5280,9 +4871,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -5355,10 +4943,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -5487,26 +5071,6 @@ packages: jsdom: optional: true - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -5561,11 +5125,6 @@ packages: snapshots: - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.0.2 - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -6194,8 +5753,6 @@ snapshots: '@better-fetch/fetch@1.1.21': {} - '@braintree/sanitize-url@7.1.2': {} - '@changesets/apply-release-plan@7.1.0': dependencies: '@changesets/config': 3.1.3 @@ -6339,23 +5896,6 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@chevrotain/cst-dts-gen@11.1.2': - dependencies: - '@chevrotain/gast': 11.1.2 - '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 - - '@chevrotain/gast@11.1.2': - dependencies: - '@chevrotain/types': 11.1.2 - lodash-es: 4.17.23 - - '@chevrotain/regexp-to-ast@11.1.2': {} - - '@chevrotain/types@11.1.2': {} - - '@chevrotain/utils@11.1.2': {} - '@clack/core@0.4.2': dependencies: picocolors: 1.1.1 @@ -6972,14 +6512,6 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.1.0': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - mlly: 1.8.1 - '@inquirer/external-editor@1.0.3(@types/node@25.2.3)': dependencies: chardet: 2.1.1 @@ -7336,10 +6868,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@mermaid-js/parser@1.0.0': - dependencies: - langium: 4.2.1 - '@noble/ciphers@2.1.1': {} '@noble/hashes@1.8.0': {} @@ -8672,123 +8200,6 @@ snapshots: '@types/cookiejar@2.1.5': {} - '@types/d3-array@3.2.2': {} - - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-chord@3.0.6': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.2 - '@types/geojson': 7946.0.16 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.7': {} - - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-dsv@3.0.7': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - - '@types/d3-hierarchy@3.1.7': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.1.0': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-selection@3.0.11': {} - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time-format@4.0.3': {} - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.7 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.1 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.9 - '@types/d3-scale-chromatic': 3.1.0 - '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -8814,8 +8225,6 @@ snapshots: '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 - '@types/geojson@7946.0.16': {} - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -8881,9 +8290,6 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/trusted-types@2.0.7': - optional: true - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -9121,20 +8527,6 @@ snapshots: check-error@2.1.3: {} - chevrotain-allstar@0.3.1(chevrotain@11.1.2): - dependencies: - chevrotain: 11.1.2 - lodash-es: 4.17.23 - - chevrotain@11.1.2: - dependencies: - '@chevrotain/cst-dts-gen': 11.1.2 - '@chevrotain/gast': 11.1.2 - '@chevrotain/regexp-to-ast': 11.1.2 - '@chevrotain/types': 11.1.2 - '@chevrotain/utils': 11.1.2 - lodash-es: 4.17.23 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -9184,10 +8576,6 @@ snapshots: commander@13.1.0: {} - commander@7.2.0: {} - - commander@8.3.0: {} - component-emitter@1.3.1: {} compute-scroll-into-view@2.0.4: {} @@ -9199,8 +8587,6 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - confbox@0.1.8: {} - content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -9215,14 +8601,6 @@ snapshots: cookiejar@2.1.4: {} - cose-base@1.0.3: - dependencies: - layout-base: 1.0.2 - - cose-base@2.2.0: - dependencies: - layout-base: 2.0.1 - crelt@1.0.6: {} cross-spawn@7.0.6: @@ -9235,199 +8613,13 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): - dependencies: - cose-base: 1.0.3 - cytoscape: 3.33.1 - - cytoscape-fcose@2.2.0(cytoscape@3.33.1): - dependencies: - cose-base: 2.2.0 - cytoscape: 3.33.1 - - cytoscape@3.33.1: {} - - d3-array@2.12.1: - dependencies: - internmap: 1.0.1 - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-axis@3.0.0: {} - - d3-brush@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3-chord@3.0.1: - dependencies: - d3-path: 3.1.0 - - d3-color@3.1.0: {} - - d3-contour@4.0.2: - dependencies: - d3-array: 3.2.4 - - d3-delaunay@6.0.4: - dependencies: - delaunator: 5.0.1 - - d3-dispatch@3.0.1: {} - - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - d3-dsv@3.0.1: - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - - d3-ease@3.0.1: {} - - d3-fetch@3.0.1: - dependencies: - d3-dsv: 3.0.1 - - d3-force@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@3.1.2: {} - - d3-geo@3.1.1: - dependencies: - d3-array: 3.2.4 - - d3-hierarchy@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@1.0.9: {} - - d3-path@3.1.0: {} - - d3-polygon@3.0.1: {} - - d3-quadtree@3.0.1: {} - - d3-random@3.0.1: {} - - d3-sankey@0.12.3: - dependencies: - d3-array: 2.12.1 - d3-shape: 1.3.7 - - d3-scale-chromatic@3.1.0: - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-selection@3.0.0: {} - - d3-shape@1.3.7: - dependencies: - d3-path: 1.0.9 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3@7.9.0: - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.2 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - d@1.0.2: dependencies: es5-ext: 0.10.64 type: 2.7.3 - dagre-d3-es@7.0.13: - dependencies: - d3: 7.9.0 - lodash-es: 4.17.23 - dateformat@4.6.3: {} - dayjs@1.11.19: {} - debug@4.4.3: dependencies: ms: 2.1.3 @@ -9449,10 +8641,6 @@ snapshots: defu@6.1.4: {} - delaunator@5.0.1: - dependencies: - robust-predicates: 3.0.2 - delayed-stream@1.0.0: {} depd@2.0.0: {} @@ -9484,10 +8672,6 @@ snapshots: dependencies: path-type: 4.0.0 - dompurify@3.3.2: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dotenv@16.6.1: {} dotenv@17.3.1: {} @@ -9894,8 +9078,6 @@ snapshots: graceful-fs@4.2.11: {} - hachure-fill@0.5.2: {} - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -9944,10 +9126,6 @@ snapshots: human-id@4.1.3: {} - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -9960,10 +9138,6 @@ snapshots: inline-style-parser@0.2.7: {} - internmap@1.0.1: {} - - internmap@2.0.3: {} - intersection-observer@0.10.0: {} ipaddr.js@1.9.1: {} @@ -10040,28 +9214,10 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - katex@0.16.37: - dependencies: - commander: 8.3.0 - - khroma@2.1.0: {} - kleur@4.1.5: {} kysely@0.28.11: {} - langium@4.2.1: - dependencies: - chevrotain: 11.1.2 - chevrotain-allstar: 0.3.1(chevrotain@11.1.2) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - layout-base@1.0.2: {} - - layout-base@2.0.1: {} - lexical@0.35.0: {} lib0@0.2.117: @@ -10121,8 +9277,6 @@ snapshots: dependencies: p-locate: 4.1.0 - lodash-es@4.17.23: {} - lodash.startcase@4.4.0: {} longest-streak@3.1.0: {} @@ -10149,8 +9303,6 @@ snapshots: markdown-table@3.0.4: {} - marked@16.4.2: {} - math-intrinsics@1.1.0: {} mdast-util-directive@3.1.0: @@ -10353,29 +9505,6 @@ snapshots: merge2@1.4.1: {} - mermaid@11.12.3: - dependencies: - '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.0 - '@types/d3': 7.4.3 - cytoscape: 3.33.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) - cytoscape-fcose: 2.2.0(cytoscape@3.33.1) - d3: 7.9.0 - d3-sankey: 0.12.3 - dagre-d3-es: 7.0.13 - dayjs: 1.11.19 - dompurify: 3.3.2 - katex: 0.16.37 - khroma: 2.1.0 - lodash-es: 4.17.23 - marked: 16.4.2 - roughjs: 4.6.6 - stylis: 4.3.6 - ts-dedent: 2.2.0 - uuid: 11.1.0 - methods@1.1.2: {} micromark-core-commonmark@2.0.3: @@ -10693,13 +9822,6 @@ snapshots: dependencies: minimist: 1.2.8 - mlly@1.8.1: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - mri@1.2.0: {} ms@2.1.3: {} @@ -10771,8 +9893,6 @@ snapshots: dependencies: quansync: 0.2.11 - package-manager-detector@1.6.0: {} - parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -10785,8 +9905,6 @@ snapshots: parseurl@1.3.3: {} - path-data-parser@0.1.0: {} - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -10889,19 +10007,6 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 3.1.0 - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.1 - pathe: 2.0.3 - - points-on-curve@0.2.0: {} - - points-on-path@0.2.1: - dependencies: - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 @@ -11173,8 +10278,6 @@ snapshots: reusify@1.1.0: {} - robust-predicates@3.0.2: {} - rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -11208,13 +10311,6 @@ snapshots: rou3@0.7.12: {} - roughjs@4.6.6: - dependencies: - hachure-fill: 0.5.2 - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - points-on-path: 0.2.1 - router@2.2.0: dependencies: debug: 4.4.3 @@ -11231,8 +10327,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rw@1.3.3: {} - sade@1.8.1: dependencies: mri: 1.2.0 @@ -11396,8 +10490,6 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylis@4.3.6: {} - superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -11438,8 +10530,6 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.2: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -11461,8 +10551,6 @@ snapshots: trough@2.2.0: {} - ts-dedent@2.2.0: {} - tslib@2.8.1: {} tsx@4.21.0: @@ -11489,8 +10577,6 @@ snapshots: typescript@5.9.3: {} - ufo@1.6.3: {} - undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -11567,8 +10653,6 @@ snapshots: util-deprecate@1.0.2: {} - uuid@11.1.0: {} - uvu@0.5.6: dependencies: dequal: 2.0.3 @@ -11774,23 +10858,6 @@ snapshots: - tsx - yaml - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - vscode-uri@3.1.0: {} - w3c-keyname@2.2.8: {} which@2.0.2: diff --git a/server/package.json b/server/package.json index 9ec0c494..eaf73505 100644 --- a/server/package.json +++ b/server/package.json @@ -35,6 +35,7 @@ "@paperclipai/adapter-codex-local": "workspace:*", "@paperclipai/adapter-cursor-local": "workspace:*", "@paperclipai/adapter-opencode-local": "workspace:*", + "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", "@paperclipai/adapter-utils": "workspace:*", diff --git a/server/src/__tests__/opencode-local-adapter-environment.test.ts b/server/src/__tests__/opencode-local-adapter-environment.test.ts index c539d771..736dd9f8 100644 --- a/server/src/__tests__/opencode-local-adapter-environment.test.ts +++ b/server/src/__tests__/opencode-local-adapter-environment.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { testEnvironment } from "@paperclipai/adapter-opencode-local/server"; describe("opencode_local environment diagnostics", () => { - it("creates a missing working directory when cwd is absolute", async () => { + it("reports a missing working directory as an error when cwd is absolute", async () => { const cwd = path.join( os.tmpdir(), `paperclip-opencode-local-cwd-${Date.now()}-${Math.random().toString(16).slice(2)}`, @@ -23,11 +23,9 @@ describe("opencode_local environment diagnostics", () => { }, }); - expect(result.checks.some((check) => check.code === "opencode_cwd_valid")).toBe(true); - expect(result.checks.some((check) => check.level === "error")).toBe(false); - const stats = await fs.stat(cwd); - expect(stats.isDirectory()).toBe(true); - await fs.rm(path.dirname(cwd), { recursive: true, force: true }); + expect(result.checks.some((check) => check.code === "opencode_cwd_invalid")).toBe(true); + expect(result.checks.some((check) => check.level === "error")).toBe(true); + expect(result.status).toBe("fail"); }); it("treats an empty OPENAI_API_KEY override as missing", async () => { diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index b58712dd..d9e153ed 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -45,6 +45,15 @@ import { } from "@paperclipai/adapter-openclaw-gateway"; import { listCodexModels } from "./codex-models.js"; import { listCursorModels } from "./cursor-models.js"; +import { + execute as piExecute, + testEnvironment as piTestEnvironment, + sessionCodec as piSessionCodec, + listPiModels, +} from "@paperclipai/adapter-pi-local/server"; +import { + agentConfigurationDoc as piAgentConfigurationDoc, +} from "@paperclipai/adapter-pi-local"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; @@ -110,11 +119,23 @@ const openCodeLocalAdapter: ServerAdapterModule = { agentConfigurationDoc: openCodeAgentConfigurationDoc, }; +const piLocalAdapter: ServerAdapterModule = { + type: "pi_local", + execute: piExecute, + testEnvironment: piTestEnvironment, + sessionCodec: piSessionCodec, + models: [], + listModels: listPiModels, + supportsLocalAgentJwt: true, + agentConfigurationDoc: piAgentConfigurationDoc, +}; + const adaptersByType = new Map( [ claudeLocalAdapter, codexLocalAdapter, openCodeLocalAdapter, + piLocalAdapter, cursorLocalAdapter, openclawAdapter, openclawGatewayAdapter, diff --git a/server/src/app.ts b/server/src/app.ts index 5ce4e292..d663654f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -136,7 +136,7 @@ export async function createApp( if (uiDist) { app.use(express.static(uiDist)); app.get(/.*/, (_req, res) => { - res.sendFile(path.join(uiDist, "index.html")); + res.sendFile("index.html", { root: uiDist }); }); } else { console.warn("[paperclip] UI dist not found; running in API-only mode"); diff --git a/server/src/routes/sidebar-badges.ts b/server/src/routes/sidebar-badges.ts index edf34709..0cd302e5 100644 --- a/server/src/routes/sidebar-badges.ts +++ b/server/src/routes/sidebar-badges.ts @@ -45,7 +45,7 @@ export function sidebarBadgeRoutes(db: Db) { const alertsCount = (summary.agents.error > 0 && !hasFailedRuns ? 1 : 0) + (summary.costs.monthBudgetCents > 0 && summary.costs.monthUtilizationPercent >= 80 ? 1 : 0); - badges.inbox = badges.failedRuns + alertsCount + staleIssueCount + joinRequestCount; + badges.inbox = badges.failedRuns + alertsCount + staleIssueCount + joinRequestCount + badges.approvals; res.json(badges); }); diff --git a/ui/package.json b/ui/package.json index e265209f..eee73a40 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,9 +17,10 @@ "@paperclipai/adapter-claude-local": "workspace:*", "@paperclipai/adapter-codex-local": "workspace:*", "@paperclipai/adapter-cursor-local": "workspace:*", + "@paperclipai/adapter-opencode-local": "workspace:*", + "@paperclipai/adapter-pi-local": "workspace:*", "@paperclipai/adapter-openclaw": "workspace:*", "@paperclipai/adapter-openclaw-gateway": "workspace:*", - "@paperclipai/adapter-opencode-local": "workspace:*", "@paperclipai/adapter-utils": "workspace:*", "@paperclipai/shared": "workspace:*", "@radix-ui/react-slot": "^1.2.4", diff --git a/ui/src/adapters/pi-local/config-fields.tsx b/ui/src/adapters/pi-local/config-fields.tsx new file mode 100644 index 00000000..e6afacb3 --- /dev/null +++ b/ui/src/adapters/pi-local/config-fields.tsx @@ -0,0 +1,47 @@ +import type { AdapterConfigFieldsProps } from "../types"; +import { + Field, + DraftInput, +} from "../../components/agent-config-primitives"; +import { ChoosePathButton } from "../../components/PathInstructionsModal"; + +const inputClass = + "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; +const instructionsFileHint = + "Absolute path to a markdown file (e.g. AGENTS.md) that defines this agent's behavior. Injected into the system prompt at runtime."; + +export function PiLocalConfigFields({ + isCreate, + values, + set, + config, + eff, + mark, +}: AdapterConfigFieldsProps) { + return ( + +
+ + isCreate + ? set!({ instructionsFilePath: v }) + : mark("adapterConfig", "instructionsFilePath", v || undefined) + } + immediate + className={inputClass} + placeholder="/absolute/path/to/AGENTS.md" + /> + +
+
+ ); +} diff --git a/ui/src/adapters/pi-local/index.ts b/ui/src/adapters/pi-local/index.ts new file mode 100644 index 00000000..cfebf669 --- /dev/null +++ b/ui/src/adapters/pi-local/index.ts @@ -0,0 +1,12 @@ +import type { UIAdapterModule } from "../types"; +import { parsePiStdoutLine } from "@paperclipai/adapter-pi-local/ui"; +import { PiLocalConfigFields } from "./config-fields"; +import { buildPiLocalConfig } from "@paperclipai/adapter-pi-local/ui"; + +export const piLocalUIAdapter: UIAdapterModule = { + type: "pi_local", + label: "Pi (local)", + parseStdoutLine: parsePiStdoutLine, + ConfigFields: PiLocalConfigFields, + buildAdapterConfig: buildPiLocalConfig, +}; diff --git a/ui/src/adapters/registry.ts b/ui/src/adapters/registry.ts index b2b69052..a641b265 100644 --- a/ui/src/adapters/registry.ts +++ b/ui/src/adapters/registry.ts @@ -3,6 +3,7 @@ import { claudeLocalUIAdapter } from "./claude-local"; import { codexLocalUIAdapter } from "./codex-local"; import { cursorLocalUIAdapter } from "./cursor"; import { openCodeLocalUIAdapter } from "./opencode-local"; +import { piLocalUIAdapter } from "./pi-local"; import { openClawUIAdapter } from "./openclaw"; import { openClawGatewayUIAdapter } from "./openclaw-gateway"; import { processUIAdapter } from "./process"; @@ -13,6 +14,7 @@ const adaptersByType = new Map( claudeLocalUIAdapter, codexLocalUIAdapter, openCodeLocalUIAdapter, + piLocalUIAdapter, cursorLocalUIAdapter, openClawUIAdapter, openClawGatewayUIAdapter, diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 5b1667ce..7a4bceeb 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -53,6 +53,7 @@ type AdapterType = | "claude_local" | "codex_local" | "opencode_local" + | "pi_local" | "cursor" | "process" | "http" @@ -666,6 +667,12 @@ export function OnboardingWizard() { icon: OpenCodeLogoIcon, desc: "Local multi-provider agent" }, + { + value: "pi_local" as const, + label: "Pi", + icon: Terminal, + desc: "Local Pi agent" + }, { value: "openclaw" as const, label: "OpenClaw", @@ -730,7 +737,7 @@ export function OnboardingWizard() { }} > {opt.recommended && ( - + Recommended )} @@ -748,6 +755,7 @@ export function OnboardingWizard() { {(adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "opencode_local" || + adapterType === "pi_local" || adapterType === "cursor") && (
diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 2888d8c9..06c3a2f4 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -468,7 +468,7 @@ export function AgentDetail() { disabled={agentAction.isPending || isPendingApproval} > - Invoke + Run Heartbeat {agent.status === "paused" ? (