diff --git a/.opencode/plans/1773495986494-brave-wizard.md b/.opencode/plans/1773495986494-brave-wizard.md deleted file mode 100644 index 46c4c7c..0000000 --- a/.opencode/plans/1773495986494-brave-wizard.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -model: minimax-m2.5-free ---- - -# Plan: Package multi-model tool as npm OpenCode Plugin - -## Overview - -Convert the existing `.opencode/tools/multi-model.ts` into a sharable npm package that users can install and configure. The tool will be packaged as an **OpenCode Plugin** that exposes the multi-model tool functionality. - -## Key Requirements - -1. **Package as OpenCode Plugin** - Users install via `"plugin"` field in config -2. **Custom npm scope** - Package name will be user-defined (e.g., `@username/opencode-multi-model`) -3. **Configurable binary** - Support both `opencode` and `kilo` binaries via environment variable/config - -## Architecture - -### Current State -- Tool lives in `.opencode/tools/multi-model.ts` -- Uses `@opencode-ai/plugin` to define custom tool -- Already has comprehensive tests in `.opencode/multi-model.test.ts` - -### Target State -``` -├── src/ -│ ├── index.ts # Plugin entry point -│ └── multi-model.ts # Tool definition (refactored) -├── tests/ -│ └── multi-model.test.ts # Tests (refactored from existing) -├── package.json # NPM package config -├── tsconfig.json # TypeScript config -├── README.md # Documentation -└── LICENSE # License -``` - -## Implementation Steps - -### 1. Create Plugin Structure - -Create new package with proper TypeScript configuration: - -```json -// package.json -{ - "name": "@username/opencode-multi-model", - "version": "1.0.0", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "dependencies": { - "@opencode-ai/plugin": "^1.2.26", - }, - "devDependencies": { - "bun-types": "^1.0.0", - "typescript": "^5.0.0" - } -} -``` - -### 2. Refactor Tool for Plugin Export - -The tool definition stays similar but needs env-based binary configuration: - -```typescript -// src/multi-model.ts -import { tool, type ToolContext } from "@opencode-ai/plugin" - -// Make binary name configurable via environment variable -function getBinaryName(context: ToolContext): string { - // Users set OPENCODE_MULTI_MODEL_BINARY=kilo or OPENCODE_MULTI_MODEL_BINARY=opencode - return process.env.OPENCODE_MULTI_MODEL_BINARY || "opencode" -} - -export const multiModelTool = tool({ - description: "Launch multiple OpenCode models in tmux", - args: { - sessionName: tool.schema.string().min(1).describe("tmux session name to create"), - models: tool.schema - .array(tool.schema.string().min(1)) - .min(1) - .describe("one or more OpenCode model ids to launch"), - }, - async execute(args, context) { - const binaryName = getBinaryName(context) - // ... use binaryName instead of hardcoded "opencode" - } -}) -``` - -### 3. Create Plugin Entry Point - -```typescript -// src/index.ts -import type { Plugin } from "@opencode-ai/plugin" -import { multiModelTool } from "./multi-model" - -export const OpenCodeMultiModelPlugin: Plugin = async (ctx) => { - return { - tool: { - "multi-model": multiModelTool, - }, - } -} - -export default OpenCodeMultiModelPlugin -export { multiModelTool } -``` - -### 4. Add Build Configuration - -```json -// tsconfig.json -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "declaration": true, - "outDir": "./dist", - "rootDir": "./src", - "skipLibCheck": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} -``` - -### 5. Add Build Script and Tests - -```json -// package.json scripts -{ - "scripts": { - "build": "bun build src/index.ts --outdir dist --target bun --format esm", - "test": "bun test", - "prepublishOnly": "bun run build" - } -} -``` - -**Tests**: Copy `.opencode/multi-model.test.ts` to `tests/multi-model.test.ts` and update imports: -- Change `import multiModelTool, { __testing_helpers } from "./tools/multi-model"` -- To `import multiModelTool, { __testing_helpers } from "../src/multi-model"` - -The existing tests should work with minimal changes since the tool API remains the same. - -### 6. Create README with Usage Instructions - -```markdown -# opencode-multi-model - -Plugin for OpenCode that enables launching multiple AI models in tmux sessions. - -## Installation - -Add to your OpenCode config (`opencode.json`): - -```json -{ - "plugin": ["@username/opencode-multi-model"] -} -``` - -## Usage - -``` -Use multi-model tool to launch multiple models: -sessionName: my-session, models: ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] -``` - -## Configuration - -### Custom Binary - -By default uses `opencode`. To use `kilo` instead, set environment variable: - -```json -{ - "plugin": ["@username/opencode-multi-model"], - "env": { - "OPENCODE_MULTI_MODEL_BINARY": "kilo" - } -} -``` - -## Requirements - -- tmux -- git -- opencode or kilo binary - - -## Files to Create/Modify - -### New Files -- `package.json` - NPM package configuration -- `tsconfig.json` - TypeScript configuration -- `src/index.ts` - Plugin entry point -- `src/multi-model.ts` - Refactored tool with configurable binary -- `tests/multi-model.test.ts` - Tests (copy from `.opencode/multi-model.test.ts`) -- `README.md` - Installation and usage docs -- `LICENSE` - License file - -### Delete -- Delete `.opencode/tools/multi-model.ts` after refactoring to new package -- Delete `.opencode/multi-model.test.ts` after copying to new package - - -## Verification - -1. **Build**: Run `bun run build` to compile TypeScript -2. **Test**: Run `bun test` in the new package directory -3. **Local Test**: - - Link package locally with `bun link` - - Add to test project config - - Verify tool works -4. **Publish**: Run `npm publish --access public` (or `bun publish`) - -## Usage for End Users - -After publishing, users will: - -1. Install package: -```bash -npm install -D @username/opencode-multi-model -``` - -2. Add to config: -```json -{ - "plugin": ["@username/opencode-multi-model"] -} -``` - -3. Use the tool: -``` -Use tool multi-model with sessionName "testing" with models opencode/mimo-v2-flash-free and opencode/minimax-m2.5-free -``` diff --git a/.opencode/plans/1773656570427-mighty-tiger.md b/.opencode/plans/1773656570427-mighty-tiger.md new file mode 100644 index 0000000..2108bec --- /dev/null +++ b/.opencode/plans/1773656570427-mighty-tiger.md @@ -0,0 +1,888 @@ +--- +model: ollama/kimi-k2.5:cloud +--- + +# Plan: Multi-format multi-model tool - OpenCode Plugin + CLI + Project Tool + +## Overview + +Convert the existing `.opencode/tools/multi-model.ts` into a sharable npm package that users can install and configure. The tool will support **three usage formats**: +1. **OpenCode Plugin** - Installed and loaded via plugin system +2. **Standalone CLI** - Direct command-line usage without OpenCode +3. **Project Tool** - Re-exported in `.opencode/tools/` for local testing + +## Key Requirements + +1. **Package as OpenCode Plugin** - Users install via `"plugin"` field in config +2. **Custom npm scope** - Package name will be user-defined (e.g., `@username/opencode-multi-model`) +3. **Configurable binary** - Support both `opencode` and `kilo` binaries via environment variable/config +4. **Standalone CLI** - Can be used directly via `npx @username/opencode-multi-model ...` +5. **Project-level tool** - Re-export tool in `.opencode/tools/multi-model.ts` for local development +6. **Shared core logic** - Single implementation across all three formats to avoid duplication + +## Architecture + +### Current State +- Tool lives in `.opencode/tools/multi-model.ts` +- Uses `@opencode-ai/plugin` to define custom tool +- Already has comprehensive tests in `.opencode/multi-model.test.ts` + +### Target State +``` +├── src/ +│ ├── index.ts # Plugin entry point - exports open and close tools +│ ├── cli.ts # CLI entry point with open/close subcommands +│ ├── core/ +│ │ ├── index.ts # Core exports (launchMultiModel, closeMultiModel) +│ │ ├── launch.ts # Launch session logic +│ │ ├── close.ts # Close session logic +│ │ └── utils.ts # Helper functions (shellQuote, levenshtein, etc.) +│ ├── tools/ +│ │ ├── open.ts # Open tool definition +│ │ └── close.ts # Close tool definition +│ └── types.ts # Shared TypeScript types +├── tests/ +│ └── multi-model.test.ts # Tests (refactored from existing) +├── package.json # NPM package config +├── tsconfig.json # TypeScript config +├── README.md # Documentation +└── LICENSE # License +``` + +## Implementation Steps + +### 1. Create Plugin Structure + +Create new package with proper TypeScript configuration: + +```json +// package.json +{ + "name": "@username/opencode-multi-model", + "version": "1.0.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "opencode-multi-model": "./dist/cli.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./core": { + "import": "./dist/core/index.js", + "types": "./dist/core/index.d.ts" + } + }, + "dependencies": { + "@opencode-ai/plugin": "^1.2.26", + "commander": "^12.0.0" + }, + "devDependencies": { + "bun-types": "^1.0.0", + "typescript": "^5.0.0" + } +} +``` + +### 2. Create Shared Types + +```typescript +// src/types.ts +export interface MultiModelOptions { + sessionName: string; + models: string[]; + binaryName?: string; // 'opencode' or 'kilo' +} + +export interface MultiModelResult { + success: boolean; + sessionName: string; + windows?: string[]; + error?: string; + instructions?: string; // Connection and cleanup instructions +} + +export interface CloseSessionOptions { + sessionName: string; + cleanupWorktrees?: boolean; // Remove worktrees and branches + force?: boolean; // Skip confirmation prompts +} + +export interface CloseSessionResult { + success: boolean; + sessionName: string; + error?: string; + cleanupPerformed?: boolean; + worktreesRemoved?: string[]; + branchesDeleted?: string[]; +} +``` + +### 3. Refactor Utils to Separate File + +```typescript +// src/core/utils.ts +// All helper functions extracted from current multi-model.ts + +export function runCommand(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { + // Implementation using Bun.$ +} + +export function shellQuote(str: string): string { + // POSIX-safe shell escaping +} + +export function normalizeModels(models: unknown): string[] { + // Input sanitization +} + +export function findDuplicates(arr: T[]): T[] { + // Detect duplicates +} + +export function createWindowBaseName(model: string): string { + // Generate tmux-safe window names +} + +export function createWindowPlans(models: string[]): Array<{ model: string; windowName: string }> { + // Create unique window plans with collision handling +} + +export function levenshtein(a: string, b: string): number { + // Edit distance calculation for fuzzy matching +} + +export function suggestModels(input: string, validModels: string[], maxSuggestions?: number): string[] { + // Fuzzy matching and suggestion limits +} + +export function formatInvalidModelError(invalidModels: string[], suggestions: Record): string { + // Error message formatting +} + +export function sanitizeName(name: string): string { + // Session name sanitization +} + +export function getWorktreePath(sessionName: string, windowName: string): string { + // Worktree path generation + const homedir = process.env.HOME || process.env.USERPROFILE || "/tmp"; + return `${homedir}/.local/share/opencode/multi-model/${sessionName}/${windowName}`; +} + +export interface WorktreeInfo { + path: string; + branch: string; +} + +export async function getWorktreesForSession(sessionName: string): Promise { + // Get all worktrees and filter for this session + const { stdout } = await runCommand("git worktree list --porcelain"); + const worktrees: WorktreeInfo[] = []; + + const homedir = process.env.HOME || process.env.USERPROFILE || "/tmp"; + const sessionPath = `${homedir}/.local/share/opencode/multi-model/${sessionName}`; + + let currentWorktree: Partial = {}; + + for (const line of stdout.split("\n")) { + if (line.startsWith("worktree ")) { + if (currentWorktree.path && currentWorktree.branch) { + worktrees.push(currentWorktree as WorktreeInfo); + } + currentWorktree = { + path: line.slice(9) + }; + } else if (line.startsWith("branch ")) { + currentWorktree.branch = line.slice(7); + } + } + + // Add last worktree + if (currentWorktree.path && currentWorktree.branch) { + worktrees.push(currentWorktree as WorktreeInfo); + } + + // Filter for session-specific worktrees + return worktrees.filter(wt => wt.path.startsWith(sessionPath)); +} + +export function undoWorktree(worktreePath: string): Promise { + // Cleanup helper for failed operations +} + +export function getBinaryName(context?: { config?: { multiModelBinary?: string } }): string { + // Priority: context config > env var > default + return context?.config?.multiModelBinary || + process.env.OPENCODE_MULTI_MODEL_BINARY || + "opencode"; +} +``` + +### 4. Create Core Launch Logic + +```typescript +// src/core/launch.ts +import type { MultiModelOptions, MultiModelResult } from "../types.js"; +import { + runCommand, shellQuote, normalizeModels, findDuplicates, + createWindowPlans, suggestModels, formatInvalidModelError, + sanitizeName, getWorktreePath, undoWorktree, getBinaryName +} from "./utils.js"; + +export async function launchMultiModel(options: MultiModelOptions): Promise { + // Core implementation from current multi-model.ts + // All the validation, tmux setup, worktree creation, etc. + + // Returns result with instructions for connecting and cleanup + return { + success: true, + sessionName: options.sessionName, + windows: [], // populated with actual window names + instructions: ` +Session "${options.sessionName}" created successfully! + +To attach to the session: + tmux attach -t ${options.sessionName} + +To list all windows: + tmux list-windows -t ${options.sessionName} + +To close the session and cleanup: + opencode-multi-model close ${options.sessionName} + +Or manually: + tmux kill-session -t ${options.sessionName} + rm -rf ~/.local/share/opencode/multi-model/${safeSessionName} && git worktree prune + git branch -D $(git branch --format='%(refname:short)' --list 'opencode/${safeSessionName}/*') +` + }; +} +``` + +### 5. Create Core Close Logic + +```typescript +// src/core/close.ts +import type { CloseSessionOptions, CloseSessionResult } from "../types.ts"; +import { runCommand, getWorktreePath, getWorktreesForSession, sanitizeName } from "./utils.ts"; +import * as readline from "readline"; + +async function promptUser(question: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} + +export async function closeMultiModel(options: CloseSessionOptions): Promise { + const worktreesRemoved: string[] = []; + const branchesDeleted: string[] = []; + + try { + // Check if session exists + const { exitCode } = await runCommand(`tmux has-session -t ${options.sessionName} 2>/dev/null`); + const sessionExists = exitCode === 0; + + // Get list of worktrees for this session before killing tmux + const worktrees = await getWorktreesForSession(options.sessionName); + + // If cleanup requested and not forced, ask for confirmation + if (options.cleanupWorktrees && !options.force && worktrees.length > 0) { + console.log(`\nThe following worktrees and branches will be removed:`); + worktrees.forEach(wt => console.log(` - ${wt.path} (branch: ${wt.branch})`)); + + const answer = await promptUser("\nDo you want to proceed? (y/N): "); + if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { + return { + success: false, + sessionName: options.sessionName, + error: "Cleanup cancelled by user" + }; + } + } + + // Kill tmux session if it exists + if (sessionExists) { + await runCommand(`tmux kill-session -t ${options.sessionName}`); + } + + let cleanupPerformed = false; + + // Cleanup worktrees and optionally branches + if (options.cleanup) { + for (const worktree of worktrees) { + try { + // Remove worktree + await runCommand(`git worktree remove -f ${worktree.path}`); + worktreesRemoved.push(worktree.path); + + await runCommand(`git branch -D ${worktree.branch}`); + branchesDeleted.push(worktree.branch); + } catch (err) { + console.warn(`Warning: Failed to cleanup worktree ${worktree.path}: ${err}`); + } + } + + // Also remove the base directory if empty + const worktreeBase = `~/.local/share/opencode/multi-model/${options.sessionName}`; + try { + await runCommand(`rmdir ${worktreeBase} 2>/dev/null || true`); + } catch { + // Ignore errors if directory not empty + } + + cleanupPerformed = worktreesRemoved.length > 0; + } + + return { + success: true, + sessionName: options.sessionName, + cleanupPerformed, + worktreesRemoved, + branchesDeleted + }; + } catch (error) { + return { + success: false, + sessionName: options.sessionName, + error: String(error), + worktreesRemoved, + branchesDeleted + }; + } +} +``` + +### 6. Create Core Index + +```typescript +// src/core/index.ts +export { launchMultiModel } from "./launch.ts"; +export { closeMultiModel } from "./close.ts"; +export * from "./utils.ts"; +export * from "../types.ts"; +``` + +### 7. Create Open Tool Definition + +```typescript +// src/tools/open.ts +import { tool, type ToolContext } from "@opencode-ai/plugin"; +import { launchMultiModel } from "../core/launch.ts"; +import { getBinaryName } from "../core/utils.ts"; + +export const openTool = tool({ + name: "multi-model-open", + description: "Launch multiple OpenCode models in tmux session", + args: { + sessionName: tool.schema.string().min(1).describe("tmux session name to create"), + models: tool.schema + .array(tool.schema.string().min(1)) + .min(1) + .describe("one or more OpenCode model ids to launch"), + }, + async execute(args, context: ToolContext) { + const binaryName = getBinaryName(context); + const result = await launchMultiModel({ + sessionName: args.sessionName, + models: args.models, + binaryName + }); + + if (!result.success) { + throw new Error(result.error); + } + + return { + content: result.instructions || `Created session "${result.sessionName}" with ${result.windows?.length || 0} windows` + }; + } +}); + +export default openTool; +``` + +### 8. Create Close Tool Definition + +```typescript +// src/tools/close.ts +import { tool, type ToolContext } from "@opencode-ai/plugin"; +import { closeMultiModel } from "../core/close.ts"; + +export const closeTool = tool({ + name: "multi-model-close", + description: "Close a multi-model tmux session and optionally cleanup worktrees and branches", + args: { + sessionName: tool.schema.string().min(1).describe("tmux session name to close"), + cleanupWorktrees: tool.schema.boolean().optional().describe("whether to remove worktrees and delete branches (default: true)"),(default: true)"), + }, + async execute(args, context: ToolContext) { + // In plugin mode, we skip confirmation (force=true) since there's no interactive terminal + const result = await closeMultiModel({ + sessionName: args.sessionName, + cleanupWorktrees: args.cleanupWorktrees, + force: true, // Skip confirmation in plugin mode + }); + + if (!result.success) { + throw new Error(result.error); + } + + let details = ""; + if (result.cleanupPerformed) { + details += ` Removed ${result.worktreesRemoved?.length || 0} worktrees.`; + } + + return { + content: `Session "${result.sessionName}" has been closed.${details}` + }; + } +}); + +export default closeTool; +``` + +### 9. Create Plugin Entry Point + +```typescript +// src/index.ts +import type { Plugin } from "@opencode-ai/plugin"; +import { openTool } from "./tools/open.ts"; +import { closeTool } from "./tools/close.ts"; + +export const OpenCodeMultiModelPlugin: Plugin = async (ctx) => { + return { + tool: { + "multi-model-open": openTool, + "multi-model-close": closeTool, + }, + }; +}; + +export default OpenCodeMultiModelPlugin; +export { openTool, closeTool }; +export * from "./core/index.ts"; +``` + +### 10. Create CLI Entry Point (with env support) + +```typescript +// src/cli.ts +#!/usr/bin/env node +import { Command } from "commander"; +import { launchMultiModel } from "./core/launch.js"; +import { closeMultiModel } from "./core/close.js"; +import { getBinaryName } from "./core/utils.js"; + +const program = new Command(); + +program + .name("opencode-multi-model") + .description("Launch multiple OpenCode models in tmux sessions") + .version("1.0.0"); + +// Open subcommand +program + .command("open") + .description("Create a new multi-model tmux session") + .argument("", "Name for the tmux session") + .option("-m, --models ", "Model IDs to launch (space-separated)", []) + .option("-b, --binary ", "Binary to use (opencode or kilo). Defaults to env var OPENCODE_MULTI_MODEL_BINARY or 'opencode'") + .action(async (sessionName, options) => { + try { + // CLI flag takes precedence, then env var via getBinaryName() + const binaryName = options.binary || getBinaryName(); + + const result = await launchMultiModel({ + sessionName, + models: options.models, + binaryName + }); + + if (result.success) { + console.log(result.instructions || `✓ Created session: ${result.sessionName}`); + } else { + console.error(`✗ Failed: ${result.error}`); + process.exit(1); + } + } catch (error) { + console.error(`✗ Error: ${error}`); + process.exit(1); + } + }); + +// Close subcommand +program + .command("close") + .description("Close a multi-model tmux session") + .argument("", "Name of the tmux session to close") + .option("-c, --cleanup", "Remove worktrees and delete branches", false) + .option("-f, --force", "Skip confirmation prompts", false) + .option("-k, --keep-branches", "Keep git branches (only remove worktrees)", false) + .action(async (sessionName, options) => { + try { + const result = await closeMultiModel({ + sessionName, + cleanup: options.cleanup, + force: options.force, + keepBranches: options.keepBranches + }); + + if (result.success) { + console.log(`✓ Closed session: ${result.sessionName}`); + if (result.cleanupPerformed) { + console.log(` Removed ${result.worktreesRemoved?.length || 0} worktrees`); + if (result.branchesDeleted && result.branchesDeleted.length > 0) { + console.log(` Deleted ${result.branchesDeleted.length} branches`); + } + } + } else { + console.error(`✗ Failed: ${result.error}`); + process.exit(1); + } + } catch (error) { + console.error(`✗ Error: ${error}`); + process.exit(1); + } + }); + +// Default to open command for backwards compatibility +program + .argument("[session-name]", "Name for the tmux session (deprecated, use 'open' command)") + .option("-m, --models ", "Model IDs to launch", []) + .option("-b, --binary ", "Binary to use") + .action(async (sessionName, options) => { + if (!sessionName) { + program.help(); + return; + } + + console.warn("Warning: Direct session name is deprecated. Use 'opencode-multi-model open ' instead."); + + const binaryName = options.binary || getBinaryName(); + + const result = await launchMultiModel({ + sessionName, + models: options.models, + binaryName + }); + + if (result.success) { + console.log(result.instructions); + } else { + console.error(`✗ Failed: ${result.error}`); + process.exit(1); + } + }); + +program.parse(); +``` + +### 11. Create Project-Level Tool Re-export + +```typescript +// .opencode/tools/multi-model.ts +// Re-export both open and close tools for project-level use + +import { openTool, closeTool } from "../../src/tools/open.js"; + +export { openTool, closeTool }; +export default openTool; // Default to open for backwards compatibility +``` + +### 12. Add Build Configuration + +```json +// tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### 13. Add Build Script and Tests + +```json +// package.json scripts +{ + "scripts": { + "build": "bun build src/index.ts src/cli.ts --outdir dist --target bun --format esm", + "test": "bun test", + "prepublishOnly": "bun run build" + } +} +``` + +**Tests**: Copy `.opencode/multi-model.test.ts` to `tests/multi-model.test.ts` and update imports: +- Change `import multiModelTool, { __testing_helpers } from "./tools/multi-model"` +- To `import { launchMultiModel, closeMultiModel, runCommand, ... } from "../src/core"` + +The existing tests should work with minimal changes since the core logic remains the same. + +### 14. Create README with Usage Instructions + +```markdown +# opencode-multi-model + +Launch multiple AI models in tmux sessions. Available as: +- OpenCode Plugin +- Standalone CLI +- Project-level tool + +## Installation + +### As OpenCode Plugin + +Add to your OpenCode config (`opencode.json`): + +```json +{ + "plugin": ["@username/opencode-multi-model"] +} +``` + +### As Standalone CLI + +```bash +# Using npx (no install) +npx @username/opencode-multi-model open my-session -m openai/gpt-4o anthropic/claude-3-5-sonnet + +# Or install globally +npm install -g @username/opencode-multi-model +opencode-multi-model open my-session -m openai/gpt-4o +``` + +### As Project Tool (for development) + +```typescript +// .opencode/tools/multi-model.ts +import { openTool, closeTool } from "@username/opencode-multi-model" +export { openTool, closeTool } +export default openTool +``` + +## Usage + +### Plugin Usage + +**Open a session:** +``` +Use multi-model-open tool: +sessionName: my-session, models: ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] +``` + +**Close a session:** +``` +Use multi-model-close tool: +sessionName: my-session, cleanup: true +``` + +### CLI Usage + +**Open a session:** +```bash +# Basic usage +opencode-multi-model open my-session -m openai/gpt-4o anthropic/claude-3-5-sonnet + +# With custom binary (via flag) +opencode-multi-model open my-session -m openai/gpt-4o -b kilo + +# With custom binary (via env) +export OPENCODE_MULTI_MODEL_BINARY=kilo +opencode-multi-model open my-session -m openai/gpt-4o +``` + +**Close a session:** +```bash +# Just close the tmux session +opencode-multi-model close my-session + +# Close and cleanup worktrees +opencode-multi-model close my-session --cleanup +``` + +**Help:** +```bash +opencode-multi-model --help +opencode-multi-model open --help +opencode-multi-model close --help +``` + +## Configuration + +### Custom Binary + +By default uses `opencode`. To use `kilo` instead: + +**Via environment variable:** +```bash +export OPENCODE_MULTI_MODEL_BINARY=kilo +``` + +**Via OpenCode config:** +```json +{ + "plugin": ["@username/opencode-multi-model"], + "multiModelBinary": "kilo" +} +``` + +**Via CLI flag:** +```bash +opencode-multi-model open my-session -m openai/gpt-4o -b kilo +``` + +Priority: CLI flag > OpenCode config > Environment variable > Default ("opencode") + +## Requirements + +- tmux +- git +- opencode or kilo binary + +## Development + +For local development and testing without publishing: + +```typescript +// .opencode/tools/multi-model.ts +import { openTool, closeTool } from "../../src/tools/open.js" +export { openTool, closeTool } +export default openTool +``` + +This allows testing the tool without publishing to npm. +``` + +## Files to Create/Modify + +### New Files +- `package.json` - NPM package configuration with CLI bin entry +- `tsconfig.json` - TypeScript configuration +- `src/types.ts` - Shared TypeScript types +- `src/index.ts` - Plugin entry point (exports open and close tools) +- `src/cli.ts` - CLI entry point with open/close subcommands +- `src/core/index.ts` - Core exports +- `src/core/utils.ts` - Helper functions +- `src/core/launch.ts` - Launch session logic +- `src/core/close.ts` - Close session logic +- `src/tools/open.ts` - Open tool definition +- `src/tools/close.ts` - Close tool definition +- `tests/multi-model.test.ts` - Tests (copy from `.opencode/multi-model.test.ts`) +- `README.md` - Installation and usage docs +- `LICENSE` - License file + +### Modified Files +- `.opencode/tools/multi-model.ts` - Re-export from package (for project-level use) + +### Delete (after migration) +- `.opencode/multi-model.test.ts` - After copying to new package + +## Verification + +1. **Build**: Run `bun run build` to compile TypeScript +2. **Test**: Run `bun test` in the new package directory +3. **CLI Test**: + ```bash + # Test open + node dist/cli.js open test-session -m openai/gpt-4o + + # Test close + node dist/cli.js close test-session + + # Test env var + OPENCODE_MULTI_MODEL_BINARY=kilo node dist/cli.js open test-session -m openai/gpt-4o + ``` +4. **Plugin Test**: + - Link package locally with `bun link` + - Add to test project config + - Verify both open and close tools work +5. **Project Tool Test**: + - Update `.opencode/tools/multi-model.ts` to re-export + - Run from project directory +6. **Publish**: Run `npm publish --access public` (or `bun publish`) + +## Usage Examples + +### End User - Plugin + +```json +// opencode.json +{ + "plugin": ["@username/opencode-multi-model"] +} +``` + +Then use via OpenCode interface with `multi-model-open` and `multi-model-close` tools. + +### End User - CLI + +```bash +# Install globally +npm install -g @username/opencode-multi-model + +# Open session +opencode-multi-model open compare-session -m openai/gpt-4o anthropic/claude-3-opus google/gemini-pro + +# Close session with cleanup +opencode-multi-model close compare-session --cleanup +``` + +### Developer - Project Tool + +```typescript +// .opencode/tools/multi-model.ts +// For testing local changes before publishing +import { openTool, closeTool } from "../../src/tools/open.js" +export { openTool, closeTool } +export default openTool +``` + +This allows testing the tool without publishing to npm. + +## Key Design Decisions + +### 1. Utils in Separate File +All helper functions (shellQuote, levenshtein, etc.) are in `src/core/utils.ts` for better organization and testability. + +### 2. CLI Honors Environment Variable +The CLI now properly checks `OPENCODE_MULTI_MODEL_BINARY` environment variable via the `getBinaryName()` function. Priority order: +1. CLI flag (`-b, --binary`) +2. OpenCode config (`multiModelBinary`) +3. Environment variable (`OPENCODE_MULTI_MODEL_BINARY`) +4. Default (`"opencode"`) + +### 3. Detailed Return Instructions +Both the tool and CLI return detailed instructions for: +- How to attach to the tmux session +- How to list windows +- How to close the session +- How to cleanup worktrees + +### 4. Open/Close Command Structure +The tool is split into two commands: +- `multi-model-open` (or `open` in CLI) - Creates sessions +- `multi-model-close` (or `close` in CLI) - Closes sessions + +This provides a cleaner API and makes it easier for users to manage sessions.