diff --git a/PAI-Install/cli/quick-install.ts b/PAI-Install/cli/quick-install.ts index 87fafbd0..7eda99a3 100644 --- a/PAI-Install/cli/quick-install.ts +++ b/PAI-Install/cli/quick-install.ts @@ -16,7 +16,9 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { InstallState } from "../engine/types"; import { createFreshState } from "../engine/state"; -import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, ZEN_FREE_MODELS, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh"; +import { stepPrerequisites, stepBuildOpenCode, stepProviderConfig, stepIdentity, stepVoice, stepInstallPAI } from "../engine/steps-fresh"; +import { PROVIDER_MODELS } from "../engine/provider-models"; +import type { ProviderName } from "../engine/provider-models"; import { stepDetectMigration, stepCreateBackup, stepMigrate, stepBinaryUpdate, stepMigrationDone } from "../engine/steps-migrate"; import { stepDetectUpdate, stepApplyUpdate, stepUpdateDone } from "../engine/steps-update"; @@ -153,25 +155,17 @@ async function runFreshInstall(): Promise { // Step 4: Provider Config onProgress(75, "Configuring provider..."); + const validProviders = Object.keys(PROVIDER_MODELS) as ProviderName[]; const preset = values.preset || "zen"; - - // Type guard for valid presets - const validPresets = ["zen", "quick", "standard", "advanced", "anthropic", "openrouter", "openai"]; - const validatedPreset = validPresets.includes(preset) ? preset : "zen"; - - const models = validatedPreset === "zen" ? ZEN_FREE_MODELS : { - quick: "claude-haiku-3.5", - standard: "claude-sonnet-4.6", - advanced: "claude-opus-4.6", - }; - + const provider: ProviderName = validProviders.includes(preset as ProviderName) + ? (preset as ProviderName) + : "zen"; + await stepProviderConfig( state, { - provider: validatedPreset, + provider, apiKey: values["api-key"] || "", - modelTier: "standard", - models, }, onProgress ); diff --git a/PAI-Install/engine/provider-models.ts b/PAI-Install/engine/provider-models.ts new file mode 100644 index 00000000..ef3b1bc9 --- /dev/null +++ b/PAI-Install/engine/provider-models.ts @@ -0,0 +1,66 @@ +/** + * PAI-OpenCode Installer — Provider Model Maps + * + * Defines quick/standard/advanced model strings for each supported provider. + * The installer substitutes these into the opencode.json template at install time. + * + * To add a new provider: add an entry below and handle it in steps-fresh.ts. + */ + +export type ProviderName = "anthropic" | "zen" | "openrouter" | "openai"; + +export interface ModelTierMap { + quick: string; + standard: string; + advanced: string; +} + +/** + * Model strings per provider, formatted as "provider/model-name" ready for + * insertion into opencode.json agent entries. + */ +export const PROVIDER_MODELS: Record = { + anthropic: { + quick: "anthropic/claude-haiku-4-5", + standard: "anthropic/claude-sonnet-4-5", + advanced: "anthropic/claude-opus-4-6", + }, + zen: { + // OpenCode Zen — cost-optimised tiers (IDs verified against opencode.ai/docs/zen/) + quick: "zen/minimax-m2.5-free", // FREE + standard: "zen/gpt-5.1-codex-mini", // $0.25/M in+out + advanced: "zen/claude-3-5-haiku", // $0.80/M — catalog ID for Claude Haiku 3.5 + }, + openrouter: { + quick: "openrouter/google/gemini-flash-1.5", + standard: "openrouter/anthropic/claude-4.5-sonnet", + advanced: "openrouter/anthropic/claude-opus-4-6", + }, + openai: { + quick: "openai/gpt-4o-mini", + standard: "openai/gpt-4o", + advanced: "openai/gpt-5", + }, +}; + +/** + * Human-readable labels shown in the installer wizard. + */ +export const PROVIDER_LABELS: Record = { + anthropic: { + label: "Anthropic (Claude)", + description: "Premium quality — requires Anthropic API key", + }, + zen: { + label: "OpenCode Zen (recommended)", + description: "Free tier available — 60× cost optimisation vs direct Anthropic", + }, + openrouter: { + label: "OpenRouter", + description: "Multi-provider flexibility — one API key for many models", + }, + openai: { + label: "OpenAI", + description: "GPT-4o and GPT-5 — requires OpenAI API key", + }, +}; diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index d9536598..dcb28b42 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -1,14 +1,16 @@ #!/usr/bin/env bun /** * PAI-OpenCode Installer — Fresh Install Steps - * + * * 7-step fresh installation flow with OpenCode-Zen as default provider. */ -import type { InstallState } from "./types"; -import { buildOpenCodeBinary } from "./build-opencode"; -import type { BuildResult } from "./build-opencode"; -import { existsSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs"; +import type { InstallState } from "./types.ts"; +import { buildOpenCodeBinary } from "./build-opencode.ts"; +import type { BuildResult } from "./build-opencode.ts"; +import { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; +import type { ProviderName } from "./provider-models.ts"; +import { existsSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, unlinkSync, lstatSync, realpathSync } from "node:fs"; import { join, resolve } from "node:path"; import { homedir } from "node:os"; @@ -110,39 +112,19 @@ export async function stepBuildOpenCode( // ═══════════════════════════════════════════════════════════ export interface ProviderConfig { - provider: "zen" | "anthropic" | "openrouter" | "openai"; + provider: ProviderName; apiKey: string; - modelTier: "quick" | "standard" | "advanced"; - models: { - quick: string; - standard: string; - advanced: string; - }; } -export const ZEN_FREE_MODELS = { - quick: "minimax-m2.5-free", // FREE - standard: "gpt-5.1-codex-mini", // $0.25/M - advanced: "claude-haiku-3.5", // $0.80/M -}; - -export const ANTHROPIC_MODELS = { - quick: "claude-haiku-3.5", - standard: "claude-sonnet-4.6", - advanced: "claude-opus-4.6", -}; - -export const OPENROUTER_MODELS = { - quick: "google/gemini-flash-1.5", - standard: "anthropic/claude-3.5-sonnet", - advanced: "anthropic/claude-3-opus", -}; - -export const OPENAI_MODELS = { - quick: "gpt-4o-mini", - standard: "gpt-4o", - advanced: "gpt-5", -}; +// Re-export for consumers that imported these from this module +export { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; + +// Legacy aliases kept for CLI quick-install.ts compatibility. +// Spread into new objects so mutations by consumers cannot corrupt PROVIDER_MODELS. +export const ZEN_FREE_MODELS = { ...PROVIDER_MODELS.zen }; +export const ANTHROPIC_MODELS = { ...PROVIDER_MODELS.anthropic }; +export const OPENROUTER_MODELS = { ...PROVIDER_MODELS.openrouter }; +export const OPENAI_MODELS = { ...PROVIDER_MODELS.openai }; export async function stepProviderConfig( state: InstallState, @@ -150,14 +132,12 @@ export async function stepProviderConfig( onProgress: (percent: number, message: string) => void ): Promise { onProgress(75, "Configuring AI provider..."); - - // Save provider settings + + // Save provider + key; model strings are resolved from PROVIDER_MODELS at write time state.collected.provider = config.provider; state.collected.apiKey = config.apiKey; - state.collected.modelTier = config.modelTier; - state.collected.models = config.models; - - // API key will be saved to .env by config generation step + + // API key will be saved to .env by the install step } // ═══════════════════════════════════════════════════════════ @@ -245,8 +225,7 @@ export async function stepInstallPAI( default: state.collected.provider || "zen", [state.collected.provider || "zen"]: { // apiKey is stored in .env, not here - modelTier: state.collected.modelTier || "standard", - models: state.collected.models || [], + // model strings are written to opencode.json via PROVIDER_MODELS }, }, }; @@ -280,35 +259,75 @@ ${providerEnvVar}=${state.collected.apiKey || ""} chmodSync(envPath, 0o600); onProgress(95, "Created .env with secure permissions..."); - // Generate opencode.json - const modelProvider = state.collected.provider || "anthropic"; - const modelTier = state.collected.modelTier || "standard"; - const modelMap = state.collected.models; - const modelName = modelMap && typeof modelMap === 'object' ? - (modelMap[modelTier] || modelMap['standard']) : - "claude-sonnet-4.6"; - const modelString = `${modelProvider}/${modelName}`; - + // Generate opencode.json — full agent-tier structure matching the repo template + const provider = (state.collected.provider || "zen") as ProviderName; + const tiers = PROVIDER_MODELS[provider] ?? PROVIDER_MODELS.zen; + + /** + * Build a standard agent entry with quick/standard/advanced tiers. + * The top-level `model` mirrors the standard tier so opencode has a + * sensible default when no tier is specified by a caller. + */ + function agentEntry(standard: string, quick: string, advanced: string) { + return { + model: standard, + model_tiers: { + quick: { model: quick }, + standard: { model: standard }, + advanced: { model: advanced }, + }, + }; + } + const opencode = { - ai: { - name: state.collected.aiName || "PAI", - model: modelString, - }, - voice: { - enabled: state.collected.voiceEnabled || false, - provider: state.collected.voiceProvider || "none", - voiceId: state.collected.voiceId || "default", + $schema: "https://opencode.ai/config.json", + theme: "dark", + model: tiers.standard, + snapshot: true, + username: state.collected.principalName || "User", + permission: { + "*": "allow", + websearch: "allow", + codesearch: "allow", + webfetch: "allow", + doom_loop: "ask", + external_directory: "ask", }, - memory: { - enabled: true, + mode: { + build: { + prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.", + }, + plan: { + prompt: "You are a Personal AI assistant powered by PAI-OpenCode infrastructure.", + }, }, - skills: { - autoLoad: true, + agent: { + // Algorithm agent always uses the highest-quality model for orchestration + Algorithm: { model: tiers.advanced }, + Architect: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Engineer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + general: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // explore is always the quick model — speed matters more than quality + explore: { model: tiers.quick }, + Intern: agentEntry(tiers.quick, tiers.quick, tiers.standard), + Writer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + DeepResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // Specialised researchers keep their primary model but fall back to provider tiers + GeminiResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + GrokResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + PerplexityResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + CodexResearcher: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + // QATester has no tier override — single model is intentional + QATester: { model: tiers.standard }, + Pentester: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Designer: agentEntry(tiers.standard, tiers.quick, tiers.advanced), + Artist: agentEntry(tiers.standard, tiers.quick, tiers.advanced), }, }; + writeFileSync( join(localOpencodeDir, "opencode.json"), - JSON.stringify(opencode, null, 2) + JSON.stringify(opencode, null, 2), ); onProgress(97, "Generated opencode.json..."); @@ -319,20 +338,21 @@ ${providerEnvVar}=${state.collected.apiKey || ""} // Check if ~/.opencode exists if (existsSync(globalOpencodeLink)) { const stats = lstatSync(globalOpencodeLink); - + if (stats.isSymbolicLink()) { // It's already a symlink - check if it points to our location let currentTarget: string; try { currentTarget = realpathSync(globalOpencodeLink); - } catch (err) { - // Symlink target doesn't exist (broken symlink) - // Remove and recreate + } catch { + // Symlink target doesn't exist (broken symlink) — remove and recreate unlinkSync(globalOpencodeLink); symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); - continue; + // Assign so the subsequent check sees a defined, correct value + // and doesn't attempt a redundant remove+recreate + currentTarget = localOpencodeDir; } - + if (currentTarget !== localOpencodeDir) { // Remove old symlink and create new one unlinkSync(globalOpencodeLink); @@ -388,25 +408,17 @@ export async function runFreshInstall( // Step 3: Provider Configuration (API Keys) await emit({ event: "step_start", step: "api-keys" }); // Collect provider config via interactive callbacks - const providerChoices = [ - { label: "OpenCode Zen (FREE tier available)", value: "zen", description: "Recommended - 60x cost optimization" }, - { label: "Anthropic (Claude)", value: "anthropic", description: "Premium quality, higher cost" }, - { label: "OpenRouter", value: "openrouter", description: "Multi-provider flexibility" }, - ]; - const provider = await requestChoice("provider", "Choose your AI provider:", providerChoices); + const providerChoices = Object.entries(PROVIDER_LABELS).map(([value, { label, description }]) => ({ + label, + value, + description, + })); + const provider = (await requestChoice("provider", "Choose your AI provider:", providerChoices)) as ProviderName || "zen"; const apiKey = await requestInput("api-key", `Enter your ${provider} API key:`, "key", "sk-..."); - - // Select models based on provider - const models = provider === "zen" ? ZEN_FREE_MODELS : - provider === "anthropic" ? ANTHROPIC_MODELS : - provider === "openrouter" ? OPENROUTER_MODELS : - provider === "openai" ? OPENAI_MODELS : ZEN_FREE_MODELS; - + await stepProviderConfig(state, { - provider: provider || "zen", + provider, apiKey: apiKey || "", - modelTier: "standard", - models, }, (percent, message) => { emit({ event: "progress", step: "api-keys", percent, detail: message }); }); diff --git a/docs/epic/OPTIMIZED-PR-PLAN.md b/docs/epic/OPTIMIZED-PR-PLAN.md index 291bb94c..3c53b1f7 100644 --- a/docs/epic/OPTIMIZED-PR-PLAN.md +++ b/docs/epic/OPTIMIZED-PR-PLAN.md @@ -35,7 +35,8 @@ tags: [architecture, migration, v3.0, PR-strategy, native-transformation] | **WP-N5** | Plan Update | #54 | ✅ **Merged** | Sync all planning docs to reflect N1-N4 complete | | **WP-N6** | System Self-Awareness | #55 | ✅ **Merged** | OpenCodeSystem skill, 4 architecture reference docs, ADR-017 | | **WP-N7** | roborev + Biome CI | #56 | ✅ **Merged** | roborev plugin handler, CodeReview skill, GitHub Actions CI, ADR-018 | -| **WP-N8** | Obsidian Formatting Guidelines | — | 🔄 **In Progress** | Formatting guidelines, agent capability matrix (split from WP-N7) | +| **WP-N8** | Obsidian Formatting Guidelines | #57 | ✅ **Merged** | Formatting guidelines, agent capability matrix (split from WP-N7) | +| **WP-N9** | Installer opencode.json Fix | — | 🔄 **In Progress** | provider-models.ts, full agent-tier generation, principalName in username | > [!NOTE] > **2026-03-08 Live Audit:** WP-C scope significantly reduced after comparing repo against v4.0.3. @@ -218,11 +219,11 @@ Current state (dev branch): | Metric | 2026-03-08 | 2026-03-11 | **Current (2026-03-12)** | |--------|------------|------------|--------------------------| | Port WPs done | 8 ✅ | 9 ✅ (WP-E) | **9 ✅** | -| Native WPs done | 0 | 4 ✅ (N1–N4) | **7 ✅ (N1–N7), N8 in progress** | -| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N8 — open, in progress)** | -| Remaining native work | Not planned | WP-N6 in progress | **WP-N8 in progress (Obsidian formatting)** | +| Native WPs done | 0 | 4 ✅ (N1–N4) | **8 ✅ (N1–N8), N9 in progress** | +| Open PRs | 2 (C, D) | 1 (#55) | **1 (WP-N9 — open, in progress)** | +| Remaining native work | Not planned | WP-N6 in progress | **WP-N9 in progress (installer opencode.json fix)** | -**Status:** Port complete. Native transformation: WP-N1 through WP-N7 merged (PR #50–#56). WP-N8 in progress (Obsidian formatting guidelines + agent capability matrix). +**Status:** Port complete. Native transformation: WP-N1 through WP-N8 merged (PR #50–#57). WP-N9 in progress (installer opencode.json full agent-tier generation). **Native transformation plan:** `docs/epic/EPIC-v3.0-OpenCode-Native.md` **Full gap analysis:** `docs/epic/GAP-ANALYSIS-v3.0.md` diff --git a/docs/epic/TODO-v3.0.md b/docs/epic/TODO-v3.0.md index 9f743119..fd69b104 100644 --- a/docs/epic/TODO-v3.0.md +++ b/docs/epic/TODO-v3.0.md @@ -34,7 +34,8 @@ WP-N4 ████████████ 100% ✅ ← LSP + Fork Documentatio WP-N5 ████████████ 100% ✅ ← Plan Update complete, PR #54 WP-N6 ████████████ 100% ✅ ← System Self-Awareness, PR #55 merged WP-N7 ████████████ 100% ✅ ← roborev + Biome CI, PR #56 merged -WP-N8 ████████░░░░ 80% 🔄 ← Obsidian formatting + agent matrix, PR open +WP-N8 ████████████ 100% ✅ ← Obsidian formatting + agent matrix, PR #57 merged +WP-N9 ██████████░░ 90% 🔄 ← Installer opencode.json fix, PR #58 open ``` > **The port is done. The native transformation starts with WP-N1.**