Browse Source

Getting ready for initial release

Alvin Unreal 2 months ago
parent
commit
fa9110dbd0

+ 313 - 0
CODEREF.md

@@ -0,0 +1,313 @@
+# Code Reference
+
+Quick reference for understanding and extending oh-my-opencode-lite.
+
+## Directory Structure
+
+```
+src/
+├── agents/           # Agent definitions
+├── cli/              # Installer CLI
+├── config/           # Configuration loading & constants
+├── features/         # Background task manager
+├── tools/            # Tool definitions (background_task, etc.)
+├── utils/            # Shared utilities
+└── index.ts          # Plugin entry point
+```
+
+## Core Concepts
+
+### 1. Plugin Entry Point
+
+**File:** `src/index.ts`
+
+```typescript
+const OhMyOpenCodeLite: Plugin = async (ctx) => {
+  return {
+    name: "oh-my-opencode-lite",
+    agent: agents,      // Agent configurations
+    tool: tools,        // Custom tools
+    config: (cfg) => {} // Modify OpenCode config
+  };
+};
+```
+
+### 2. Agents
+
+**Location:** `src/agents/`
+
+Each agent is a factory function that returns an `AgentDefinition`:
+
+```typescript
+// src/agents/example.ts
+import type { AgentDefinition } from "./orchestrator";
+
+export function createExampleAgent(model: string): AgentDefinition {
+  return {
+    name: "example",
+    description: "What this agent does",
+    config: {
+      model,
+      temperature: 0.1,
+      system: PROMPT,
+    },
+  };
+}
+```
+
+**To add a new agent:**
+
+1. Create `src/agents/my-agent.ts`
+2. Register in `src/agents/index.ts`:
+   ```typescript
+   import { createMyAgent } from "./my-agent";
+   
+   const SUBAGENT_FACTORIES = {
+     // ...existing
+     "my-agent": createMyAgent,
+   };
+   ```
+3. Add to `AgentName` type in `src/config/schema.ts`
+4. Add default model in `src/config/schema.ts` → `DEFAULT_MODELS`
+
+### 3. Configuration
+
+**Files:**
+- `src/config/schema.ts` - Zod schemas & types
+- `src/config/constants.ts` - Timeouts, intervals
+- `src/config/loader.ts` - Config file loading
+
+**Key Types:**
+
+```typescript
+// Agent override (user config)
+type AgentOverrideConfig = {
+  model?: string;
+  temperature?: number;
+  prompt?: string;
+  prompt_append?: string;
+  disable?: boolean;
+};
+
+// Plugin config structure
+type PluginConfig = {
+  agents?: Record<string, AgentOverrideConfig>;
+  disabled_agents?: string[];
+};
+```
+
+**Config file locations:**
+- User: `~/.config/opencode/oh-my-opencode-lite.json`
+- Project: `.opencode/oh-my-opencode-lite.json`
+
+### 4. Tools
+
+**Location:** `src/tools/`
+
+Tools use the `@opencode-ai/plugin` SDK:
+
+```typescript
+import { tool } from "@opencode-ai/plugin";
+
+const z = tool.schema;
+
+const my_tool = tool({
+  description: "What this tool does",
+  args: {
+    param1: z.string().describe("Description"),
+    param2: z.boolean().optional(),
+  },
+  async execute(args, context) {
+    // Implementation
+    return "Result string";
+  },
+});
+```
+
+### 5. Background Tasks
+
+**Files:**
+- `src/features/background-manager.ts` - Task lifecycle management
+- `src/tools/background.ts` - Tool definitions
+
+**Flow:**
+```
+background_task (async)
+    └── BackgroundTaskManager.launch()
+           └── Creates session, sends prompt
+           └── Polls for completion
+           
+background_task (sync)
+    └── executeSync()
+           └── Creates session
+           └── Polls until stable
+           └── Returns result directly
+```
+
+## Key Patterns
+
+### Override Application
+
+All agent overrides use the shared helper:
+
+```typescript
+// src/agents/index.ts
+function applyOverrides(agent: AgentDefinition, override: AgentOverrideConfig): void {
+  if (override.model) agent.config.model = override.model;
+  if (override.temperature !== undefined) agent.config.temperature = override.temperature;
+  if (override.prompt) agent.config.system = override.prompt;
+  if (override.prompt_append) {
+    agent.config.system = `${agent.config.system}\n\n${override.prompt_append}`;
+  }
+}
+```
+
+### Model Lookup Table
+
+Provider-specific models defined in one place:
+
+```typescript
+// src/cli/config-manager.ts
+const MODEL_MAPPINGS = {
+  antigravity: {
+    orchestrator: "google/claude-opus-4-5-thinking",
+    // ...
+  },
+  openai: { /* ... */ },
+  cerebras: { /* ... */ },
+};
+```
+
+### Constants
+
+All magic numbers centralized:
+
+```typescript
+// src/config/constants.ts
+export const POLL_INTERVAL_MS = 500;
+export const MAX_POLL_TIME_MS = 5 * 60 * 1000;
+export const DEFAULT_TIMEOUT_MS = 120_000;
+export const STABLE_POLLS_THRESHOLD = 3;
+```
+
+## Extending the Code
+
+### Add a New Agent
+
+```bash
+# 1. Create agent file
+touch src/agents/my-agent.ts
+```
+
+```typescript
+// src/agents/my-agent.ts
+import type { AgentDefinition } from "./orchestrator";
+
+export function createMyAgent(model: string): AgentDefinition {
+  return {
+    name: "my-agent",
+    description: "Short description for orchestrator",
+    config: {
+      model,
+      temperature: 0.5,
+      system: `Your prompt here...`,
+    },
+  };
+}
+```
+
+```typescript
+// src/agents/index.ts - add to SUBAGENT_FACTORIES
+"my-agent": createMyAgent,
+
+// src/config/schema.ts - add to AgentName
+export type AgentName = "orchestrator" | ... | "my-agent";
+
+// src/config/schema.ts - add to DEFAULT_MODELS
+export const DEFAULT_MODELS: Record<AgentName, string> = {
+  // ...
+  "my-agent": "google/gemini-3-flash",
+};
+```
+
+### Add a New Tool
+
+```typescript
+// src/tools/my-tool.ts
+import { tool } from "@opencode-ai/plugin";
+
+const z = tool.schema;
+
+export const my_tool = tool({
+  description: "What it does",
+  args: {
+    input: z.string(),
+  },
+  async execute(args) {
+    return `Processed: ${args.input}`;
+  },
+});
+```
+
+```typescript
+// src/tools/index.ts - export it
+export { my_tool } from "./my-tool";
+
+// src/index.ts - add to tool object
+tool: {
+  ...backgroundTools,
+  my_tool,
+},
+```
+
+### Add New Constants
+
+```typescript
+// src/config/constants.ts
+export const MY_NEW_TIMEOUT_MS = 30_000;
+
+// Use it
+import { MY_NEW_TIMEOUT_MS } from "../config";
+```
+
+## File Quick Reference
+
+| File | Purpose |
+|------|---------|
+| `src/index.ts` | Plugin entry, exports |
+| `src/agents/index.ts` | Agent factory, override logic |
+| `src/agents/orchestrator.ts` | Main orchestrator agent |
+| `src/config/schema.ts` | Types, Zod schemas, defaults |
+| `src/config/constants.ts` | Timeouts, intervals |
+| `src/config/loader.ts` | Config file loading |
+| `src/tools/background.ts` | Background task tools |
+| `src/features/background-manager.ts` | Task lifecycle |
+| `src/utils/polling.ts` | Polling utilities |
+| `src/cli/index.ts` | CLI entry point |
+| `src/cli/install.ts` | Installation logic |
+| `src/cli/config-manager.ts` | Config file management |
+
+## Type Imports
+
+```typescript
+// SDK types
+import type { Plugin, PluginInput, ToolDefinition } from "@opencode-ai/plugin";
+import type { AgentConfig as SDKAgentConfig } from "@opencode-ai/sdk";
+
+// Local types
+import type { AgentDefinition } from "./agents";
+import type { PluginConfig, AgentOverrideConfig, AgentName } from "./config";
+```
+
+## Build & Test
+
+```bash
+# Build
+bun run build
+
+# Type check
+bun run tsc --noEmit
+
+# Run CLI
+bun run src/cli/index.ts install
+```

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025
+
+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.

+ 388 - 80
README.md

@@ -1,16 +1,346 @@
-# oh-my-opencode-lite 🚀
+<div align="center">
 
 
-A lightweight, powerful agent orchestration plugin for **OpenCode**. It transforms your AI assistant into a manager capable of delegating complex tasks to specialized sub-agents, running searches in the background, and managing multi-step workflows with ease.
+# oh-my-opencode-slim
 
 
-## 🏗️ Architecture & Flow
+**A lightweight, powerful agent orchestration plugin for OpenCode**
+
+<img src="img/hero.png" alt="Agent Orchestration - Hub and Spoke Model" width="520">
+
+*Transform your AI assistant into a manager capable of delegating complex tasks to specialized sub-agents, running searches in the background, and managing multi-step workflows with ease.*
+
+</div>
+
+> Slimmed-down fork of [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) — focused on core agent orchestration without the extra bells and whistles.
+
+---
+
+## Installation
+
+### For Humans
+
+Run the interactive installer:
+
+```bash
+bunx oh-my-opencode-slim install
+```
+
+Or use non-interactive mode:
+
+```bash
+bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no
+```
+
+**Alternative: Ask any coding agent**
+
+Paste this into Claude Code, AmpCode, Cursor, or any coding agent:
+
+```
+Install and configure by following the instructions here:
+https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/refs/heads/master/README.md
+```
+
+### For LLM Agents
+
+<details>
+<summary>Instructions for LLM Agents (click to expand)</summary>
+
+If you're an LLM Agent helping set up oh-my-opencode-slim, follow these steps **exactly in order**. Ask questions **one at a time** and wait for the user's response before proceeding.
+
+---
+
+#### Step 1: Check OpenCode Installation
+
+First, verify OpenCode is installed:
+
+```bash
+if command -v opencode &> /dev/null; then
+    echo "OpenCode $(opencode --version) is installed"
+else
+    echo "OpenCode is not installed. Please install it first."
+    echo "Ref: https://opencode.ai/docs"
+fi
+```
+
+If OpenCode is not installed, **STOP** and ask the user to install it first.
+
+---
+
+#### Step 2: Ask About Provider Access (ONE QUESTION AT A TIME)
+
+**IMPORTANT**: Ask these questions **one by one**. Wait for the user's answer before asking the next question. Store their answers to determine the final configuration.
+
+**Question 1** (ask first, wait for response):
+> "Do you have an **Antigravity** subscription? (yes/no)"
+> 
+> *Antigravity provides access to Claude and Gemini models via the `google/` prefix.*
+
+**Question 2** (ask after Question 1 is answered):
+> "Do you have access to **OpenAI** API? (yes/no)"
+> 
+> *This enables `openai/` models like GPT-5.2 Codex.*
+
+**Question 3** (ask after Question 2 is answered):
+> "Do you have access to **Cerebras** API? (yes/no)"
+> 
+> *This enables `cerebras/` models for fast exploration tasks.*
+
+---
+
+#### Step 3: Run the Installer
+
+Based on the user's answers, run the installer with appropriate flags:
+
+```bash
+bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --cerebras=<yes|no>
+```
+
+**Examples:**
+- User has Antigravity + OpenAI: `bunx oh-my-opencode-slim install --no-tui --antigravity=yes --openai=yes --cerebras=no`
+- User has only OpenAI: `bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=yes --cerebras=no`
+- User has Cerebras only: `bunx oh-my-opencode-slim install --no-tui --antigravity=no --openai=no --cerebras=yes`
+
+---
+
+#### Alternative: Manual Configuration
+
+If the installer doesn't work, create the configuration files manually:
+
+##### 3a. Create `~/.config/opencode/opencode.json`
+
+**If user has Antigravity (regardless of other providers):**
+```json
+{
+  "plugin": [
+    "oh-my-opencode-slim",
+    "opencode-antigravity-auth@latest"
+  ],
+  "provider": {
+    "google": {
+      "name": "Google",
+      "models": {
+        "gemini-3-pro-high": {
+          "name": "Gemini 3 Pro High",
+          "limit": { "context": 1048576, "output": 65535 }
+        },
+        "gemini-3-flash": {
+          "name": "Gemini 3 Flash",
+          "limit": { "context": 1048576, "output": 65536 }
+        },
+        "claude-opus-4-5-thinking": {
+          "name": "Claude Opus 4.5 Thinking",
+          "limit": { "context": 200000, "output": 32000 }
+        }
+      }
+    }
+  }
+}
+```
+
+**If user does NOT have Antigravity:**
+```json
+{
+  "plugin": [
+    "oh-my-opencode-slim"
+  ]
+}
+```
+
+##### 3b. Create `~/.config/opencode/oh-my-opencode-slim.json`
+
+Select the appropriate configuration based on the user's answers:
+
+**Scenario A: Antigravity ONLY (no OpenAI, no Cerebras)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "google/claude-opus-4-5-thinking" },
+    "oracle": { "model": "google/claude-opus-4-5-thinking" },
+    "librarian": { "model": "google/gemini-3-flash" },
+    "explore": { "model": "google/gemini-3-flash" },
+    "frontend-ui-ux-engineer": { "model": "google/gemini-3-flash" },
+    "document-writer": { "model": "google/gemini-3-flash" },
+    "multimodal-looker": { "model": "google/gemini-3-flash" },
+    "code-simplicity-reviewer": { "model": "google/claude-opus-4-5-thinking" }
+  }
+}
+```
+
+**Scenario B: Antigravity + OpenAI (no Cerebras)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "google/claude-opus-4-5-thinking" },
+    "oracle": { "model": "openai/gpt-5.2-codex" },
+    "librarian": { "model": "google/gemini-3-flash" },
+    "explore": { "model": "google/gemini-3-flash" },
+    "frontend-ui-ux-engineer": { "model": "google/gemini-3-flash" },
+    "document-writer": { "model": "google/gemini-3-flash" },
+    "multimodal-looker": { "model": "google/gemini-3-flash" },
+    "code-simplicity-reviewer": { "model": "google/claude-opus-4-5-thinking" }
+  }
+}
+```
+
+**Scenario C: Antigravity + Cerebras (no OpenAI)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "google/claude-opus-4-5-thinking" },
+    "oracle": { "model": "google/claude-opus-4-5-thinking" },
+    "librarian": { "model": "google/gemini-3-flash" },
+    "explore": { "model": "cerebras/zai-glm-4.6" },
+    "frontend-ui-ux-engineer": { "model": "google/gemini-3-flash" },
+    "document-writer": { "model": "google/gemini-3-flash" },
+    "multimodal-looker": { "model": "google/gemini-3-flash" },
+    "code-simplicity-reviewer": { "model": "google/claude-opus-4-5-thinking" }
+  }
+}
+```
+
+**Scenario D: Antigravity + OpenAI + Cerebras (full access)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "google/claude-opus-4-5-thinking" },
+    "oracle": { "model": "openai/gpt-5.2-codex" },
+    "librarian": { "model": "google/gemini-3-flash" },
+    "explore": { "model": "cerebras/zai-glm-4.6" },
+    "frontend-ui-ux-engineer": { "model": "google/gemini-3-flash" },
+    "document-writer": { "model": "google/gemini-3-flash" },
+    "multimodal-looker": { "model": "google/gemini-3-flash" },
+    "code-simplicity-reviewer": { "model": "google/claude-opus-4-5-thinking" }
+  }
+}
+```
+
+**Scenario E: OpenAI ONLY (no Antigravity, no Cerebras)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "openai/gpt-5.2-codex" },
+    "oracle": { "model": "openai/gpt-5.2-codex" },
+    "librarian": { "model": "openai/gpt-4.1-mini" },
+    "explore": { "model": "openai/gpt-4.1-mini" },
+    "frontend-ui-ux-engineer": { "model": "openai/gpt-4.1-mini" },
+    "document-writer": { "model": "openai/gpt-4.1-mini" },
+    "multimodal-looker": { "model": "openai/gpt-4.1-mini" },
+    "code-simplicity-reviewer": { "model": "openai/gpt-5.2-codex" }
+  }
+}
+```
+
+**Scenario F: OpenAI + Cerebras (no Antigravity)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "openai/gpt-5.2-codex" },
+    "oracle": { "model": "openai/gpt-5.2-codex" },
+    "librarian": { "model": "openai/gpt-4.1-mini" },
+    "explore": { "model": "cerebras/zai-glm-4.6" },
+    "frontend-ui-ux-engineer": { "model": "openai/gpt-4.1-mini" },
+    "document-writer": { "model": "openai/gpt-4.1-mini" },
+    "multimodal-looker": { "model": "openai/gpt-4.1-mini" },
+    "code-simplicity-reviewer": { "model": "openai/gpt-5.2-codex" }
+  }
+}
+```
+
+**Scenario G: Cerebras ONLY (no Antigravity, no OpenAI)**
+```json
+{
+  "agents": {
+    "orchestrator": { "model": "cerebras/zai-glm-4.6" },
+    "oracle": { "model": "cerebras/zai-glm-4.6" },
+    "librarian": { "model": "cerebras/zai-glm-4.6" },
+    "explore": { "model": "cerebras/zai-glm-4.6" },
+    "frontend-ui-ux-engineer": { "model": "cerebras/zai-glm-4.6" },
+    "document-writer": { "model": "cerebras/zai-glm-4.6" },
+    "multimodal-looker": { "model": "cerebras/zai-glm-4.6" },
+    "code-simplicity-reviewer": { "model": "cerebras/zai-glm-4.6" }
+  }
+}
+```
+
+---
+
+#### Step 4: Verify Configuration
+
+```bash
+opencode --version  # Should be 1.0.150 or higher
+cat ~/.config/opencode/opencode.json
+cat ~/.config/opencode/oh-my-opencode-slim.json
+```
+
+---
+
+#### Step 5: Configure Authentication
+
+Guide the user to authenticate with their providers:
+
+**For Antigravity users:**
+```bash
+opencode auth login
+# Select: Google → OAuth with Google (Antigravity)
+```
+
+**For OpenAI users:**
+```bash
+# Set OPENAI_API_KEY environment variable
+export OPENAI_API_KEY="sk-..."
+```
+
+**For Cerebras users:**
+```bash
+# Set CEREBRAS_API_KEY environment variable
+export CEREBRAS_API_KEY="..."
+```
+
+---
+
+#### Step 6: Print Final Instructions for User
+
+After writing the configuration files, print this message:
+
+```
+Configuration complete!
+
+To authenticate with your providers, run:
+
+    $ opencode auth login
+
+After authentication, start OpenCode:
+
+    $ opencode
+```
+
+---
+
+#### Quick Reference: Provider to Scenario Mapping
+
+| Antigravity | OpenAI | Cerebras | Scenario | Auth Plugin Required |
+|:-----------:|:------:|:--------:|:--------:|:--------------------:|
+| Yes | No | No | A | Yes |
+| Yes | Yes | No | B | Yes |
+| Yes | No | Yes | C | Yes |
+| Yes | Yes | Yes | D | Yes |
+| No | Yes | No | E | No |
+| No | Yes | Yes | F | No |
+| No | No | Yes | G | No |
+
+</details>
+
+---
+
+## Architecture & Flow
 
 
 The plugin follows a "Hub and Spoke" model:
 The plugin follows a "Hub and Spoke" model:
 
 
-1.  **The Orchestrator (Hub)**: The main entry point for user requests. It analyzes the task and decides which specialized agents to call.
-2.  **Specialized Agents (Spokes)**: Domain-specific experts (e.g., UI/UX, Documentation, Architecture) that handle narrow tasks with high precision.
-3.  **Background Manager**: A robust engine that allows the Orchestrator to "fire and forget" tasks (like deep codebase searches or documentation research) while continuing to work on other parts of the problem.
+1. **The Orchestrator (Hub)**: The main entry point for user requests. It analyzes the task and decides which specialized agents to call.
+2. **Specialized Agents (Spokes)**: Domain-specific experts (e.g., UI/UX, Documentation, Architecture) that handle narrow tasks with high precision.
+3. **Background Manager**: A robust engine that allows the Orchestrator to "fire and forget" tasks (like deep codebase searches or documentation research) while continuing to work on other parts of the problem.
 
 
 ### The Flow of a Request
 ### The Flow of a Request
+
 1. **User Prompt**: "Refactor the auth logic and update the docs."
 1. **User Prompt**: "Refactor the auth logic and update the docs."
 2. **Orchestrator**: Creates a TODO list.
 2. **Orchestrator**: Creates a TODO list.
 3. **Delegation**:
 3. **Delegation**:
@@ -21,105 +351,83 @@ The plugin follows a "Hub and Spoke" model:
 
 
 ---
 ---
 
 
-## 🤖 Meet the Agents
+## Agents
 
 
-| Agent | Role | Best Used For... |
-| :--- | :--- | :--- |
-| **orchestrator** | Manager | Planning, task delegation, and overall coordination. |
-| **oracle** | Architect | Complex debugging, architectural decisions, and code reviews. |
-| **explore** | Searcher | Fast codebase grep, finding patterns, and locating definitions. |
-| **librarian** | Researcher | External library docs, GitHub examples, and API research. |
-| **frontend-ui-ux** | Designer | Visual changes, CSS/styling, and React/Vue component polish. |
-| **document-writer** | Scribe | Technical documentation, READMEs, and inline code comments. |
-| **multimodal-looker** | Visionary | Analyzing screenshots, wireframes, or UI designs. |
-| **code-simplicity-reviewer** | Minimalist | Ruthless code simplification and YAGNI principle enforcement. |
+| Agent | Role | Default Model | Best Used For |
+| :--- | :--- | :--- | :--- |
+| **orchestrator** | Manager | `google/claude-opus-4-5-thinking` | Planning, task delegation, and overall coordination. |
+| **oracle** | Architect | `openai/gpt-5.2-codex` | Complex debugging, architectural decisions, and code reviews. |
+| **explore** | Searcher | `cerebras/zai-glm-4.6` | Fast codebase grep, finding patterns, and locating definitions. |
+| **librarian** | Researcher | `google/gemini-3-flash` | External library docs, GitHub examples, and API research. |
+| **frontend-ui-ux-engineer** | Designer | `google/gemini-3-flash` | Visual changes, CSS/styling, and React/Vue component polish. |
+| **document-writer** | Scribe | `google/gemini-3-flash` | Technical documentation, READMEs, and inline code comments. |
+| **multimodal-looker** | Visionary | `google/gemini-3-flash` | Analyzing screenshots, wireframes, or UI designs. |
+| **code-simplicity-reviewer** | Minimalist | `google/claude-opus-4-5-thinking` | Ruthless code simplification and YAGNI principle enforcement. |
 
 
 ---
 ---
 
 
-## 🛠️ Tools & Capabilities
+## Tools & Capabilities
 
 
 ### Background Tasks
 ### Background Tasks
+
 The plugin provides three core tools to manage asynchronous work:
 The plugin provides three core tools to manage asynchronous work:
 
 
--   `background_task`: Launches an agent in a new session.
-    -   `sync=true`: Blocks until the agent finishes (ideal for quick sub-tasks).
-    -   `sync=false`: Runs in the background (ideal for long searches or research).
--   `background_output`: Fetches the result of a background task using its ID.
--   `background_cancel`: Aborts running tasks if they are no longer needed.
+- `background_task`: Launches an agent in a new session.
+  - `sync=true`: Blocks until the agent finishes (ideal for quick sub-tasks).
+  - `sync=false`: Runs in the background (ideal for long searches or research).
+- `background_output`: Fetches the result of a background task using its ID.
+- `background_cancel`: Aborts running tasks if they are no longer needed.
 
 
 ---
 ---
 
 
-## ⚙️ Configuration
+## Configuration
 
 
-You can customize the behavior of the plugin via the OpenCode configuration.
+You can customize the behavior of the plugin via JSON configuration files.
 
 
-### Customizing & Adding Agents
-You can customize built-in agents or add entirely new ones in your `oh-my-opencode-lite.json` file.
+### Configuration Files
 
 
-#### Adding a New Agent
-Simply define a `custom_agents` array. The Orchestrator will automatically learn when to use your new agent based on its `description`.
+The plugin looks for configuration in two places (and merges them):
 
 
-```json
-{
-  "custom_agents": [
-    {
-      "name": "sql-expert",
-      "description": "Schema design, query optimization, and complex joins",
-      "prompt": "You are a Senior DBA. Analyze the schema and provide optimized SQL...",
-      "model": "openai/gpt-4.5"
-    }
-  ]
-}
-```
+1. **User Global**: `~/.config/opencode/oh-my-opencode-slim.json` (or OS equivalent)
+2. **Project Local**: `./.opencode/oh-my-opencode-slim.json`
+
+| Platform | User Config Path |
+| :--- | :--- |
+| **Windows** | `~/.config/opencode/oh-my-opencode-slim.json` or `%APPDATA%\opencode\oh-my-opencode-slim.json` |
+| **macOS/Linux** | `~/.config/opencode/oh-my-opencode-slim.json` |
+
+### Disabling Agents
+
+You can disable specific agents using the `disabled_agents` array:
 
 
-#### Overriding Built-in Agents
 ```json
 ```json
 {
 {
-  "agents": {
-    "oracle": {
-      "model": "claude-3-5-sonnet",
-      "temperature": 0,
-      "prompt_append": "Always prioritize security in your reviews."
-    }
-  },
-  "disabled_agents": ["multimodal-looker"]
+  "disabled_agents": ["multimodal-looker", "code-simplicity-reviewer"]
 }
 }
 ```
 ```
 
 
 ---
 ---
 
 
-## 👨‍💻 Development
+## Uninstallation
 
 
-### Configuration Files
-The plugin looks for configuration in two places (and merges them):
-1.  **User Global**: `~/.config/opencode/oh-my-opencode-lite.json` (or OS equivalent)
-2.  **Project Local**: `./.opencode/oh-my-opencode-lite.json`
-
-### Getting Started
-1.  **Install dependencies**:
-    ```bash
-    bun install
-    ```
-2.  **Run in development mode**:
-    ```bash
-    npm run dev
-    ```
-    This will build the plugin and launch OpenCode with the plugin active.
-
-### Project Structure
--   `src/index.ts`: Plugin entry point and tool registration.
--   `src/agents/`: Definitions and system prompts for all sub-agents.
--   `src/features/background-manager.ts`: State management for async tasks.
--   `src/tools/background.ts`: Implementation of the `background_*` tools.
--   `src/config/`: Configuration schema and loading logic.
-
-### Building
-```bash
-npm run build
-```
-Generates the `dist/` folder with bundled ESM code and TypeScript declarations.
+1. **Remove the plugin from your OpenCode config**:
+
+   Edit `~/.config/opencode/opencode.json` and remove `"oh-my-opencode-slim"` from the `plugin` array.
+
+2. **Remove configuration files (optional)**:
+   ```bash
+   rm -f ~/.config/opencode/oh-my-opencode-slim.json
+   rm -f .opencode/oh-my-opencode-slim.json
+   ```
 
 
 ---
 ---
 
 
-## 📜 License
+## Credits
+
+This is a slimmed-down fork of [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) by [@code-yeongyu](https://github.com/code-yeongyu).
+
+---
+
+## License
+
 MIT
 MIT

BIN
img/hero.png


+ 33 - 4
package.json

@@ -1,15 +1,44 @@
 {
 {
-  "name": "oh-my-opencode-lite",
+  "name": "oh-my-opencode-slim",
   "version": "0.1.0",
   "version": "0.1.0",
-  "description": "Minimal agent orchestration plugin for OpenCode",
+  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
   "types": "dist/index.d.ts",
+  "bin": {
+    "oh-my-opencode-slim": "./dist/cli/index.js"
+  },
   "type": "module",
   "type": "module",
+  "license": "MIT",
+  "keywords": [
+    "opencode",
+    "opencode-plugin",
+    "ai",
+    "agents",
+    "orchestration",
+    "llm",
+    "claude",
+    "gpt",
+    "gemini"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/alvinunreal/oh-my-opencode-slim"
+  },
+  "bugs": {
+    "url": "https://github.com/alvinunreal/oh-my-opencode-slim/issues"
+  },
+  "homepage": "https://github.com/alvinunreal/oh-my-opencode-slim#readme",
+  "files": [
+    "dist",
+    "README.md",
+    "LICENSE"
+  ],
   "scripts": {
   "scripts": {
-    "build": "bun build src/index.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly",
+    "build": "bun build src/index.ts --outdir dist --target bun --format esm && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm && tsc --emitDeclarationOnly",
     "typecheck": "tsc --noEmit",
     "typecheck": "tsc --noEmit",
     "test": "bun test",
     "test": "bun test",
-    "dev": "bun run build && opencode"
+    "dev": "bun run build && opencode",
+    "prepublishOnly": "bun run build"
   },
   },
   "dependencies": {
   "dependencies": {
     "@opencode-ai/plugin": "^1.1.19",
     "@opencode-ai/plugin": "^1.1.19",

+ 0 - 1
src/agents/document-writer.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createDocumentWriterAgent(model: string): AgentDefinition {
 export function createDocumentWriterAgent(model: string): AgentDefinition {

+ 0 - 1
src/agents/explore.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createExploreAgent(model: string): AgentDefinition {
 export function createExploreAgent(model: string): AgentDefinition {

+ 0 - 1
src/agents/frontend.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createFrontendAgent(model: string): AgentDefinition {
 export function createFrontendAgent(model: string): AgentDefinition {

+ 15 - 22
src/agents/index.ts

@@ -1,5 +1,5 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
-import { DEFAULT_MODELS, type AgentName, type PluginConfig } from "../config";
+import type { AgentConfig as SDKAgentConfig } from "@opencode-ai/sdk";
+import { DEFAULT_MODELS, type AgentName, type PluginConfig, type AgentOverrideConfig } from "../config";
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOrchestratorAgent, type AgentDefinition } from "./orchestrator";
 import { createOracleAgent } from "./oracle";
 import { createOracleAgent } from "./oracle";
 import { createLibrarianAgent } from "./librarian";
 import { createLibrarianAgent } from "./librarian";
@@ -13,6 +13,15 @@ export type { AgentDefinition } from "./orchestrator";
 
 
 type AgentFactory = (model: string) => AgentDefinition;
 type AgentFactory = (model: string) => AgentDefinition;
 
 
+function applyOverrides(agent: AgentDefinition, override: AgentOverrideConfig): void {
+  if (override.model) agent.config.model = override.model;
+  if (override.temperature !== undefined) agent.config.temperature = override.temperature;
+  if (override.prompt) agent.config.system = override.prompt;
+  if (override.prompt_append) {
+    agent.config.system = `${agent.config.system}\n\n${override.prompt_append}`;
+  }
+}
+
 const SUBAGENT_FACTORIES: Omit<Record<AgentName, AgentFactory>, "orchestrator"> = {
 const SUBAGENT_FACTORIES: Omit<Record<AgentName, AgentFactory>, "orchestrator"> = {
   oracle: createOracleAgent,
   oracle: createOracleAgent,
   librarian: createLibrarianAgent,
   librarian: createLibrarianAgent,
@@ -27,21 +36,12 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
   const disabledAgents = new Set(config?.disabled_agents ?? []);
   const disabledAgents = new Set(config?.disabled_agents ?? []);
   const agentOverrides = config?.agents ?? {};
   const agentOverrides = config?.agents ?? {};
 
 
-  // 1. Gather all sub-agent proto-definitions (built-in + custom)
+  // 1. Gather all sub-agent proto-definitions
   const protoSubAgents: AgentDefinition[] = [
   const protoSubAgents: AgentDefinition[] = [
     ...Object.entries(SUBAGENT_FACTORIES).map(([name, factory]) => {
     ...Object.entries(SUBAGENT_FACTORIES).map(([name, factory]) => {
       const model = DEFAULT_MODELS[name as AgentName];
       const model = DEFAULT_MODELS[name as AgentName];
       return factory(model);
       return factory(model);
     }),
     }),
-    ...(config?.custom_agents ?? []).map((ca) => ({
-      name: ca.name,
-      description: ca.description,
-      config: {
-        model: ca.model ?? "anthropic/claude-sonnet-4-5",
-        temperature: ca.temperature ?? 0.1,
-        system: ca.prompt,
-      },
-    })),
   ];
   ];
 
 
   // 2. Apply common filtering and overrides
   // 2. Apply common filtering and overrides
@@ -50,11 +50,7 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
     .map((agent) => {
     .map((agent) => {
       const override = agentOverrides[agent.name];
       const override = agentOverrides[agent.name];
       if (override) {
       if (override) {
-        if (override.model) agent.config.model = override.model;
-        if (override.temperature !== undefined) agent.config.temperature = override.temperature;
-        if (override.prompt) agent.config.system = override.prompt;
-        if (override.prompt_append)
-          agent.config.system = `${agent.config.system}\n\n${override.prompt_append}`;
+        applyOverrides(agent, override);
       }
       }
       return agent;
       return agent;
     });
     });
@@ -65,16 +61,13 @@ export function createAgents(config?: PluginConfig): AgentDefinition[] {
   const orchestrator = createOrchestratorAgent(orchestratorModel, allSubAgents);
   const orchestrator = createOrchestratorAgent(orchestratorModel, allSubAgents);
   const oOverride = agentOverrides["orchestrator"];
   const oOverride = agentOverrides["orchestrator"];
   if (oOverride) {
   if (oOverride) {
-    if (oOverride.temperature !== undefined) orchestrator.config.temperature = oOverride.temperature;
-    if (oOverride.prompt) orchestrator.config.system = oOverride.prompt;
-    if (oOverride.prompt_append)
-      orchestrator.config.system = `${orchestrator.config.system}\n\n${oOverride.prompt_append}`;
+    applyOverrides(orchestrator, oOverride);
   }
   }
 
 
   return [orchestrator, ...allSubAgents];
   return [orchestrator, ...allSubAgents];
 }
 }
 
 
-export function getAgentConfigs(config?: PluginConfig): Record<string, AgentConfig> {
+export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentConfig> {
   const agents = createAgents(config);
   const agents = createAgents(config);
   return Object.fromEntries(agents.map((a) => [a.name, a.config]));
   return Object.fromEntries(agents.map((a) => [a.name, a.config]));
 }
 }

+ 0 - 1
src/agents/librarian.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createLibrarianAgent(model: string): AgentDefinition {
 export function createLibrarianAgent(model: string): AgentDefinition {

+ 0 - 1
src/agents/multimodal.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createMultimodalAgent(model: string): AgentDefinition {
 export function createMultimodalAgent(model: string): AgentDefinition {

+ 0 - 1
src/agents/oracle.ts

@@ -1,4 +1,3 @@
-import type { AgentConfig } from "@opencode-ai/sdk";
 import type { AgentDefinition } from "./orchestrator";
 import type { AgentDefinition } from "./orchestrator";
 
 
 export function createOracleAgent(model: string): AgentDefinition {
 export function createOracleAgent(model: string): AgentDefinition {

+ 331 - 0
src/cli/config-manager.ts

@@ -0,0 +1,331 @@
+import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs"
+import { homedir } from "node:os"
+import { join } from "node:path"
+import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
+
+const PACKAGE_NAME = "oh-my-opencode-lite"
+
+function getConfigDir(): string {
+  return process.env.XDG_CONFIG_HOME
+    ? join(process.env.XDG_CONFIG_HOME, "opencode")
+    : join(homedir(), ".config", "opencode")
+}
+
+function getConfigJson(): string {
+  return join(getConfigDir(), "opencode.json")
+}
+
+function getLiteConfig(): string {
+  return join(getConfigDir(), "oh-my-opencode-lite.json")
+}
+
+function ensureConfigDir(): void {
+  const configDir = getConfigDir()
+  if (!existsSync(configDir)) {
+    mkdirSync(configDir, { recursive: true })
+  }
+}
+
+interface OpenCodeConfig {
+  plugin?: string[]
+  provider?: Record<string, unknown>
+  [key: string]: unknown
+}
+
+function parseConfig(path: string): OpenCodeConfig | null {
+  try {
+    if (!existsSync(path)) return null
+    const stat = statSync(path)
+    if (stat.size === 0) return null
+    const content = readFileSync(path, "utf-8")
+    if (content.trim().length === 0) return null
+    return JSON.parse(content) as OpenCodeConfig
+  } catch {
+    return null
+  }
+}
+
+export async function isOpenCodeInstalled(): Promise<boolean> {
+  try {
+    const proc = Bun.spawn(["opencode", "--version"], {
+      stdout: "pipe",
+      stderr: "pipe",
+    })
+    await proc.exited
+    return proc.exitCode === 0
+  } catch {
+    return false
+  }
+}
+
+export async function getOpenCodeVersion(): Promise<string | null> {
+  try {
+    const proc = Bun.spawn(["opencode", "--version"], {
+      stdout: "pipe",
+      stderr: "pipe",
+    })
+    const output = await new Response(proc.stdout).text()
+    await proc.exited
+    return proc.exitCode === 0 ? output.trim() : null
+  } catch {
+    return null
+  }
+}
+
+export async function fetchLatestVersion(packageName: string): Promise<string | null> {
+  try {
+    const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`)
+    if (!res.ok) return null
+    const data = (await res.json()) as { version: string }
+    return data.version
+  } catch {
+    return null
+  }
+}
+
+export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
+  try {
+    ensureConfigDir()
+  } catch (err) {
+    return {
+      success: false,
+      configPath: getConfigDir(),
+      error: `Failed to create config directory: ${err}`,
+    }
+  }
+
+  const configPath = getConfigJson()
+
+  try {
+    let config = parseConfig(configPath) ?? {}
+    const plugins = config.plugin ?? []
+
+    // Remove existing oh-my-opencode-lite entries
+    const filteredPlugins = plugins.filter(
+      (p) => p !== PACKAGE_NAME && !p.startsWith(`${PACKAGE_NAME}@`)
+    )
+
+    // Add fresh entry
+    filteredPlugins.push(PACKAGE_NAME)
+    config.plugin = filteredPlugins
+
+    writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
+    return { success: true, configPath }
+  } catch (err) {
+    return {
+      success: false,
+      configPath,
+      error: `Failed to update opencode config: ${err}`,
+    }
+  }
+}
+
+export async function addAuthPlugins(installConfig: InstallConfig): Promise<ConfigMergeResult> {
+  const configPath = getConfigJson()
+
+  try {
+    ensureConfigDir()
+    let config = parseConfig(configPath) ?? {}
+    const plugins = config.plugin ?? []
+
+    if (installConfig.hasAntigravity) {
+      const version = await fetchLatestVersion("opencode-antigravity-auth")
+      const pluginEntry = version
+        ? `opencode-antigravity-auth@${version}`
+        : "opencode-antigravity-auth@latest"
+
+      if (!plugins.some((p) => p.startsWith("opencode-antigravity-auth"))) {
+        plugins.push(pluginEntry)
+      }
+    }
+
+    config.plugin = plugins
+    writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
+    return { success: true, configPath }
+  } catch (err) {
+    return {
+      success: false,
+      configPath,
+      error: `Failed to add auth plugins: ${err}`,
+    }
+  }
+}
+
+/**
+ * Provider configurations for Google models (via Antigravity auth plugin)
+ */
+const GOOGLE_PROVIDER_CONFIG = {
+  google: {
+    name: "Google",
+    models: {
+      "gemini-3-pro-high": {
+        name: "Gemini 3 Pro High",
+        thinking: true,
+        attachment: true,
+        limit: { context: 1048576, output: 65535 },
+        modalities: { input: ["text", "image", "pdf"], output: ["text"] },
+      },
+      "gemini-3-flash": {
+        name: "Gemini 3 Flash",
+        attachment: true,
+        limit: { context: 1048576, output: 65536 },
+        modalities: { input: ["text", "image", "pdf"], output: ["text"] },
+      },
+      "claude-opus-4-5-thinking": {
+        name: "Claude Opus 4.5 Thinking",
+        attachment: true,
+        limit: { context: 200000, output: 32000 },
+        modalities: { input: ["text", "image", "pdf"], output: ["text"] },
+      },
+      "claude-sonnet-4-5-thinking": {
+        name: "Claude Sonnet 4.5 Thinking",
+        attachment: true,
+        limit: { context: 200000, output: 32000 },
+        modalities: { input: ["text", "image", "pdf"], output: ["text"] },
+      },
+    },
+  },
+}
+
+export function addProviderConfig(installConfig: InstallConfig): ConfigMergeResult {
+  const configPath = getConfigJson()
+
+  try {
+    ensureConfigDir()
+    let config = parseConfig(configPath) ?? {}
+
+    if (installConfig.hasAntigravity) {
+      const providers = (config.provider ?? {}) as Record<string, unknown>
+      providers.google = GOOGLE_PROVIDER_CONFIG.google
+      config.provider = providers
+    }
+
+    writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
+    return { success: true, configPath }
+  } catch (err) {
+    return {
+      success: false,
+      configPath,
+      error: `Failed to add provider config: ${err}`,
+    }
+  }
+}
+
+// Model mappings by provider priority
+const MODEL_MAPPINGS = {
+  antigravity: {
+    orchestrator: "google/claude-opus-4-5-thinking",
+    "code-simplicity-reviewer": "google/claude-opus-4-5-thinking",
+    oracle: "google/claude-opus-4-5-thinking",
+    librarian: "google/gemini-3-flash",
+    explore: "google/gemini-3-flash",
+    "frontend-ui-ux-engineer": "google/gemini-3-flash",
+    "document-writer": "google/gemini-3-flash",
+    "multimodal-looker": "google/gemini-3-flash",
+  },
+  openai: {
+    orchestrator: "openai/gpt-5.2-codex",
+    "code-simplicity-reviewer": "openai/gpt-5.2-codex",
+    oracle: "openai/gpt-5.2-codex",
+    librarian: "openai/gpt-4.1-mini",
+    explore: "openai/gpt-4.1-mini",
+    "frontend-ui-ux-engineer": "openai/gpt-4.1-mini",
+    "document-writer": "openai/gpt-4.1-mini",
+    "multimodal-looker": "openai/gpt-4.1-mini",
+  },
+  cerebras: {
+    orchestrator: "cerebras/zai-glm-4.6",
+    "code-simplicity-reviewer": "cerebras/zai-glm-4.6",
+    oracle: "cerebras/zai-glm-4.6",
+    librarian: "cerebras/zai-glm-4.6",
+    explore: "cerebras/zai-glm-4.6",
+    "frontend-ui-ux-engineer": "cerebras/zai-glm-4.6",
+    "document-writer": "cerebras/zai-glm-4.6",
+    "multimodal-looker": "cerebras/zai-glm-4.6",
+  },
+} as const;
+
+export function generateLiteConfig(installConfig: InstallConfig): Record<string, unknown> {
+  // Determine base provider
+  const baseProvider = installConfig.hasAntigravity
+    ? "antigravity"
+    : installConfig.hasOpenAI
+      ? "openai"
+      : installConfig.hasCerebras
+        ? "cerebras"
+        : null;
+
+  if (!baseProvider) {
+    return { agents: {} };
+  }
+
+  // Start with base provider models
+  const agents: Record<string, { model: string }> = Object.fromEntries(
+    Object.entries(MODEL_MAPPINGS[baseProvider]).map(([k, v]) => [k, { model: v }])
+  );
+
+  // Apply provider-specific overrides for mixed configurations
+  if (installConfig.hasAntigravity) {
+    if (installConfig.hasOpenAI) {
+      agents["oracle"] = { model: "openai/gpt-5.2-codex" };
+    }
+    if (installConfig.hasCerebras) {
+      agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+    }
+  } else if (installConfig.hasOpenAI && installConfig.hasCerebras) {
+    agents["explore"] = { model: "cerebras/zai-glm-4.6" };
+  }
+
+  return { agents };
+}
+
+export function writeLiteConfig(installConfig: InstallConfig): ConfigMergeResult {
+  const configPath = getLiteConfig()
+
+  try {
+    ensureConfigDir()
+    const config = generateLiteConfig(installConfig)
+    writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n")
+    return { success: true, configPath }
+  } catch (err) {
+    return {
+      success: false,
+      configPath,
+      error: `Failed to write lite config: ${err}`,
+    }
+  }
+}
+
+export function detectCurrentConfig(): DetectedConfig {
+  const result: DetectedConfig = {
+    isInstalled: false,
+    hasAntigravity: false,
+    hasOpenAI: false,
+    hasCerebras: false,
+  }
+
+  const config = parseConfig(getConfigJson())
+  if (!config) return result
+
+  const plugins = config.plugin ?? []
+  result.isInstalled = plugins.some((p) => p.startsWith(PACKAGE_NAME))
+  result.hasAntigravity = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
+
+  // Try to detect from lite config
+  const liteConfig = parseConfig(getLiteConfig())
+  if (liteConfig && typeof liteConfig === "object") {
+    const agents = (liteConfig as Record<string, unknown>).agents as
+      | Record<string, { model?: string }>
+      | undefined
+
+    if (agents) {
+      const models = Object.values(agents)
+        .map((a) => a?.model)
+        .filter(Boolean)
+      result.hasOpenAI = models.some((m) => m?.startsWith("openai/"))
+      result.hasCerebras = models.some((m) => m?.startsWith("cerebras/"))
+    }
+  }
+
+  return result
+}

+ 70 - 0
src/cli/index.ts

@@ -0,0 +1,70 @@
+#!/usr/bin/env bun
+import { install } from "./install"
+import type { InstallArgs, BooleanArg } from "./types"
+
+function parseArgs(args: string[]): InstallArgs {
+  const result: InstallArgs = {
+    tui: true,
+  }
+
+  for (const arg of args) {
+    if (arg === "--no-tui") {
+      result.tui = false
+    } else if (arg === "--skip-auth") {
+      result.skipAuth = true
+    } else if (arg.startsWith("--antigravity=")) {
+      result.antigravity = arg.split("=")[1] as BooleanArg
+    } else if (arg.startsWith("--openai=")) {
+      result.openai = arg.split("=")[1] as BooleanArg
+    } else if (arg.startsWith("--cerebras=")) {
+      result.cerebras = arg.split("=")[1] as BooleanArg
+    } else if (arg === "-h" || arg === "--help") {
+      printHelp()
+      process.exit(0)
+    }
+  }
+
+  return result
+}
+
+function printHelp(): void {
+  console.log(`
+oh-my-opencode-lite installer
+
+Usage: bunx oh-my-opencode-lite install [OPTIONS]
+
+Options:
+  --antigravity=yes|no   Antigravity subscription (yes/no)
+  --openai=yes|no        OpenAI API access (yes/no)
+  --cerebras=yes|no      Cerebras API access (yes/no)
+  --no-tui               Non-interactive mode (requires all flags)
+  --skip-auth            Skip authentication reminder
+  -h, --help             Show this help message
+
+Examples:
+  bunx oh-my-opencode-lite install
+  bunx oh-my-opencode-lite install --no-tui --antigravity=yes --openai=yes --cerebras=no
+`)
+}
+
+async function main(): Promise<void> {
+  const args = process.argv.slice(2)
+
+  if (args.length === 0 || args[0] === "install") {
+    const installArgs = parseArgs(args.slice(args[0] === "install" ? 1 : 0))
+    const exitCode = await install(installArgs)
+    process.exit(exitCode)
+  } else if (args[0] === "-h" || args[0] === "--help") {
+    printHelp()
+    process.exit(0)
+  } else {
+    console.error(`Unknown command: ${args[0]}`)
+    console.error("Run with --help for usage information")
+    process.exit(1)
+  }
+}
+
+main().catch((err) => {
+  console.error("Fatal error:", err)
+  process.exit(1)
+})

+ 278 - 0
src/cli/install.ts

@@ -0,0 +1,278 @@
+import type { InstallArgs, InstallConfig, BooleanArg, DetectedConfig } from "./types"
+import {
+  addPluginToOpenCodeConfig,
+  writeLiteConfig,
+  isOpenCodeInstalled,
+  getOpenCodeVersion,
+  addAuthPlugins,
+  addProviderConfig,
+  detectCurrentConfig,
+} from "./config-manager"
+
+const GREEN = "\x1b[32m"
+const BLUE = "\x1b[34m"
+const YELLOW = "\x1b[33m"
+const RED = "\x1b[31m"
+const BOLD = "\x1b[1m"
+const DIM = "\x1b[2m"
+const RESET = "\x1b[0m"
+
+const SYMBOLS = {
+  check: `${GREEN}✓${RESET}`,
+  cross: `${RED}✗${RESET}`,
+  arrow: `${BLUE}→${RESET}`,
+  bullet: `${DIM}•${RESET}`,
+  info: `${BLUE}ℹ${RESET}`,
+  warn: `${YELLOW}⚠${RESET}`,
+  star: `${YELLOW}★${RESET}`,
+}
+
+function printHeader(isUpdate: boolean): void {
+  const mode = isUpdate ? "Update" : "Install"
+  console.log()
+  console.log(`${BOLD}oh-my-opencode-lite ${mode}${RESET}`)
+  console.log("=".repeat(30))
+  console.log()
+}
+
+function printStep(step: number, total: number, message: string): void {
+  console.log(`${DIM}[${step}/${total}]${RESET} ${message}`)
+}
+
+function printSuccess(message: string): void {
+  console.log(`${SYMBOLS.check} ${message}`)
+}
+
+function printError(message: string): void {
+  console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`)
+}
+
+function printInfo(message: string): void {
+  console.log(`${SYMBOLS.info} ${message}`)
+}
+
+function printWarning(message: string): void {
+  console.log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`)
+}
+
+function formatConfigSummary(config: InstallConfig): string {
+  const lines: string[] = []
+  lines.push(`${BOLD}Configuration Summary${RESET}`)
+  lines.push("")
+  lines.push(`  ${config.hasAntigravity ? SYMBOLS.check : DIM + "○" + RESET} Antigravity`)
+  lines.push(`  ${config.hasOpenAI ? SYMBOLS.check : DIM + "○" + RESET} OpenAI`)
+  lines.push(`  ${config.hasCerebras ? SYMBOLS.check : DIM + "○" + RESET} Cerebras`)
+  return lines.join("\n")
+}
+
+function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } {
+  const errors: string[] = []
+
+  if (args.antigravity === undefined) {
+    errors.push("--antigravity is required (values: yes, no)")
+  } else if (!["yes", "no"].includes(args.antigravity)) {
+    errors.push(`Invalid --antigravity value: ${args.antigravity} (expected: yes, no)`)
+  }
+
+  if (args.openai === undefined) {
+    errors.push("--openai is required (values: yes, no)")
+  } else if (!["yes", "no"].includes(args.openai)) {
+    errors.push(`Invalid --openai value: ${args.openai} (expected: yes, no)`)
+  }
+
+  if (args.cerebras === undefined) {
+    errors.push("--cerebras is required (values: yes, no)")
+  } else if (!["yes", "no"].includes(args.cerebras)) {
+    errors.push(`Invalid --cerebras value: ${args.cerebras} (expected: yes, no)`)
+  }
+
+  return { valid: errors.length === 0, errors }
+}
+
+function argsToConfig(args: InstallArgs): InstallConfig {
+  return {
+    hasAntigravity: args.antigravity === "yes",
+    hasOpenAI: args.openai === "yes",
+    hasCerebras: args.cerebras === "yes",
+  }
+}
+
+function detectedToInitialValues(detected: DetectedConfig): {
+  antigravity: BooleanArg
+  openai: BooleanArg
+  cerebras: BooleanArg
+} {
+  return {
+    antigravity: detected.hasAntigravity ? "yes" : "no",
+    openai: detected.hasOpenAI ? "yes" : "no",
+    cerebras: detected.hasCerebras ? "yes" : "no",
+  }
+}
+
+async function askYesNo(prompt: string, defaultValue: BooleanArg = "no"): Promise<BooleanArg> {
+  const defaultHint = defaultValue === "yes" ? "[Y/n]" : "[y/N]"
+  process.stdout.write(`${BLUE}${prompt}${RESET} ${defaultHint}: `)
+
+  const reader = Bun.stdin.stream().getReader()
+  const { value } = await reader.read()
+  reader.releaseLock()
+
+  const answer = value ? new TextDecoder().decode(value).trim().toLowerCase() : ""
+
+  if (answer === "" || answer === "\n") return defaultValue
+  if (answer === "y" || answer === "yes") return "yes"
+  if (answer === "n" || answer === "no") return "no"
+  return defaultValue
+}
+
+async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | null> {
+  const initial = detectedToInitialValues(detected)
+
+  console.log(`${BOLD}Question 1/3:${RESET}`)
+  const antigravity = await askYesNo(
+    "Do you have an Antigravity subscription?",
+    initial.antigravity
+  )
+  console.log()
+
+  console.log(`${BOLD}Question 2/3:${RESET}`)
+  const openai = await askYesNo("Do you have access to OpenAI API?", initial.openai)
+  console.log()
+
+  console.log(`${BOLD}Question 3/3:${RESET}`)
+  const cerebras = await askYesNo("Do you have access to Cerebras API?", initial.cerebras)
+  console.log()
+
+  return {
+    hasAntigravity: antigravity === "yes",
+    hasOpenAI: openai === "yes",
+    hasCerebras: cerebras === "yes",
+  }
+}
+
+async function runInstall(args: InstallArgs, config: InstallConfig): Promise<number> {
+  const detected = detectCurrentConfig()
+  const isUpdate = detected.isInstalled
+
+  printHeader(isUpdate)
+
+  const totalSteps = config.hasAntigravity ? 5 : 3
+  let step = 1
+
+  // Step 1: Check OpenCode
+  printStep(step++, totalSteps, "Checking OpenCode installation...")
+  const installed = await isOpenCodeInstalled()
+  if (!installed) {
+    printError("OpenCode is not installed on this system.")
+    printInfo("Visit https://opencode.ai/docs for installation instructions")
+    return 1
+  }
+
+  const version = await getOpenCodeVersion()
+  printSuccess(`OpenCode ${version ?? ""} detected`)
+
+  // Step 2: Add plugin
+  printStep(step++, totalSteps, "Adding oh-my-opencode-lite plugin...")
+  const pluginResult = await addPluginToOpenCodeConfig()
+  if (!pluginResult.success) {
+    printError(`Failed: ${pluginResult.error}`)
+    return 1
+  }
+  printSuccess(`Plugin added ${SYMBOLS.arrow} ${DIM}${pluginResult.configPath}${RESET}`)
+
+  // Step 3-4: Auth plugins and provider config (if Antigravity)
+  if (config.hasAntigravity) {
+    printStep(step++, totalSteps, "Adding auth plugins...")
+    const authResult = await addAuthPlugins(config)
+    if (!authResult.success) {
+      printError(`Failed: ${authResult.error}`)
+      return 1
+    }
+    printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${DIM}${authResult.configPath}${RESET}`)
+
+    printStep(step++, totalSteps, "Adding provider configurations...")
+    const providerResult = addProviderConfig(config)
+    if (!providerResult.success) {
+      printError(`Failed: ${providerResult.error}`)
+      return 1
+    }
+    printSuccess(`Providers configured ${SYMBOLS.arrow} ${DIM}${providerResult.configPath}${RESET}`)
+  }
+
+  // Step 5: Write lite config
+  printStep(step++, totalSteps, "Writing oh-my-opencode-lite configuration...")
+  const liteResult = writeLiteConfig(config)
+  if (!liteResult.success) {
+    printError(`Failed: ${liteResult.error}`)
+    return 1
+  }
+  printSuccess(`Config written ${SYMBOLS.arrow} ${DIM}${liteResult.configPath}${RESET}`)
+
+  // Summary
+  console.log()
+  console.log(formatConfigSummary(config))
+  console.log()
+
+  if (!config.hasAntigravity && !config.hasOpenAI && !config.hasCerebras) {
+    printWarning("No providers configured. At least one provider is required.")
+    return 1
+  }
+
+  console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? "Configuration updated!" : "Installation complete!"}${RESET}`)
+  console.log()
+  console.log(`${BOLD}Next steps:${RESET}`)
+  console.log()
+  console.log(`  1. Authenticate with your providers:`)
+  console.log(`     ${BLUE}$ opencode auth login${RESET}`)
+  console.log()
+  console.log(`  2. Start OpenCode:`)
+  console.log(`     ${BLUE}$ opencode${RESET}`)
+  console.log()
+
+  return 0
+}
+
+export async function install(args: InstallArgs): Promise<number> {
+  if (!args.tui) {
+    // Non-TUI mode: validate args
+    const validation = validateNonTuiArgs(args)
+    if (!validation.valid) {
+      printHeader(false)
+      printError("Validation failed:")
+      for (const err of validation.errors) {
+        console.log(`  ${SYMBOLS.bullet} ${err}`)
+      }
+      console.log()
+      printInfo(
+        "Usage: bunx oh-my-opencode-lite install --no-tui --antigravity=<yes|no> --openai=<yes|no> --cerebras=<yes|no>"
+      )
+      console.log()
+      return 1
+    }
+
+    const config = argsToConfig(args)
+    return runInstall(args, config)
+  }
+
+  // TUI mode
+  const detected = detectCurrentConfig()
+
+  printHeader(detected.isInstalled)
+
+  printStep(1, 1, "Checking OpenCode installation...")
+  const installed = await isOpenCodeInstalled()
+  if (!installed) {
+    printError("OpenCode is not installed on this system.")
+    printInfo("Visit https://opencode.ai/docs for installation instructions")
+    return 1
+  }
+
+  const version = await getOpenCodeVersion()
+  printSuccess(`OpenCode ${version ?? ""} detected`)
+  console.log()
+
+  const config = await runTuiMode(detected)
+  if (!config) return 1
+
+  return runInstall(args, config)
+}

+ 28 - 0
src/cli/types.ts

@@ -0,0 +1,28 @@
+export type BooleanArg = "yes" | "no"
+
+export interface InstallArgs {
+  tui: boolean
+  antigravity?: BooleanArg
+  openai?: BooleanArg
+  cerebras?: BooleanArg
+  skipAuth?: boolean
+}
+
+export interface InstallConfig {
+  hasAntigravity: boolean
+  hasOpenAI: boolean
+  hasCerebras: boolean
+}
+
+export interface ConfigMergeResult {
+  success: boolean
+  configPath: string
+  error?: string
+}
+
+export interface DetectedConfig {
+  isInstalled: boolean
+  hasAntigravity: boolean
+  hasOpenAI: boolean
+  hasCerebras: boolean
+}

+ 11 - 0
src/config/constants.ts

@@ -0,0 +1,11 @@
+// Polling configuration
+export const POLL_INTERVAL_MS = 500;
+export const POLL_INTERVAL_SLOW_MS = 1000;
+export const POLL_INTERVAL_BACKGROUND_MS = 2000;
+
+// Timeouts
+export const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes
+export const MAX_POLL_TIME_MS = 5 * 60 * 1000; // 5 minutes
+
+// Polling stability
+export const STABLE_POLLS_THRESHOLD = 3;

+ 1 - 0
src/config/index.ts

@@ -1,2 +1,3 @@
 export * from "./schema";
 export * from "./schema";
+export * from "./constants";
 export { loadPluginConfig } from "./loader";
 export { loadPluginConfig } from "./loader";

+ 2 - 10
src/config/loader.ts

@@ -18,15 +18,13 @@ function loadConfigFromPath(configPath: string): PluginConfig | null {
       const result = PluginConfigSchema.safeParse(rawConfig);
       const result = PluginConfigSchema.safeParse(rawConfig);
       
       
       if (!result.success) {
       if (!result.success) {
-        console.error(`[lite] Config validation error in ${configPath}:`, result.error.issues);
         return null;
         return null;
       }
       }
       
       
-      console.log(`[lite] Config loaded from ${configPath}`);
       return result.data;
       return result.data;
     }
     }
-  } catch (err) {
-    console.error(`[lite] Error loading config from ${configPath}:`, err);
+  } catch {
+    // Silently ignore config loading errors
   }
   }
   return null;
   return null;
 }
 }
@@ -73,12 +71,6 @@ export function loadPluginConfig(directory: string): PluginConfig {
       ...config,
       ...config,
       ...projectConfig,
       ...projectConfig,
       agents: deepMerge(config.agents, projectConfig.agents),
       agents: deepMerge(config.agents, projectConfig.agents),
-      custom_agents: [
-        ...new Map([
-          ...(config.custom_agents ?? []).map((a) => [a.name, a] as const),
-          ...(projectConfig.custom_agents ?? []).map((a) => [a.name, a] as const),
-        ]).values(),
-      ],
       disabled_agents: [
       disabled_agents: [
         ...new Set([
         ...new Set([
           ...(config.disabled_agents ?? []),
           ...(config.disabled_agents ?? []),

+ 12 - 19
src/config/schema.ts

@@ -1,7 +1,7 @@
 import { z } from "zod";
 import { z } from "zod";
 
 
-// Agent configuration
-export const AgentConfigSchema = z.object({
+// Agent override configuration (distinct from SDK's AgentConfig)
+export const AgentOverrideConfigSchema = z.object({
   model: z.string().optional(),
   model: z.string().optional(),
   temperature: z.number().min(0).max(2).optional(),
   temperature: z.number().min(0).max(2).optional(),
   prompt: z.string().optional(),
   prompt: z.string().optional(),
@@ -9,18 +9,11 @@ export const AgentConfigSchema = z.object({
   disable: z.boolean().optional(),
   disable: z.boolean().optional(),
 });
 });
 
 
-export type AgentConfig = z.infer<typeof AgentConfigSchema>;
+export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>;
 
 
 // Main plugin config
 // Main plugin config
 export const PluginConfigSchema = z.object({
 export const PluginConfigSchema = z.object({
-  agents: z.record(z.string(), AgentConfigSchema).optional(),
-  custom_agents: z.array(z.object({
-    name: z.string(),
-    description: z.string(),
-    prompt: z.string(),
-    model: z.string().optional(),
-    temperature: z.number().optional(),
-  })).optional(),
+  agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),
   disabled_agents: z.array(z.string()).optional(),
   disabled_agents: z.array(z.string()).optional(),
 });
 });
 
 
@@ -38,12 +31,12 @@ export type AgentName =
   | "code-simplicity-reviewer";
   | "code-simplicity-reviewer";
 
 
 export const DEFAULT_MODELS: Record<AgentName, string> = {
 export const DEFAULT_MODELS: Record<AgentName, string> = {
-  orchestrator: "anthropic/claude-sonnet-4-5",
-  oracle: "openai/gpt-4.1",
-  librarian: "anthropic/claude-sonnet-4-5",
-  explore: "anthropic/claude-haiku-4-5",
-  "frontend-ui-ux-engineer": "google/gemini-2.5-pro",
-  "document-writer": "google/gemini-2.5-pro",
-  "multimodal-looker": "google/gemini-2.5-flash",
-  "code-simplicity-reviewer": "anthropic/claude-sonnet-4-5",
+  orchestrator: "google/claude-opus-4-5-thinking",
+  oracle: "openai/gpt-5.2-codex",
+  librarian: "google/gemini-3-flash",
+  explore: "cerebras/zai-glm-4.6",
+  "frontend-ui-ux-engineer": "google/gemini-3-flash",
+  "document-writer": "google/gemini-3-flash",
+  "multimodal-looker": "google/gemini-3-flash",
+  "code-simplicity-reviewer": "google/claude-opus-4-5-thinking",
 };
 };

+ 3 - 2
src/features/background-manager.ts

@@ -1,4 +1,5 @@
 import type { PluginInput } from "@opencode-ai/plugin";
 import type { PluginInput } from "@opencode-ai/plugin";
+import { POLL_INTERVAL_BACKGROUND_MS, POLL_INTERVAL_SLOW_MS } from "../config";
 
 
 type OpencodeClient = PluginInput["client"];
 type OpencodeClient = PluginInput["client"];
 
 
@@ -96,7 +97,7 @@ export class BackgroundTaskManager {
       if (status === "completed" || status === "failed") {
       if (status === "completed" || status === "failed") {
         return task;
         return task;
       }
       }
-      await new Promise((r) => setTimeout(r, 1000));
+      await new Promise((r) => setTimeout(r, POLL_INTERVAL_SLOW_MS));
     }
     }
 
 
     return task;
     return task;
@@ -128,7 +129,7 @@ export class BackgroundTaskManager {
 
 
   private startPolling() {
   private startPolling() {
     if (this.pollInterval) return;
     if (this.pollInterval) return;
-    this.pollInterval = setInterval(() => this.pollAllTasks(), 2000);
+    this.pollInterval = setInterval(() => this.pollAllTasks(), POLL_INTERVAL_BACKGROUND_MS);
   }
   }
 
 
   private async pollAllTasks() {
   private async pollAllTasks() {

+ 1 - 8
src/index.ts

@@ -27,16 +27,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
         Object.assign(configAgent, agents);
         Object.assign(configAgent, agents);
       }
       }
     },
     },
-
-    event: async (input) => {
-      const { event } = input;
-      if (event.type === "session.created") {
-        console.log("[lite] Session created");
-      }
-    },
   };
   };
 };
 };
 
 
 export default OhMyOpenCodeLite;
 export default OhMyOpenCodeLite;
 
 
-export type { PluginConfig, AgentConfig, AgentName } from "./config";
+export type { PluginConfig, AgentOverrideConfig, AgentName } from "./config";

+ 11 - 7
src/tools/background.ts

@@ -1,5 +1,11 @@
 import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
 import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin";
 import type { BackgroundTaskManager } from "../features";
 import type { BackgroundTaskManager } from "../features";
+import {
+  POLL_INTERVAL_MS,
+  MAX_POLL_TIME_MS,
+  DEFAULT_TIMEOUT_MS,
+  STABLE_POLLS_THRESHOLD,
+} from "../config";
 
 
 const z = tool.schema;
 const z = tool.schema;
 
 
@@ -66,7 +72,7 @@ Use \`background_output\` with task_id="${task.id}" to get results.`;
     async execute(args) {
     async execute(args) {
       const taskId = String(args.task_id);
       const taskId = String(args.task_id);
       const block = args.block === true;
       const block = args.block === true;
-      const timeout = typeof args.timeout === "number" ? args.timeout : 120000;
+      const timeout = typeof args.timeout === "number" ? args.timeout : DEFAULT_TIMEOUT_MS;
 
 
       const task = await manager.getResult(taskId, block, timeout);
       const task = await manager.getResult(taskId, block, timeout);
       if (!task) {
       if (!task) {
@@ -174,13 +180,11 @@ session_id: ${sessionID}
 </task_metadata>`;
 </task_metadata>`;
   }
   }
 
 
-  const POLL_INTERVAL = 500;
-  const MAX_POLL_TIME = 5 * 60 * 1000;
   const pollStart = Date.now();
   const pollStart = Date.now();
   let lastMsgCount = 0;
   let lastMsgCount = 0;
   let stablePolls = 0;
   let stablePolls = 0;
 
 
-  while (Date.now() - pollStart < MAX_POLL_TIME) {
+  while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
     if (toolContext.abort?.aborted) {
     if (toolContext.abort?.aborted) {
       return `Task aborted.
       return `Task aborted.
 
 
@@ -189,7 +193,7 @@ session_id: ${sessionID}
 </task_metadata>`;
 </task_metadata>`;
     }
     }
 
 
-    await new Promise((r) => setTimeout(r, POLL_INTERVAL));
+    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
 
 
     const statusResult = await ctx.client.session.status();
     const statusResult = await ctx.client.session.status();
     const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
     const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
@@ -207,14 +211,14 @@ session_id: ${sessionID}
 
 
     if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
     if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
       stablePolls++;
       stablePolls++;
-      if (stablePolls >= 3) break;
+      if (stablePolls >= STABLE_POLLS_THRESHOLD) break;
     } else {
     } else {
       stablePolls = 0;
       stablePolls = 0;
       lastMsgCount = currentMsgCount;
       lastMsgCount = currentMsgCount;
     }
     }
   }
   }
 
 
-  if (Date.now() - pollStart >= MAX_POLL_TIME) {
+  if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
     return `Error: Agent timed out after 5 minutes.
     return `Error: Agent timed out after 5 minutes.
 
 
 <task_metadata>
 <task_metadata>

+ 1 - 0
src/utils/index.ts

@@ -0,0 +1 @@
+export * from "./polling";

+ 67 - 0
src/utils/polling.ts

@@ -0,0 +1,67 @@
+import {
+  POLL_INTERVAL_MS,
+  MAX_POLL_TIME_MS,
+  STABLE_POLLS_THRESHOLD,
+} from "../config";
+
+export interface PollOptions {
+  pollInterval?: number;
+  maxPollTime?: number;
+  stableThreshold?: number;
+  signal?: AbortSignal;
+}
+
+export interface PollResult<T> {
+  success: boolean;
+  data?: T;
+  timedOut?: boolean;
+  aborted?: boolean;
+}
+
+/**
+ * Generic polling utility that waits for a condition to be met.
+ * Returns when the condition is satisfied or timeout/abort occurs.
+ */
+export async function pollUntilStable<T>(
+  fetchFn: () => Promise<T>,
+  isStable: (current: T, previous: T | null, stableCount: number) => boolean,
+  opts: PollOptions = {}
+): Promise<PollResult<T>> {
+  const pollInterval = opts.pollInterval ?? POLL_INTERVAL_MS;
+  const maxPollTime = opts.maxPollTime ?? MAX_POLL_TIME_MS;
+  const stableThreshold = opts.stableThreshold ?? STABLE_POLLS_THRESHOLD;
+
+  const pollStart = Date.now();
+  let previousData: T | null = null;
+  let stablePolls = 0;
+
+  while (Date.now() - pollStart < maxPollTime) {
+    if (opts.signal?.aborted) {
+      return { success: false, aborted: true };
+    }
+
+    await new Promise((r) => setTimeout(r, pollInterval));
+
+    const currentData = await fetchFn();
+
+    if (isStable(currentData, previousData, stablePolls)) {
+      stablePolls++;
+      if (stablePolls >= stableThreshold) {
+        return { success: true, data: currentData };
+      }
+    } else {
+      stablePolls = 0;
+    }
+
+    previousData = currentData;
+  }
+
+  return { success: false, timedOut: true, data: previousData ?? undefined };
+}
+
+/**
+ * Simple delay utility
+ */
+export function delay(ms: number): Promise<void> {
+  return new Promise((r) => setTimeout(r, ms));
+}