diff --git a/.codemie/codemie-cli.config.json b/.codemie/codemie-cli.config.json index f5ebbebc5..22d84d1e1 100644 --- a/.codemie/codemie-cli.config.json +++ b/.codemie/codemie-cli.config.json @@ -8,10 +8,10 @@ "codeMieUrl": "https://codemie.lab.epam.com", "apiKey": "sso-provided", "baseUrl": "https://codemie.lab.epam.com/code-assistant-api", - "model": "claude-sonnet-4-6", + "model": "claude-sonnet-5", "haikuModel": "claude-haiku-4-5-20251001", - "sonnetModel": "claude-sonnet-4-6", - "opusModel": "claude-opus-4-8", + "sonnetModel": "claude-sonnet-5", + "opusModel": "claude-opus-5", "name": "epm-cdme" } }, diff --git a/.gitignore b/.gitignore index 7a6a6d067..bfbb1152a 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,5 @@ docs/superpowers/tasks/*/.state.json # otherwise get counted inside the next diff, inflating review scope. docs/superpowers/tasks/*/code-review.diff docs/superpowers/tasks/*/code-review-check.diff +/.pi/ +/.pi-subagents/ diff --git a/bin/codemie-pi.js b/bin/codemie-pi.js new file mode 100755 index 000000000..6ad4a4e19 --- /dev/null +++ b/bin/codemie-pi.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +/** + * Pi Agent Entry Point + * Direct entry point for codemie-pi command + */ + +import { AgentCLI } from '../dist/agents/core/AgentCLI.js'; +import { AgentRegistry } from '../dist/agents/registry.js'; + +const agent = AgentRegistry.getAgent('pi'); +if (!agent) { + console.error('✗ Pi agent not found in registry'); + process.exit(1); +} + +const cli = new AgentCLI(agent); +await cli.run(process.argv); diff --git a/docs/superpowers/plans/2026-08-07-codemie-pi-required-packages.md b/docs/superpowers/plans/2026-08-07-codemie-pi-required-packages.md new file mode 100644 index 000000000..05a79164b --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-codemie-pi-required-packages.md @@ -0,0 +1,225 @@ +# Install Required Pi Packages with `codemie install pi` — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `codemie install pi` install Pi and then globally install the three required Pi packages (`superpowers`, `pi-subagents`, `pi-mcp-adapter`). + +**Architecture:** Add a dedicated `pi.packages.ts` module that runs `pi install ` for each required package, and wire it into `PiPlugin.additionalInstallation()` so it executes after the npm install of Pi succeeds. + +**Tech Stack:** TypeScript, ES modules, Node.js `child_process` via the project’s `exec()` utility, project error classes. + +## Global Constraints + +- Packages must be installed **globally** (no `-l` project-local flag). +- Use the existing `exec()` utility from `src/utils/exec.ts` for all command execution. +- Fail-fast: if any package install fails, throw `AgentInstallationError` and abort the remaining installs. +- Respect `CODEMIE_PI_BIN` by using `this.metadata.cliCommand` from `PiPluginMetadata`. +- Per-package timeout default is 5 minutes (`300000` ms). +- Do not write or run tests unless the user explicitly asks; validate with `typecheck` and `lint` instead. +- Do not perform git operations unless the user explicitly asks. + +--- + +### Task 1: Create the Pi package installer module + +**Files:** +- Create: `src/agents/plugins/pi/pi.packages.ts` + +**Interfaces:** +- Consumes: `exec` from `@/utils/exec.js`, `AgentInstallationError` from `@/utils/errors.js`, `logger` from `@/utils/logger.js`. +- Produces: `REQUIRED_PI_PACKAGES: readonly string[]`, `InstallPiPackagesOptions` interface, `installRequiredPiPackages(options): Promise`. + +- [ ] **Step 1: Write `src/agents/plugins/pi/pi.packages.ts`** + +```typescript +import { exec } from '@/utils/exec.js'; +import { logger } from '@/utils/logger.js'; +import { AgentInstallationError } from '@/utils/errors.js'; + +export const REQUIRED_PI_PACKAGES: readonly string[] = [ + 'git:github.com/obra/superpowers', + 'npm:pi-subagents', + 'npm:pi-mcp-adapter', +]; + +export interface InstallPiPackagesOptions { + /** Pi CLI command name or path (default: 'pi') */ + cliCommand?: string; + /** Working directory for the install commands (default: process.cwd()) */ + cwd?: string; + /** Per-package timeout in milliseconds (default: 300000) */ + timeout?: number; +} + +const DEFAULT_TIMEOUT_MS = 300_000; + +export async function installRequiredPiPackages( + options: InstallPiPackagesOptions = {}, +): Promise { + const cliCommand = options.cliCommand || 'pi'; + const cwd = options.cwd ?? process.cwd(); + const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS; + + logger.info(`[pi] Installing required Pi packages using ${cliCommand}`); + + for (const pkg of REQUIRED_PI_PACKAGES) { + logger.info(`[pi] Installing package: ${pkg}`); + + const result = await exec(cliCommand, ['install', pkg], { + cwd, + timeout, + }); + + if (result.code !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join('\n'); + throw new AgentInstallationError( + 'pi', + `Failed to install Pi package "${pkg}": ${output}`, + ); + } + + logger.success(`[pi] Installed package: ${pkg}`); + } +} +``` + +- [ ] **Step 2: Verify the new module compiles in isolation** + +Run: +```bash +npx tsc --noEmit src/agents/plugins/pi/pi.packages.ts +``` + +Expected: no TypeScript errors. + +--- + +### Task 2: Wire the installer into `PiPlugin.additionalInstallation()` + +**Files:** +- Modify: `src/agents/plugins/pi/pi.plugin.ts` + +**Interfaces:** +- Consumes: `installRequiredPiPackages` and `InstallPiPackagesOptions` from `./pi.packages.js`. +- Produces: `PiPlugin.additionalInstallation()` override. + +- [ ] **Step 1: Add the import** + +Add this import after the existing imports in `src/agents/plugins/pi/pi.plugin.ts`: + +```typescript +import { installRequiredPiPackages } from './pi.packages.js'; +``` + +- [ ] **Step 2: Add the `additionalInstallation` method to `PiPlugin`** + +Insert the following method inside the `PiPlugin` class (after the `constructor`): + +```typescript + async additionalInstallation( + _options?: import('../../core/types.js').AgentInstallationOptions, + ): Promise { + await installRequiredPiPackages({ cliCommand: this.metadata.cliCommand }); + } +``` + +The full `PiPlugin` class should now look like: + +```typescript +export class PiPlugin extends BaseAgentAdapter { + constructor() { + super(PiPluginMetadata); + } + + async additionalInstallation( + _options?: import('../../core/types.js').AgentInstallationOptions, + ): Promise { + await installRequiredPiPackages({ cliCommand: this.metadata.cliCommand }); + } +} +``` + +- [ ] **Step 3: Verify the modified plugin compiles** + +Run: +```bash +npx tsc --noEmit src/agents/plugins/pi/pi.plugin.ts +``` + +Expected: no TypeScript errors. + +--- + +### Task 3: Project-wide validation + +**Files:** +- (no new files; validates changes from Tasks 1 and 2) + +- [ ] **Step 1: Run TypeScript typecheck** + +Run: +```bash +npm run typecheck +``` + +Expected: zero TypeScript errors. + +- [ ] **Step 2: Run linter** + +Run: +```bash +npm run lint +``` + +Expected: zero ESLint warnings or errors. + +- [ ] **Step 3: Run build** + +Run: +```bash +npm run build +``` + +Expected: build completes successfully. + +--- + +### Task 4: Manual smoke test (optional, requires Pi not already installed) + +**Files:** +- (no new files) + +- [ ] **Step 1: Run the install command** + +```bash +codemie install pi +``` + +Expected output includes three sequential `pi install` steps and ends with success messages for all packages. + +- [ ] **Step 2: Verify the packages exist globally** + +```bash +ls ~/.pi/agent/git/github.com/obra/superpowers +ls ~/.pi/agent/npm/pi-subagents +ls ~/.pi/agent/npm/pi-mcp-adapter +``` + +Expected: all three directories exist. + +--- + +## Self-Review + +**Spec coverage:** +- Install Pi via existing flow → unchanged, still handled by `BaseAgentAdapter`. +- Install `git:github.com/obra/superpowers` → Task 1 `REQUIRED_PI_PACKAGES`. +- Install `npm:pi-subagents` → Task 1 `REQUIRED_PI_PACKAGES`. +- Install `npm:pi-mcp-adapter` → Task 1 `REQUIRED_PI_PACKAGES`. +- Global install only → Task 1 uses `pi install ` without `-l`. +- Fail-fast on error → Task 1 throws `AgentInstallationError` when `result.code !== 0`. +- Use configured CLI command → Task 2 passes `this.metadata.cliCommand`. + +**Placeholder scan:** No TBD, TODO, or vague instructions remain. + +**Type consistency:** `installRequiredPiPackages` accepts `InstallPiPackagesOptions` and is called with `{ cliCommand: string | undefined }`, which matches the optional `cliCommand` property. diff --git a/docs/superpowers/plans/2026-08-07-codemie-pi.md b/docs/superpowers/plans/2026-08-07-codemie-pi.md new file mode 100644 index 000000000..83a5db276 --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-codemie-pi.md @@ -0,0 +1,609 @@ +# codemie-pi Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `codemie-pi` agent plugin that installs the upstream Pi npm package and launches it against the CodeMie proxy with a dynamically generated `models.json`. + +**Architecture:** Follow the existing agent plugin pattern (`src/agents/plugins//`). A new `PiPlugin` extends `BaseAgentAdapter`, implements `beforeRun` to prepare a CodeMie-managed Pi agent directory, and `enrichArgs` to inject `--provider` and `--model`. Model catalogue generation is isolated in `pi.models.ts`; path helpers live in `pi.paths.ts`. + +**Tech Stack:** TypeScript, ES modules, Node.js `fs/promises`, existing `fetchCodeMieLlmModels` / `CodeMieSSO` utilities. + +## Global Constraints + +- Node.js >= 20.0.0. +- ES modules only; imports use `.js` extensions. +- No `console.log` for debug output; use `logger.debug()`. +- All new exports have explicit return types. +- No `any`; use `unknown` + narrowing or precise types. +- Tests are out of scope unless explicitly requested by the user. +- Session analytics / MCP injection / skills mapping are out of scope for this first version. +- Use `@/` alias for deep imports; avoid `../../..` relative paths. + +--- + +## Task 1: Pi path helpers + +**Files:** +- Create: `src/agents/plugins/pi/pi.paths.ts` + +**Interfaces:** +- Produces: `getPiAgentDir(cwd?: string): string` — returns `/.pi/codemie/agent`. +- Produces: `getUserPiAgentDir(): string` — returns `~/.pi/agent`. +- Produces: `getPiModelsPath(cwd?: string): string` — returns `models.json` path inside the CodeMie-managed dir. + +- [ ] **Step 1: Implement path helpers** + +```typescript +import { join } from 'path'; +import { homedir } from 'os'; + +export function getUserPiAgentDir(): string { + return join(homedir(), '.pi', 'agent'); +} + +export function getPiAgentDir(cwd: string = process.cwd()): string { + return join(cwd, '.pi', 'codemie', 'agent'); +} + +export function getPiModelsPath(cwd: string = process.cwd()): string { + return join(getPiAgentDir(cwd), 'models.json'); +} +``` + +- [ ] **Step 2: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors related to the new file. + +--- + +## Task 2: Pi model catalogue builder + +**Files:** +- Create: `src/agents/plugins/pi/pi.models.ts` + +**Interfaces:** +- Consumes: `LlmModel` from `src/providers/plugins/sso/sso.http-client.js`. +- Produces: `fetchAndBuildPiModels(env: NodeJS.ProcessEnv, cwd?: string): Promise` — fetches live models and writes `/.pi/codemie/agent/models.json`. +- Produces: `classifyPiModel(modelId: string): PiModelClassification` — returns provider section and API override. + +- [ ] **Step 1: Define classification types and patterns** + +```typescript +export interface PiModelClassification { + provider: 'codemie-proxy' | 'codemie-anthropic'; + api?: 'openai-responses'; +} + +const RESPONSES_API_PATTERNS: RegExp[] = [ + /^gpt-5-2-/, + /^gpt-5\.2-/, + /^gpt-5-1-codex/, + /^gpt-5\.1-codex/, + /^gpt-5-3-codex/, + /^gpt-5\.3-codex/, + /^gpt-5\.4-/, + /^gpt-5-4-/, + /^gpt-5\.5-/, + /^gpt-5-5-/, + /^gpt-5\.6-/, + /^gpt-5-6-/, +]; + +export function classifyPiModel(modelId: string): PiModelClassification { + if (modelId.startsWith('claude')) { + return { provider: 'codemie-anthropic' }; + } + if (RESPONSES_API_PATTERNS.some(pattern => pattern.test(modelId))) { + return { provider: 'codemie-proxy', api: 'openai-responses' }; + } + return { provider: 'codemie-proxy' }; +} +``` + +- [ ] **Step 2: Implement model metadata heuristics** + +```typescript +import type { LlmModel } from '../../../providers/plugins/sso/sso.http-client.js'; + +export interface PiModelEntry { + id: string; + name: string; + api?: 'openai-responses'; + reasoning?: boolean; + thinkingLevelMap?: Record; + input: ('text' | 'image')[]; + contextWindow: number; + maxTokens: number; + compat?: Record; +} + +function detectLimits(id: string): { contextWindow: number; maxTokens: number } { + if (id.startsWith('claude')) return { contextWindow: 200000, maxTokens: 64000 }; + if (id.startsWith('gemini')) return { contextWindow: 1048576, maxTokens: 65536 }; + if (id.startsWith('gpt-4.1')) return { contextWindow: 1048576, maxTokens: 32768 }; + if (/^gpt-5\.5-/.test(id) || /^gpt-5-5-/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 }; + if (/^gpt-5\.6-/.test(id) || /^gpt-5-6-/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 }; + if (id.startsWith('gpt-5')) return { contextWindow: 400000, maxTokens: 128000 }; + if (/^o[134]-/.test(id) || id === 'o1') return { contextWindow: 200000, maxTokens: 100000 }; + if (id.startsWith('qwen') || id.startsWith('moonshotai') || id.startsWith('kimi')) { + return { contextWindow: 262144, maxTokens: 131072 }; + } + if (id.startsWith('deepseek')) return { contextWindow: 65536, maxTokens: 65536 }; + return { contextWindow: 128000, maxTokens: 4096 }; +} + +function defaultThinkingLevelMap(): Record { + return { + off: null, + minimal: 'minimal', + low: 'low', + medium: 'medium', + high: 'high', + xhigh: 'high', + max: 'high', + }; +} + +function isReasoningModel(id: string): boolean { + return ( + id.startsWith('claude') || + id.startsWith('gemini') || + id.startsWith('gpt-5') || + /^o[134]-/.test(id) || + id === 'o1' || + id.startsWith('deepseek') || + id.startsWith('moonshotai') || + id.startsWith('kimi') + ); +} + +export function convertLlmModelToPiEntry(model: LlmModel): PiModelEntry { + const id = model.deployment_name || model.base_name || model.label; + const classification = classifyPiModel(id); + const limits = detectLimits(id); + + const entry: PiModelEntry = { + id, + name: model.label || id, + ...(classification.api ? { api: classification.api } : {}), + ...(isReasoningModel(id) ? { reasoning: true, thinkingLevelMap: defaultThinkingLevelMap() } : {}), + input: model.multimodal ? ['text', 'image'] : ['text'], + contextWindow: limits.contextWindow, + maxTokens: limits.maxTokens, + }; + + if (id.startsWith('claude-sonnet-4-6') || id.startsWith('claude-sonnet-5') || /^claude-opus-4-[6-8]/.test(id) || id.startsWith('claude-opus-5')) { + entry.compat = { forceAdaptiveThinking: true }; + } + + return entry; +} +``` + +- [ ] **Step 3: Implement models.json writer** + +```typescript +import { mkdir, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { fetchCodeMieLlmModels } from '../../../providers/plugins/sso/sso.http-client.js'; +import { CodeMieSSO } from '../../../providers/plugins/sso/sso.auth.js'; +import { logger } from '../../../utils/logger.js'; +import { getPiAgentDir, getPiModelsPath } from './pi.paths.js'; + +interface PiModelsConfig { + providers: Record; + models: PiModelEntry[]; + }>; +} + +async function fetchCodeMieModels(env: NodeJS.ProcessEnv): Promise { + const jwtToken = env.CODEMIE_JWT_TOKEN; + const baseUrl = env.CODEMIE_BASE_URL; + + if (jwtToken && baseUrl) { + logger.debug('[pi-models] Fetching CodeMie model list via JWT auth'); + return fetchCodeMieLlmModels(baseUrl, jwtToken); + } + + const codeMieUrl = env.CODEMIE_URL; + if (codeMieUrl) { + const sso = new CodeMieSSO(); + const credentials = await sso.getStoredCredentials(codeMieUrl); + if (!credentials) { + throw new Error(`SSO credentials not found for ${codeMieUrl}. Run: codemie profile login --url ${codeMieUrl}`); + } + logger.debug('[pi-models] Fetching CodeMie model list via SSO auth'); + return fetchCodeMieLlmModels(credentials.apiUrl, credentials.cookies); + } + + throw new Error('No CodeMie authentication available. Run codemie setup or set CODEMIE_JWT_TOKEN.'); +} + +function buildStaticFallbackModel(modelId: string): PiModelsConfig { + const classification = classifyPiModel(modelId); + const entry = convertLlmModelToPiEntry({ + deployment_name: modelId, + label: modelId, + enabled: true, + multimodal: false, + features: {}, + } as LlmModel); + + return buildModelsConfig([entry], 'http://localhost:0', 'proxy-handled'); +} + +function buildModelsConfig( + entries: PiModelEntry[], + baseUrl: string, + apiKey: string, +): PiModelsConfig { + const proxyModels: PiModelEntry[] = []; + const anthropicModels: PiModelEntry[] = []; + + for (const entry of entries) { + const classification = classifyPiModel(entry.id); + if (classification.provider === 'codemie-anthropic') { + anthropicModels.push(entry); + } else { + proxyModels.push(entry); + } + } + + const providers: PiModelsConfig['providers'] = {}; + + if (proxyModels.length > 0) { + providers['codemie-proxy'] = { + baseUrl: `${baseUrl.replace(/\/$/, '')}/v1`, + api: 'openai-completions', + apiKey, + compat: { + supportsReasoningEffort: true, + thinkingFormat: 'reasoning_effort', + }, + models: proxyModels, + }; + } + + if (anthropicModels.length > 0) { + providers['codemie-anthropic'] = { + baseUrl: baseUrl.replace(/\/$/, ''), + api: 'anthropic-messages', + apiKey, + authHeader: true, + compat: { + supportsReasoningEffort: true, + thinkingFormat: 'reasoning_effort', + }, + models: anthropicModels, + }; + } + + return { providers }; +} + +export async function fetchAndBuildPiModels( + env: NodeJS.ProcessEnv, + cwd: string = process.cwd(), +): Promise { + const agentDir = getPiAgentDir(cwd); + await mkdir(agentDir, { recursive: true }); + + const baseUrl = env.CODEMIE_BASE_URL || ''; + const apiKey = env.CODEMIE_API_KEY || 'proxy-handled'; + + let entries: PiModelEntry[] = []; + try { + const rawModels = await fetchCodeMieModels(env); + entries = rawModels + .filter(model => model.enabled) + .map(convertLlmModelToPiEntry); + logger.debug(`[pi-models] Loaded ${entries.length} models from CodeMie API`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[pi-models] Failed to fetch live models, falling back to static model: ${message}`); + const configuredModel = env.CODEMIE_MODEL; + if (!configuredModel) { + throw new Error('No CodeMie model configured and live model fetch failed.'); + } + const fallback = buildStaticFallbackModel(configuredModel); + await writeFile(getPiModelsPath(cwd), JSON.stringify(fallback, null, 2), 'utf-8'); + return; + } + + if (entries.length === 0) { + throw new Error('CodeMie returned no enabled models for codemie-pi.'); + } + + const config = buildModelsConfig(entries, baseUrl, apiKey); + await writeFile(getPiModelsPath(cwd), JSON.stringify(config, null, 2), 'utf-8'); +} +``` + +- [ ] **Step 4: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors. + +--- + +## Task 3: Pi agent directory preparation helper + +**Files:** +- Create: `src/agents/plugins/pi/pi.setup.ts` + +**Interfaces:** +- Consumes: `getUserPiAgentDir()`, `getPiAgentDir()` from `pi.paths.ts`. +- Produces: `preparePiAgentDir(cwd?: string): Promise` — copies `~/.pi/agent` to the CodeMie-managed directory only on first run. + +- [ ] **Step 1: Implement directory copy helper** + +```typescript +import { existsSync } from 'fs'; +import { mkdir } from 'fs/promises'; +import { cp } from 'fs/promises'; +import { logger } from '../../../utils/logger.js'; +import { getPiAgentDir, getUserPiAgentDir } from './pi.paths.js'; + +export async function preparePiAgentDir(cwd: string = process.cwd()): Promise { + const sourceDir = getUserPiAgentDir(); + const destDir = getPiAgentDir(cwd); + + if (existsSync(destDir)) { + logger.debug(`[pi-setup] CodeMie Pi agent dir already exists, skipping copy: ${destDir}`); + return; + } + + if (!existsSync(sourceDir)) { + logger.warn(`[pi-setup] User Pi agent dir not found, starting fresh: ${sourceDir}`); + await mkdir(destDir, { recursive: true }); + return; + } + + logger.debug(`[pi-setup] Copying ${sourceDir} → ${destDir}`); + await cp(sourceDir, destDir, { recursive: true, force: true }); +} +``` + +- [ ] **Step 2: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors. + +--- + +## Task 4: Pi plugin + +**Files:** +- Create: `src/agents/plugins/pi/pi.plugin.ts` + +**Interfaces:** +- Consumes: `preparePiAgentDir()` from `pi.setup.ts`. +- Consumes: `fetchAndBuildPiModels()` from `pi.models.ts`. +- Produces: `PiPluginMetadata: AgentMetadata`. +- Produces: `PiPlugin extends BaseAgentAdapter`. + +- [ ] **Step 1: Implement plugin metadata and class** + +```typescript +import type { AgentMetadata, AgentConfig } from '../../core/types.js'; +import { BaseAgentAdapter } from '../../core/BaseAgentAdapter.js'; +import { logger } from '../../../utils/logger.js'; +import { preparePiAgentDir } from './pi.setup.js'; +import { fetchAndBuildPiModels, classifyPiModel } from './pi.models.js'; +import { getPiAgentDir } from './pi.paths.js'; + +export const PiPluginMetadata: AgentMetadata = { + name: 'pi', + displayName: 'Pi', + description: 'Pi - open-source coding agent harness', + npmPackage: '@earendil-works/pi-coding-agent', + cliCommand: process.env.CODEMIE_PI_BIN || 'pi', + + sessionAnalyticsReport: false, + + dataPaths: { + home: '.pi', + }, + + envMapping: { + baseUrl: [], + apiKey: [], + model: [], + }, + + supportedProviders: ['ai-run-sso', 'bearer-auth', 'litellm'], + + ssoConfig: { + enabled: true, + clientType: 'codemie-pi', + }, + + lifecycle: { + async beforeRun(env: NodeJS.ProcessEnv, _config: AgentConfig) { + const cwd = process.cwd(); + await preparePiAgentDir(cwd); + await fetchAndBuildPiModels(env, cwd); + env.PI_CODING_AGENT_DIR = getPiAgentDir(cwd); + logger.debug('[pi] Configured PI_CODING_AGENT_DIR', { path: env.PI_CODING_AGENT_DIR }); + return env; + }, + + enrichArgs(args: string[], _config: AgentConfig): string[] { + const model = process.env.CODEMIE_MODEL; + if (!model) { + throw new Error('No model configured for codemie-pi. Run codemie setup to select a model.'); + } + + const classification = classifyPiModel(model); + const providerId = classification.provider; + + let result = args; + + const taskIndex = result.indexOf('--task'); + if (taskIndex !== -1 && taskIndex < result.length - 1) { + const taskValue = result[taskIndex + 1]; + result = [...result.slice(0, taskIndex), ...result.slice(taskIndex + 2), taskValue]; + } + + return ['--provider', providerId, '--model', model, ...result]; + }, + }, +}; + +export class PiPlugin extends BaseAgentAdapter { + constructor() { + super(PiPluginMetadata); + } +} +``` + +- [ ] **Step 2: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors. + +--- + +## Task 5: Plugin index + +**Files:** +- Create: `src/agents/plugins/pi/index.ts` + +- [ ] **Step 1: Re-export plugin** + +```typescript +export { PiPlugin, PiPluginMetadata } from './pi.plugin.js'; +``` + +--- + +## Task 6: CLI entry point + +**Files:** +- Create: `bin/codemie-pi.js` + +- [ ] **Step 1: Add entry point** + +```javascript +#!/usr/bin/env node + +/** + * Pi Agent Entry Point + * Direct entry point for codemie-pi command + */ + +import { AgentCLI } from '../dist/agents/core/AgentCLI.js'; +import { AgentRegistry } from '../dist/agents/registry.js'; + +const agent = AgentRegistry.getAgent('pi'); +if (!agent) { + console.error('✗ Pi agent not found in registry'); + process.exit(1); +} + +const cli = new AgentCLI(agent); +await cli.run(process.argv); +``` + +- [ ] **Step 2: Make file executable** + +Run: `chmod +x bin/codemie-pi.js` + +--- + +## Task 7: Register plugin + +**Files:** +- Modify: `src/agents/registry.ts` + +- [ ] **Step 1: Import and register PiPlugin** + +Add near the top with other plugin imports: + +```typescript +import { PiPlugin } from './plugins/pi/index.js'; +``` + +Add in `AgentRegistry.initialize()` before `AgentRegistry.initialized = true;`: + +```typescript +AgentRegistry.registerPlugin(new PiPlugin()); +``` + +- [ ] **Step 2: Run typecheck** + +Run: `npm run typecheck` +Expected: No errors. + +--- + +## Task 8: Add npm bin entry + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: Add codemie-pi bin** + +In the `bin` object, add: + +```json +"codemie-pi": "./bin/codemie-pi.js" +``` + +- [ ] **Step 2: Validate JSON** + +Run: `node -e "JSON.parse(require('fs').readFileSync('package.json','utf8')); console.log('package.json valid')"` +Expected: Prints `package.json valid`. + +--- + +## Task 9: Build and final verification + +- [ ] **Step 1: Install dependencies** + +Run: `npm install` +Expected: Completes without errors. + +- [ ] **Step 2: Build** + +Run: `npm run build` +Expected: TypeScript compiles successfully; `dist/agents/plugins/pi/` and `dist/agents/registry.js` exist. + +- [ ] **Step 3: Lint** + +Run: `npm run lint` +Expected: Zero warnings/errors. + +- [ ] **Step 4: Manual smoke test** + +Run: `codemie install pi` then `codemie-pi --task "hello"` in a test project. +Expected: +- `/.pi/codemie/agent/models.json` exists. +- The file contains `codemie-proxy` and/or `codemie-anthropic` providers. +- The `baseUrl` values point to the local CodeMie proxy. +- Pi starts and routes chat through the selected model. + +--- + +## Self-review + +**Spec coverage:** +- npm global install of upstream Pi → covered by `AgentMetadata.npmPackage`. +- `PI_CODING_AGENT_DIR` set to cwd-relative dir → covered in `beforeRun`. +- Copy `~/.pi/agent` on first run → covered in `preparePiAgentDir`. +- Live model fetch and `models.json` generation → covered in `pi.models.ts`. +- `--provider`/`--model` injection → covered in `enrichArgs`. +- Session analytics out of scope → `sessionAnalyticsReport: false`. + +**Placeholder scan:** No TBD/TODO/fill-in-details. All functions include concrete code. + +**Type consistency:** +- `getPiAgentDir(cwd?: string): string` used consistently. +- `classifyPiModel(modelId: string): PiModelClassification` used in both `pi.models.ts` and `pi.plugin.ts`. +- `AgentMetadata.lifecycle.beforeRun` signature matches the interface. diff --git a/docs/superpowers/specs/2026-08-07-codemie-pi-design.md b/docs/superpowers/specs/2026-08-07-codemie-pi-design.md new file mode 100644 index 000000000..5287695df --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-codemie-pi-design.md @@ -0,0 +1,178 @@ +# Design: codemie-pi Agent Plugin + +**Status:** Approved for implementation +**Scope:** First version — launch Pi with CodeMie proxy and live models; session analytics out of scope. + +## 1. Goal + +Add a new `codemie-pi` agent to `codemie-code` that installs the upstream `@earendil-works/pi-coding-agent` npm package and configures it to route all LLM traffic through the CodeMie local proxy using models provided by the CodeMie backend. + +## 2. Background + +- `codemie-code` already supports `codemie-claude`, `codemie-codex`, and `codemie-opencode` via the plugin architecture in `src/agents/plugins/`. +- Pi (`@earendil-works/pi-coding-agent`) is an external CLI whose configuration is driven by files in its agent directory (`~/.pi/agent` by default) plus CLI flags. +- Pi supports custom providers via `models.json`. A provider declares `api` (`openai-completions`, `openai-responses`, `anthropic-messages`, etc.), `baseUrl`, `apiKey`, and a list of models. +- Pi's agent directory can be relocated with the `PI_CODING_AGENT_DIR` environment variable. + +## 3. High-level approach + +**Approach A — Generate Pi `models.json`:** + +1. Install Pi globally from npm (`@earendil-works/pi-coding-agent`). +2. At runtime, copy the user's existing `~/.pi/agent` into a CodeMie-managed directory under the current working directory. +3. Fetch the live model catalogue from the CodeMie proxy/backend. +4. Generate a fresh `models.json` inside the CodeMie-managed directory with two providers: + - `codemie-proxy` — OpenAI-compatible models (`api: "openai-completions"`, `baseUrl: /v1`). + - `codemie-anthropic` — Claude models (`api: "anthropic-messages"`, `baseUrl: `, `authHeader: true`). +5. Set `PI_CODING_AGENT_DIR` and invoke `pi --provider --model [args...]`. + +This approach was selected because it works within Pi's native config system, preserves user skills/extensions/tools, and follows the same injection pattern used by other `codemie-*` agents. + +## 4. Plugin metadata + +```typescript +export const PiPluginMetadata: AgentMetadata = { + name: 'pi', + displayName: 'Pi', + description: 'Pi - open-source coding agent harness', + npmPackage: '@earendil-works/pi-coding-agent', + cliCommand: process.env.CODEMIE_PI_BIN || 'pi', + + sessionAnalyticsReport: false, // out of scope for first version + + dataPaths: { + home: '.pi', + }, + + envMapping: { + baseUrl: [], + apiKey: [], + model: [], + }, + + supportedProviders: ['ai-run-sso', 'bearer-auth', 'litellm'], + + ssoConfig: { + enabled: true, + clientType: 'codemie-pi', + }, + + lifecycle: { beforeRun, enrichArgs }, +}; +``` + +Notes: +- `envMapping` is intentionally empty because Pi does not read `CODEMIE_BASE_URL` / `CODEMIE_API_KEY` / `CODEMIE_MODEL` natively. +- No `supportedVersion`/`minimumSupportedVersion` for the first version to avoid blocking Pi's rapid release cycle. Version pinning can be added once a stable Pi API surface is validated. + +## 5. File layout + +``` +src/agents/plugins/pi/ +├── pi.plugin.ts # AgentMetadata + PiPlugin class +├── pi.models.ts # Fetch CodeMie models + build Pi models.json +├── pi.paths.ts # Resolve cwd-relative agent dir +└── index.ts # Re-exports +bin/codemie-pi.js # Entry point +package.json # Add "codemie-pi" bin entry +src/agents/registry.ts # Register PiPlugin +``` + +## 6. Runtime data flow + +1. `bin/codemie-pi.js` resolves `PiPlugin` from `AgentRegistry` and runs it via `AgentCLI`. +2. `BaseAgentAdapter.run()`: + - Generates a `CODEMIE_SESSION_ID`. + - Calls `setupProxy()`, which starts the local CodeMie proxy and sets: + - `CODEMIE_BASE_URL = http://127.0.0.1:` + - `CODEMIE_API_KEY = proxy-handled` +3. Lifecycle `beforeRun(env, config)`: + - Resolve `agentDir = join(cwd, '.pi', 'codemie', 'agent')`. + - Copy `~/.pi/agent` → `agentDir` recursively (first run); on later runs only regenerate `models.json`. + - Fetch live models from `CODEMIE_BASE_URL/v1/llm_models?include_all=true` via existing `fetchCodeMieLlmModels` (JWT or SSO auth). + - Generate `agentDir/models.json`. + - Set `env.PI_CODING_AGENT_DIR = agentDir`. +4. Lifecycle `enrichArgs(args, config)`: + - Convert `--task ` to a trailing Pi message argument. + - Prepend `['--provider', providerId, '--model', modelId]` based on the selected model's family. +5. Spawn `pi ` with the modified environment. + +## 7. Model classification + +Models returned by the CodeMie `/v1/llm_models` endpoint are classified into provider sections: + +| Model family | Provider section | `api` override | Notes | +|---|---|---|---| +| `claude-*` | `codemie-anthropic` | — | Provider has `authHeader: true` | +| `gpt-5-2-*`, `gpt-5.2-*`, `gpt-5-1-codex-*`, `gpt-5.1-codex-*`, `gpt-5.3-codex-*`, `gpt-5.4-*`, `gpt-5.5-*`, `gpt-5.6-*`, `gpt-5-6-*` | `codemie-proxy` | `openai-responses` | Same patterns used by OpenCode/Codex | +| Everything else (gpt-4*, o*, gemini*, deepseek*, qwen*, kimi*, etc.) | `codemie-proxy` | — | Default `openai-completions` | + +Per-model fields are derived from heuristics, consistent with `opencode-dynamic-models.ts`: +- `name`: `model.label || model.deployment_name` +- `reasoning`: `true` for known reasoning families +- `thinkingLevelMap`: family-specific mapping +- `input`: `['text', 'image']` if multimodal, else `['text']` +- `contextWindow` / `maxTokens`: family-specific defaults +- `compat`: provider-level `supportsReasoningEffort: true, thinkingFormat: "reasoning_effort"`; per-model `forceAdaptiveThinking: true` for newer Claude models + +Provider-level fields: +- `codemie-proxy.baseUrl`: `${CODEMIE_BASE_URL}/v1` +- `codemie-anthropic.baseUrl`: `CODEMIE_BASE_URL` +- `apiKey`: `CODEMIE_API_KEY` (typically `"proxy-handled"`) + +## 8. Directory copy behavior + +- **Source:** `~/.pi/agent` (resolved with Pi's default logic: `join(homedir(), '.pi', 'agent')`). +- **Destination:** `join(process.cwd(), '.pi', 'codemie', 'agent')`. +- **First run:** if the destination directory does not exist, recursively copy the entire source tree. +- **Subsequent runs:** keep the existing destination tree and overwrite only `models.json` so user modifications (settings, installed tools, etc.) survive while the model catalogue stays current. +- **Missing source:** create an empty destination directory and write only `models.json`. +- **Concurrency:** concurrent `codemie-pi` runs in the same working directory may race on `models.json`; acceptable for the first version because each run refreshes the same catalogue. + +## 9. CLI argument transformations + +Pi accepts `--provider `, `--model `, and positional messages. `enrichArgs` will: + +1. If `--task ` is present, strip the flag and append `` as a positional message argument. +2. Determine `providerId` from the selected model family. +3. Prepend `['--provider', providerId, '--model', env.CODEMIE_MODEL]`. + +Example: +``` +codemie-pi --task "review this code" +→ pi --provider codemie-proxy --model gpt-5.5-2026-04-24 "review this code" +``` + +## 10. Error handling + +| Scenario | Behavior | +|---|---| +| Model fetch fails | Log warning, fall back to a minimal static `models.json` containing `CODEMIE_MODEL` if configured; otherwise throw `ConfigurationError`. | +| Copy from `~/.pi/agent` fails | Log warning, continue with an empty destination directory and generated `models.json`. | +| Selected model cannot be mapped to a provider | Throw `ConfigurationError` with the model id and available families. | +| Pi binary not found | `isInstalled()` returns `false`; `codemie install pi` installs the npm package globally. | + +## 11. Out of scope + +- Session analytics / metrics sync for Pi (separate task). +- Mapping CodeMie skills/extensions into Pi's extension model. +- MCP proxy injection into Pi. +- Version pinning / compatibility checks. + +## 12. Verification + +Manual verification steps: +1. `codemie install pi` +2. `codemie pi --task "hello"` (or `codemie-pi --task "hello"`) +3. Confirm `/.pi/codemie/agent/models.json` exists and contains `codemie-proxy` and `codemie-anthropic` providers with live models. +4. Confirm `PI_CODING_AGENT_DIR` is set to `/.pi/codemie/agent` in the spawned Pi process. + +## 13. References + +- `src/agents/plugins/opencode/opencode.plugin.ts` — config injection pattern +- `src/agents/plugins/opencode/opencode-dynamic-models.ts` — live model fetch + family detection +- `src/agents/plugins/codex/codex-models.ts` — model filtering/catalog generation +- `src/agents/core/BaseAgentAdapter.ts` — proxy setup + lifecycle orchestration +- `src/agents/registry.ts` — plugin registration +- Pi source: `/home/taras_spashchenko/TS/github/pi/packages/coding-agent/src/core/model-config.ts` — `models.json` schema +- Pi source: `/home/taras_spashchenko/TS/github/pi/packages/coding-agent/src/config.ts` — `PI_CODING_AGENT_DIR` diff --git a/docs/superpowers/specs/2026-08-07-codemie-pi-required-packages-design.md b/docs/superpowers/specs/2026-08-07-codemie-pi-required-packages-design.md new file mode 100644 index 000000000..40ba0eb79 --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-codemie-pi-required-packages-design.md @@ -0,0 +1,143 @@ +# Design: Install Required Pi Packages with `codemie install pi` + +**Status:** Approved for implementation +**Scope:** Extend the `codemie-pi` agent plugin so that running `codemie install pi` installs Pi and then installs the three required Pi packages globally. + +## 1. Goal + +When a user runs: + +```bash +codemie install pi +``` + +CodeMie must: + +1. Install the Pi CLI (`@earendil-works/pi-coding-agent`) via the existing npm-based installation flow. +2. Install the following required Pi packages globally: + - `git:github.com/obra/superpowers` + - `npm:pi-subagents` + - `npm:pi-mcp-adapter` + +These packages live in `~/.pi/agent/git/` and `~/.pi/agent/npm/` after installation and are later copied into a project-local Pi agent directory by the existing `preparePiAgentDir` logic. + +## 2. Background + +- `PiPlugin` (`src/agents/plugins/pi/pi.plugin.ts`) extends `BaseAgentAdapter` and relies on `BaseAgentAdapter.install()` to install the npm package declared in `AgentMetadata.npmPackage`. +- `createInstallCommand` in `src/cli/commands/install.ts` calls `agent.additionalInstallation(options)` after the agent is installed. +- Pi’s own `pi install ` command installs packages globally by default. +- Pi already supports idempotent re-installation and skips unchanged packages. + +## 3. High-level approach + +**Approach B — Dedicated Pi package installer module.** + +1. Create `src/agents/plugins/pi/pi.packages.ts`. +2. Export the list of required packages and an `installRequiredPiPackages()` function. +3. `PiPlugin.additionalInstallation()` invokes the installer after Pi itself is installed. + +This keeps the package list and install logic isolated and testable without mixing it into the plugin metadata class. + +## 4. File layout + +``` +src/agents/plugins/pi/ +├── pi.plugin.ts # Existing plugin; add additionalInstallation() override +├── pi.packages.ts # NEW: required package list + installer +├── pi.setup.ts # Existing agent directory preparation +├── pi.models.ts # Existing live model generation +└── index.ts # Existing re-exports +``` + +## 5. Implementation details + +### 5.1 `pi.packages.ts` + +```typescript +export const REQUIRED_PI_PACKAGES: readonly string[] = [ + 'git:github.com/obra/superpowers', + 'npm:pi-subagents', + 'npm:pi-mcp-adapter', +]; + +export interface InstallPiPackagesOptions { + /** Pi CLI command (default: 'pi') */ + cliCommand?: string; + /** Working directory for the install process (default: process.cwd()) */ + cwd?: string; + /** Per-package timeout in milliseconds (default: 300000) */ + timeout?: number; +} + +export async function installRequiredPiPackages( + options?: InstallPiPackagesOptions, +): Promise +``` + +Behavior: + +- Resolve `cliCommand` from options or fall back to `'pi'`. +- Run the following for each package in `REQUIRED_PI_PACKAGES`, sequentially: + ```bash + pi install + ``` +- Use `exec` from `src/utils/exec.js`. +- Set `cwd` to `options.cwd ?? process.cwd()`. +- Set a per-package timeout (default 5 minutes). +- Log each package installation attempt using `logger.info` and success via `logger.success`. +- If a package installation returns a non-zero exit code, throw `AgentInstallationError` with the package and captured stderr/stdout (fail-fast). + +### 5.2 `PiPlugin.additionalInstallation()` + +Override in `src/agents/plugins/pi/pi.plugin.ts`: + +```typescript +async additionalInstallation( + _options?: import('../../core/types.js').AgentInstallationOptions, +): Promise { + await installRequiredPiPackages({ cliCommand: this.metadata.cliCommand }); +} +``` + +The base adapter calls this after the npm install succeeds, so the `pi` binary is expected to be available in `PATH`. + +### 5.3 Idempotency + +`additionalInstallation()` is invoked every time `codemie install pi` runs, including when Pi is already installed. `pi install` handles already-installed packages efficiently, so no extra guard is required. + +## 6. Error handling + +| Scenario | Behavior | +|---|---| +| Pi binary missing after npm install | `exec` fails; throw `AgentInstallationError` with the command and failure reason. | +| One `pi install ` fails | Stop immediately; throw `AgentInstallationError` naming the failed package and including command output. | +| Network timeout during package install | `exec` timeout fires; propagate as `AgentInstallationError`. | +| Package already installed | `pi install` short-circuits; continue to the next package. | + +## 7. Out of scope + +- Installing packages project-locally (`pi install -l`). The user confirmed global install only. +- Pinning package versions or updating them separately from `codemie install pi`. +- Making the package list configurable via `codemie-cli.config.json` for this iteration. + +## 8. Verification + +Manual verification steps: + +1. Run `codemie install pi`. +2. Confirm Pi CLI is installed: `pi --version`. +3. Confirm the three packages exist under `~/.pi/agent/`: + - `~/.pi/agent/git/github.com/obra/superpowers` + - `~/.pi/agent/npm/pi-subagents` + - `~/.pi/agent/npm/pi-mcp-adapter` +4. Run `codemie-pi --task "hello"` in a test project and confirm `preparePiAgentDir` copies the packages into `/.pi/codemie/agent/`. + +## 9. References + +- `src/agents/plugins/pi/pi.plugin.ts` +- `src/agents/plugins/pi/pi.setup.ts` +- `src/agents/core/BaseAgentAdapter.ts` — `additionalInstallation()` hook +- `src/cli/commands/install.ts` — install command flow +- `src/utils/exec.ts` — `exec()` utility +- `src/utils/errors.ts` — `AgentInstallationError` +- Pi README: `pi install ` semantics diff --git a/package-lock.json b/package-lock.json index 417d6b8e9..222f4182f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "zod": "^4.1.12" }, "bin": { - "code": "bin/codemie.js", "codemie": "bin/codemie.js", "codemie-claude": "bin/codemie-claude.js", "codemie-claude-acp": "bin/codemie-claude-acp.js", @@ -56,6 +55,7 @@ "codemie-kimi-acp": "bin/codemie-kimi-acp.js", "codemie-mcp-proxy": "bin/codemie-mcp-proxy.js", "codemie-opencode": "bin/codemie-opencode.js", + "codemie-pi": "bin/codemie-pi.js", "proxy-daemon": "bin/proxy-daemon.js" }, "devDependencies": { diff --git a/package.json b/package.json index 4b9fcbb1c..535296372 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "codemie-claude-acp": "./bin/codemie-claude-acp.js", "codemie-gemini": "./bin/codemie-gemini.js", "codemie-opencode": "./bin/codemie-opencode.js", + "codemie-pi": "./bin/codemie-pi.js", "codemie-codex": "./bin/codemie-codex.js", "codemie-kimi": "./bin/codemie-kimi.js", "codemie-kimi-acp": "./bin/codemie-kimi-acp.js", diff --git a/src/agents/__tests__/registry.test.ts b/src/agents/__tests__/registry.test.ts index 1bcf585dd..4024e9b2b 100644 --- a/src/agents/__tests__/registry.test.ts +++ b/src/agents/__tests__/registry.test.ts @@ -22,6 +22,7 @@ describe('AgentRegistry', () => { 'gemini', 'opencode', 'codex', + 'pi', 'kimi', 'kimi-acp', 'copilot-cli', // analytics-only: read for the report, never managed by CodeMie diff --git a/src/agents/core/AgentCLI.ts b/src/agents/core/AgentCLI.ts index f478453ff..bbd50c56e 100644 --- a/src/agents/core/AgentCLI.ts +++ b/src/agents/core/AgentCLI.ts @@ -18,6 +18,7 @@ import {ClaudeAcpPluginMetadata} from "../plugins/claude/claude-acp.plugin.js"; import { CodexPluginMetadata } from '../plugins/codex/codex.plugin.js'; import { KimiPluginMetadata } from '../plugins/kimi/kimi.plugin.js'; import { KimiAcpPluginMetadata } from '../plugins/kimi/kimi-acp.plugin.js'; +import { PiPluginMetadata } from '../plugins/pi/pi.plugin.js'; import { createAssistantsSetupCommand } from '../../cli/commands/assistants/setup/index.js'; import { createSkillsSetupCommand } from '../../cli/commands/skills/setup/index.js'; import type { TargetAgent } from '../../cli/commands/shared/agent-targets.js'; @@ -578,6 +579,7 @@ export class AgentCLI { 'codex': CodexPluginMetadata, 'kimi': KimiPluginMetadata, 'kimi-acp': KimiAcpPluginMetadata, + 'pi': PiPluginMetadata, }; return metadataMap[this.adapter.name]; } diff --git a/src/agents/plugins/pi/index.ts b/src/agents/plugins/pi/index.ts new file mode 100644 index 000000000..168ec4fff --- /dev/null +++ b/src/agents/plugins/pi/index.ts @@ -0,0 +1 @@ +export { PiPlugin, PiPluginMetadata } from './pi.plugin.js'; diff --git a/src/agents/plugins/pi/pi.models.ts b/src/agents/plugins/pi/pi.models.ts new file mode 100644 index 000000000..295805ac1 --- /dev/null +++ b/src/agents/plugins/pi/pi.models.ts @@ -0,0 +1,258 @@ +import { mkdir, writeFile } from 'fs/promises'; +import type { LlmModel } from '../../../providers/plugins/sso/sso.http-client.js'; +import { fetchCodeMieLlmModels } from '../../../providers/plugins/sso/sso.http-client.js'; +import { CodeMieSSO } from '../../../providers/plugins/sso/sso.auth.js'; +import { logger } from '../../../utils/logger.js'; +import { getPiAgentDir, getPiModelsPath } from './pi.paths.js'; + +export interface PiModelClassification { + provider: 'codemie-proxy' | 'codemie-anthropic'; + api?: 'openai-responses'; +} + +const RESPONSES_API_PATTERNS: RegExp[] = [ + /^gpt-5-2-/, + /^gpt-5\.2-/, + /^gpt-5-1-codex/, + /^gpt-5\.1-codex/, + /^gpt-5-3-codex/, + /^gpt-5\.3-codex/, + /^gpt-5\.4-/, + /^gpt-5-4-/, + /^gpt-5\.5-/, + /^gpt-5-5-/, + /^gpt-5\.6-/, + /^gpt-5-6-/, +]; + +export function classifyPiModel(modelId: string): PiModelClassification { + if (modelId.startsWith('claude')) { + return { provider: 'codemie-anthropic' }; + } + if (RESPONSES_API_PATTERNS.some(pattern => pattern.test(modelId))) { + return { provider: 'codemie-proxy', api: 'openai-responses' }; + } + return { provider: 'codemie-proxy' }; +} + +export interface PiModelEntry { + id: string; + name: string; + api?: 'openai-responses'; + reasoning?: boolean; + thinkingLevelMap?: Record; + input: ('text' | 'image')[]; + contextWindow: number; + maxTokens: number; + compat?: Record; +} + +function detectLimits(id: string): { contextWindow: number; maxTokens: number } { + if (id.startsWith('claude')) return { contextWindow: 200000, maxTokens: 64000 }; + if (id.startsWith('gemini')) return { contextWindow: 1048576, maxTokens: 65536 }; + if (id.startsWith('gpt-4.1')) return { contextWindow: 1048576, maxTokens: 32768 }; + if (/^gpt-5\.5-/.test(id) || /^gpt-5-5-/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 }; + if (/^gpt-5\.6-/.test(id) || /^gpt-5-6-/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 }; + if (id.startsWith('gpt-5')) return { contextWindow: 400000, maxTokens: 128000 }; + if (/^o[134]-/.test(id) || id === 'o1') return { contextWindow: 200000, maxTokens: 100000 }; + if (id.startsWith('qwen') || id.startsWith('moonshotai') || id.startsWith('kimi')) { + return { contextWindow: 262144, maxTokens: 131072 }; + } + if (id.startsWith('deepseek')) return { contextWindow: 65536, maxTokens: 65536 }; + return { contextWindow: 128000, maxTokens: 4096 }; +} + +function defaultThinkingLevelMap(): Record { + return { + off: null, + minimal: 'minimal', + low: 'low', + medium: 'medium', + high: 'high', + xhigh: 'high', + max: 'high', + }; +} + +function isReasoningModel(id: string): boolean { + return ( + id.startsWith('claude') || + id.startsWith('gemini') || + id.startsWith('gpt-5') || + /^o[134]-/.test(id) || + id === 'o1' || + id.startsWith('deepseek') || + id.startsWith('moonshotai') || + id.startsWith('kimi') + ); +} + +export function convertLlmModelToPiEntry(model: LlmModel): PiModelEntry { + const id = model.deployment_name || model.base_name || model.label; + const limits = detectLimits(id); + + const entry: PiModelEntry = { + id, + name: model.label || id, + input: model.multimodal ? ['text', 'image'] : ['text'], + contextWindow: limits.contextWindow, + maxTokens: limits.maxTokens, + }; + + const classification = classifyPiModel(id); + if (classification.api) { + entry.api = classification.api; + } + + if (isReasoningModel(id)) { + entry.reasoning = true; + entry.thinkingLevelMap = defaultThinkingLevelMap(); + } + + if ( + id.startsWith('claude-sonnet-4-6') || + id.startsWith('claude-sonnet-5') || + /^claude-opus-4-[6-8]/.test(id) || + id.startsWith('claude-opus-5') + ) { + entry.compat = { forceAdaptiveThinking: true }; + } + + return entry; +} + +interface PiModelsConfig { + providers: Record; + models: PiModelEntry[]; + }>; +} + +async function fetchCodeMieModels(env: NodeJS.ProcessEnv): Promise { + const jwtToken = env.CODEMIE_JWT_TOKEN; + const baseUrl = env.CODEMIE_BASE_URL; + + if (jwtToken && baseUrl) { + logger.debug('[pi-models] Fetching CodeMie model list via JWT auth'); + return fetchCodeMieLlmModels(baseUrl, jwtToken); + } + + const codeMieUrl = env.CODEMIE_URL; + if (codeMieUrl) { + const sso = new CodeMieSSO(); + const credentials = await sso.getStoredCredentials(codeMieUrl); + if (!credentials) { + throw new Error(`SSO credentials not found for ${codeMieUrl}. Run: codemie profile login --url ${codeMieUrl}`); + } + logger.debug('[pi-models] Fetching CodeMie model list via SSO auth'); + return fetchCodeMieLlmModels(credentials.apiUrl, credentials.cookies); + } + + throw new Error('No CodeMie authentication available. Run codemie setup or set CODEMIE_JWT_TOKEN.'); +} + +function buildModelsConfig( + entries: PiModelEntry[], + baseUrl: string, + apiKey: string, +): PiModelsConfig { + const proxyModels: PiModelEntry[] = []; + const anthropicModels: PiModelEntry[] = []; + + for (const entry of entries) { + const classification = classifyPiModel(entry.id); + if (classification.provider === 'codemie-anthropic') { + anthropicModels.push(entry); + } else { + proxyModels.push(entry); + } + } + + const providers: PiModelsConfig['providers'] = {}; + + if (proxyModels.length > 0) { + providers['codemie-proxy'] = { + baseUrl: `${baseUrl.replace(/\/$/, '')}/v1`, + api: 'openai-completions', + apiKey, + compat: { + supportsReasoningEffort: true, + thinkingFormat: 'reasoning_effort', + }, + models: proxyModels, + }; + } + + if (anthropicModels.length > 0) { + providers['codemie-anthropic'] = { + baseUrl: baseUrl.replace(/\/$/, ''), + api: 'anthropic-messages', + apiKey, + authHeader: true, + compat: { + supportsReasoningEffort: true, + thinkingFormat: 'reasoning_effort', + }, + models: anthropicModels, + }; + } + + return { providers }; +} + +function createSyntheticLlmModel(modelId: string): LlmModel { + return { + base_name: modelId, + deployment_name: modelId, + label: modelId, + enabled: true, + multimodal: false, + features: {}, + }; +} + +function buildStaticFallbackModel(modelId: string, baseUrl: string, apiKey: string): PiModelsConfig { + const entry = convertLlmModelToPiEntry(createSyntheticLlmModel(modelId)); + return buildModelsConfig([entry], baseUrl, apiKey); +} + +export async function fetchAndBuildPiModels( + env: NodeJS.ProcessEnv, + cwd: string = process.cwd(), +): Promise { + const agentDir = getPiAgentDir(cwd); + await mkdir(agentDir, { recursive: true }); + + const baseUrl = env.CODEMIE_BASE_URL || ''; + const apiKey = env.CODEMIE_API_KEY || 'proxy-handled'; + + let entries: PiModelEntry[] = []; + try { + const rawModels = await fetchCodeMieModels(env); + entries = rawModels + .filter(model => model.enabled) + .map(convertLlmModelToPiEntry); + logger.debug(`[pi-models] Loaded ${entries.length} models from CodeMie API`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[pi-models] Failed to fetch live models, falling back to static model: ${message}`); + const configuredModel = env.CODEMIE_MODEL; + if (!configuredModel) { + throw new Error('No CodeMie model configured and live model fetch failed.'); + } + const fallback = buildStaticFallbackModel(configuredModel, baseUrl, apiKey); + await writeFile(getPiModelsPath(cwd), JSON.stringify(fallback, null, 2), 'utf-8'); + return; + } + + if (entries.length === 0) { + throw new Error('CodeMie returned no enabled models for codemie-pi.'); + } + + const config = buildModelsConfig(entries, baseUrl, apiKey); + await writeFile(getPiModelsPath(cwd), JSON.stringify(config, null, 2), 'utf-8'); +} diff --git a/src/agents/plugins/pi/pi.packages.ts b/src/agents/plugins/pi/pi.packages.ts new file mode 100644 index 000000000..43cb7b793 --- /dev/null +++ b/src/agents/plugins/pi/pi.packages.ts @@ -0,0 +1,58 @@ +import { exec, type ExecResult } from '@/utils/exec.js'; +import { logger } from '@/utils/logger.js'; +import { AgentInstallationError } from '@/utils/errors.js'; + +export const REQUIRED_PI_PACKAGES: readonly string[] = [ + 'git:github.com/obra/superpowers', + 'npm:pi-subagents', + 'npm:pi-mcp-adapter', +]; + +export interface InstallPiPackagesOptions { + /** Pi CLI command name or path (default: 'pi') */ + cliCommand?: string | null; + /** Working directory for the install commands (default: process.cwd()) */ + cwd?: string; + /** Per-package timeout in milliseconds (default: 300000) */ + timeout?: number; +} + +const DEFAULT_TIMEOUT_MS = 300_000; + +export async function installRequiredPiPackages( + options: InstallPiPackagesOptions = {}, +): Promise { + const cliCommand = options.cliCommand || 'pi'; + const cwd = options.cwd ?? process.cwd(); + const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS; + + logger.info(`[pi] Installing required Pi packages using ${cliCommand}`); + + for (const pkg of REQUIRED_PI_PACKAGES) { + logger.info(`[pi] Installing package: ${pkg}`); + + let result: ExecResult; + try { + result = await exec(cliCommand, ['install', pkg], { + cwd, + timeout, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + throw new AgentInstallationError( + 'pi', + `Failed to install Pi package "${pkg}": ${message}`, + ); + } + + if (result.code !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join('\n'); + throw new AgentInstallationError( + 'pi', + `Failed to install Pi package "${pkg}": ${output}`, + ); + } + + logger.success(`[pi] Installed package: ${pkg}`); + } +} diff --git a/src/agents/plugins/pi/pi.paths.ts b/src/agents/plugins/pi/pi.paths.ts new file mode 100644 index 000000000..f7206c76a --- /dev/null +++ b/src/agents/plugins/pi/pi.paths.ts @@ -0,0 +1,14 @@ +import { join } from 'path'; +import { homedir } from 'os'; + +export function getUserPiAgentDir(): string { + return join(homedir(), '.pi', 'agent'); +} + +export function getPiAgentDir(cwd: string = process.cwd()): string { + return join(cwd, '.pi', 'codemie', 'agent'); +} + +export function getPiModelsPath(cwd: string = process.cwd()): string { + return join(getPiAgentDir(cwd), 'models.json'); +} diff --git a/src/agents/plugins/pi/pi.plugin.ts b/src/agents/plugins/pi/pi.plugin.ts new file mode 100644 index 000000000..a433701f0 --- /dev/null +++ b/src/agents/plugins/pi/pi.plugin.ts @@ -0,0 +1,77 @@ +import type { AgentMetadata, AgentConfig } from '../../core/types.js'; +import { BaseAgentAdapter } from '../../core/BaseAgentAdapter.js'; +import { logger } from '@/utils/logger.js'; +import { preparePiAgentDir } from './pi.setup.js'; +import { fetchAndBuildPiModels, classifyPiModel } from './pi.models.js'; +import { getPiAgentDir } from './pi.paths.js'; +import { installRequiredPiPackages } from './pi.packages.js'; + +export const PiPluginMetadata: AgentMetadata = { + name: 'pi', + displayName: 'Pi', + description: 'Pi - open-source coding agent harness', + npmPackage: '@earendil-works/pi-coding-agent', + cliCommand: process.env.CODEMIE_PI_BIN || 'pi', + + sessionAnalyticsReport: false, + + dataPaths: { + home: '.pi', + }, + + envMapping: { + baseUrl: [], + apiKey: [], + model: [], + }, + + supportedProviders: ['ai-run-sso', 'bearer-auth', 'litellm'], + + ssoConfig: { + enabled: true, + clientType: 'codemie-pi', + }, + + lifecycle: { + async beforeRun(env: NodeJS.ProcessEnv, _config: AgentConfig) { + const cwd = process.cwd(); + await preparePiAgentDir(cwd); + await fetchAndBuildPiModels(env, cwd); + env.PI_CODING_AGENT_DIR = getPiAgentDir(cwd); + logger.debug('[pi] Configured PI_CODING_AGENT_DIR', { path: env.PI_CODING_AGENT_DIR }); + return env; + }, + + enrichArgs(args: string[], _config: AgentConfig): string[] { + const model = process.env.CODEMIE_MODEL; + if (!model) { + throw new Error('No model configured for codemie-pi. Run codemie setup to select a model.'); + } + + const classification = classifyPiModel(model); + const providerId = classification.provider; + + let result = args; + + const taskIndex = result.indexOf('--task'); + if (taskIndex !== -1 && taskIndex < result.length - 1) { + const taskValue = result[taskIndex + 1]; + result = [...result.slice(0, taskIndex), ...result.slice(taskIndex + 2), taskValue]; + } + + return ['--provider', providerId, '--model', model, ...result]; + }, + }, +}; + +export class PiPlugin extends BaseAgentAdapter { + constructor() { + super(PiPluginMetadata); + } + + async additionalInstallation( + _options?: import('../../core/types.js').AgentInstallationOptions, + ): Promise { + await installRequiredPiPackages({ cliCommand: this.metadata.cliCommand }); + } +} diff --git a/src/agents/plugins/pi/pi.setup.ts b/src/agents/plugins/pi/pi.setup.ts new file mode 100644 index 000000000..226bd8ac4 --- /dev/null +++ b/src/agents/plugins/pi/pi.setup.ts @@ -0,0 +1,35 @@ +import { existsSync } from 'fs'; +import { cp, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { logger } from '@/utils/logger.js'; +import { getPiAgentDir, getUserPiAgentDir } from './pi.paths.js'; + +export async function preparePiAgentDir(cwd: string = process.cwd()): Promise { + const sourceDir = getUserPiAgentDir(); + const destDir = getPiAgentDir(cwd); + + if (existsSync(destDir)) { + logger.debug(`[pi-setup] CodeMie Pi agent dir already exists, skipping copy: ${destDir}`); + return; + } + + if (!existsSync(sourceDir)) { + logger.warn(`[pi-setup] User Pi agent dir not found, starting fresh: ${sourceDir}`); + await mkdir(destDir, { recursive: true }); + return; + } + + logger.debug(`[pi-setup] Copying ${sourceDir} → ${destDir}`); + try { + const excludedSessionsDir = join(sourceDir, 'sessions'); + await cp(sourceDir, destDir, { + recursive: true, + force: true, + filter: (source) => source !== excludedSessionsDir, + }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`[pi-setup] Failed to copy user Pi agent dir, starting fresh: ${message}`); + await mkdir(destDir, { recursive: true }); + } +} diff --git a/src/agents/registry.ts b/src/agents/registry.ts index 83599ec0d..369f72118 100644 --- a/src/agents/registry.ts +++ b/src/agents/registry.ts @@ -4,6 +4,7 @@ import { CodeMieCodePlugin } from './plugins/codemie-code.plugin.js'; import { GeminiPlugin } from './plugins/gemini/gemini.plugin.js'; import { OpenCodePlugin } from './plugins/opencode/index.js'; import { CodexPlugin } from './plugins/codex/index.js'; +import { PiPlugin } from './plugins/pi/index.js'; import { KimiPlugin } from './plugins/kimi/kimi.plugin.js'; import { KimiAcpPlugin } from './plugins/kimi/kimi-acp.plugin.js'; import { CopilotCliPlugin } from './plugins/copilot-cli/index.js'; @@ -36,6 +37,7 @@ export class AgentRegistry { AgentRegistry.registerPlugin(new GeminiPlugin()); AgentRegistry.registerPlugin(new OpenCodePlugin()); AgentRegistry.registerPlugin(new CodexPlugin()); + AgentRegistry.registerPlugin(new PiPlugin()); AgentRegistry.registerPlugin(new KimiPlugin()); AgentRegistry.registerPlugin(new KimiAcpPlugin()); // Analytics-only: CodeMie never installs, launches, or manages Copilot — this