mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
Impl: tool-npm-package
This commit is contained in:
@@ -0,0 +1,907 @@
|
||||
---
|
||||
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[];
|
||||
tagsCreated?: string[];
|
||||
warnings?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Refactor Utils to Separate File
|
||||
|
||||
```typescript
|
||||
// src/core/utils.ts
|
||||
// All helper functions extracted from current multi-model.ts
|
||||
|
||||
export type CommandResult = {
|
||||
ok: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
export async function runCommand(parts: string[]): Promise<CommandResult> {
|
||||
// Implementation using Bun.$
|
||||
}
|
||||
|
||||
export function shellQuote(str: string): string {
|
||||
// POSIX-safe shell escaping
|
||||
}
|
||||
|
||||
export function normalizeModels(models: unknown): string[] {
|
||||
// Input sanitization
|
||||
}
|
||||
|
||||
export function findDuplicates<T>(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, string[]>): string {
|
||||
// Error message formatting
|
||||
}
|
||||
|
||||
export function sanitizeName(name: string): string {
|
||||
// Session name sanitization
|
||||
}
|
||||
|
||||
export function getSessionPath(sessionName: string): string {
|
||||
const homedir = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
||||
return `${homedir}/.local/share/opencode/multi-model/${sessionName}`;
|
||||
}
|
||||
|
||||
export function getWorktreePath(sessionName: string, windowName: string): string {
|
||||
// Worktree path generation
|
||||
return `${getSessionPath(sessionName)}/${windowName}`;
|
||||
}
|
||||
|
||||
export interface WorktreeInfo {
|
||||
path: string;
|
||||
branch: string;
|
||||
}
|
||||
|
||||
export async function getWorktreesForSession(sessionName: string): Promise<WorktreeInfo[]> {
|
||||
// Get all worktrees and filter for this session
|
||||
const { stdout } = await runCommand(["git", "worktree", "list", "--porcelain"]);
|
||||
const worktrees: WorktreeInfo[] = [];
|
||||
|
||||
const sessionPath = getSessionPath(sessionName);
|
||||
|
||||
let currentWorktree: Partial<WorktreeInfo> = {};
|
||||
|
||||
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<void> {
|
||||
// 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";
|
||||
import {
|
||||
runCommand, shellQuote, normalizeModels, findDuplicates,
|
||||
createWindowPlans, suggestModels, formatInvalidModelError,
|
||||
sanitizeName, getWorktreePath, getSessionPath, undoWorktree, getBinaryName
|
||||
} from "./utils";
|
||||
|
||||
export async function launchMultiModel(options: MultiModelOptions): Promise<MultiModelResult> {
|
||||
// Core implementation from current multi-model.ts
|
||||
// All the validation, tmux setup, worktree creation, etc.
|
||||
const safeSessionName = sanitizeName(options.sessionName);
|
||||
|
||||
// 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 ${getSessionPath(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";
|
||||
import { runCommand, getSessionPath, getWorktreesForSession, sanitizeName } from "./utils";
|
||||
import * as readline from "readline";
|
||||
|
||||
async function promptUser(question: string): Promise<string> {
|
||||
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<CloseSessionResult> {
|
||||
const worktreesRemoved: string[] = [];
|
||||
const branchesDeleted: string[] = [];
|
||||
const tagsCreated: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
// Check if session exists
|
||||
const { ok: sessionExists } = await runCommand(["tmux", "has-session", "-t", options.sessionName]);
|
||||
|
||||
// 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.cleanupWorktrees) {
|
||||
for (const worktree of worktrees) {
|
||||
try {
|
||||
// Remove worktree
|
||||
await runCommand(["git", "worktree", "remove", "-f", worktree.path]);
|
||||
worktreesRemoved.push(worktree.path);
|
||||
|
||||
// Create an archive tag before deleting the branch for safe recovery
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const archiveTag = `archive/${worktree.branch}-${timestamp}`;
|
||||
const tagResult = await runCommand(["git", "tag", archiveTag, worktree.branch]);
|
||||
|
||||
if (tagResult.ok) {
|
||||
tagsCreated.push(archiveTag);
|
||||
} else {
|
||||
const warningMsg = `Failed to create tag ${archiveTag} for branch ${worktree.branch}.`;
|
||||
warnings.push(warningMsg);
|
||||
console.warn(`Warning: ${warningMsg}`);
|
||||
}
|
||||
|
||||
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 = getSessionPath(options.sessionName);
|
||||
try {
|
||||
await runCommand(["rmdir", worktreeBase]);
|
||||
} catch {
|
||||
// Ignore errors if directory not empty
|
||||
}
|
||||
|
||||
cleanupPerformed = worktreesRemoved.length > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionName: options.sessionName,
|
||||
cleanupPerformed,
|
||||
worktreesRemoved,
|
||||
branchesDeleted,
|
||||
tagsCreated,
|
||||
warnings
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
sessionName: options.sessionName,
|
||||
error: String(error),
|
||||
worktreesRemoved,
|
||||
branchesDeleted,
|
||||
tagsCreated,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Create Core Index
|
||||
|
||||
```typescript
|
||||
// src/core/index.ts
|
||||
export { launchMultiModel } from "./launch";
|
||||
export { closeMultiModel } from "./close";
|
||||
export * from "./utils";
|
||||
export * from "../types";
|
||||
```
|
||||
|
||||
### 7. Create Open Tool Definition
|
||||
|
||||
```typescript
|
||||
// src/tools/open.ts
|
||||
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
||||
import { launchMultiModel } from "../core/launch";
|
||||
import { getBinaryName } from "../core/utils";
|
||||
|
||||
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`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 8. Create Close Tool Definition
|
||||
|
||||
```typescript
|
||||
// src/tools/close.ts
|
||||
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
||||
import { closeMultiModel } from "../core/close";
|
||||
|
||||
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().default(true).describe("whether to remove worktrees and delete branches (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.`;
|
||||
if (result.tagsCreated && result.tagsCreated.length > 0) {
|
||||
details += ` Created backup tags: ${result.tagsCreated.join(", ")}.`;
|
||||
}
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
details += ` Warnings: ${result.warnings.join("; ")}.`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: `Session "${result.sessionName}" has been closed.${details}`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 9. Create Plugin Entry Point
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import type { Plugin } from "@opencode-ai/plugin";
|
||||
import { openTool } from "./tools/open";
|
||||
import { closeTool } from "./tools/close";
|
||||
|
||||
export const OpenCodeMultiModelPlugin: Plugin = async (ctx) => {
|
||||
return {
|
||||
tool: {
|
||||
"multi-model-open": openTool,
|
||||
"multi-model-close": closeTool,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { openTool, closeTool };
|
||||
export * from "./core/index";
|
||||
```
|
||||
|
||||
### 10. Create CLI Entry Point (with env support)
|
||||
|
||||
```typescript
|
||||
// src/cli.ts
|
||||
#!/usr/bin/env bun
|
||||
import { Command } from "commander";
|
||||
import { launchMultiModel } from "./core/launch";
|
||||
import { closeMultiModel } from "./core/close";
|
||||
import { getBinaryName } from "./core/utils";
|
||||
|
||||
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("<session-name>", "Name for the tmux session")
|
||||
.option("-m, --models <models...>", "Model IDs to launch (space-separated)", [])
|
||||
.option("-b, --binary <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("<session-name>", "Name of the tmux session to close")
|
||||
.option("-c, --cleanup-worktrees", "Remove worktrees and delete branches", true)
|
||||
.option("-f, --force", "Skip confirmation prompts", false)
|
||||
.action(async (sessionName, options) => {
|
||||
try {
|
||||
const result = await closeMultiModel({
|
||||
sessionName,
|
||||
cleanupWorktrees: options.cleanupWorktrees,
|
||||
force: options.force,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✓ Closed session: ${result.sessionName}`);
|
||||
if (result.cleanupPerformed) {
|
||||
console.log(` Removed ${result.worktreesRemoved?.length || 0} worktrees`);
|
||||
if (result.tagsCreated && result.tagsCreated.length > 0) {
|
||||
console.log(` Created backup tags: ${result.tagsCreated.join(", ")}`);
|
||||
}
|
||||
if (result.warnings && result.warnings.length > 0) {
|
||||
console.log(` Warnings: ${result.warnings.join("; ")}`);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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 } from "../../src/tools/open";
|
||||
import { closeTool } from "../../src/tools/close";
|
||||
|
||||
export { openTool, closeTool };
|
||||
```
|
||||
|
||||
### 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 --experimental-dts",
|
||||
"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 } from "../src/core"` and `import { runCommand, ... } from "../src/core/utils"`
|
||||
|
||||
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
|
||||
|
||||
If you installed it via NPM and want to use it as a project tool, import it from the package:
|
||||
|
||||
```typescript
|
||||
// .opencode/tools/multi-model.ts
|
||||
import { openTool, closeTool } from "@username/opencode-multi-model"
|
||||
export { openTool, closeTool }
|
||||
```
|
||||
|
||||
If you are developing this package locally and want to test it in a project without publishing, import via relative paths:
|
||||
|
||||
```typescript
|
||||
// .opencode/tools/multi-model.ts
|
||||
import { openTool } from "../../src/tools/open"
|
||||
import { closeTool } from "../../src/tools/close"
|
||||
export { openTool, closeTool }
|
||||
```
|
||||
|
||||
## 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, cleanupWorktrees: 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-worktrees
|
||||
```
|
||||
|
||||
**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 } from "../../src/tools/open"
|
||||
import { closeTool } from "../../src/tools/close"
|
||||
export { openTool, closeTool }
|
||||
```
|
||||
|
||||
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
|
||||
bun dist/cli.js open test-session -m openai/gpt-4o
|
||||
|
||||
# Test close
|
||||
bun dist/cli.js close test-session
|
||||
|
||||
# Test env var
|
||||
OPENCODE_MULTI_MODEL_BINARY=kilo bun 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-worktrees
|
||||
```
|
||||
|
||||
### Developer - Project Tool
|
||||
|
||||
```typescript
|
||||
// .opencode/tools/multi-model.ts
|
||||
// For testing local changes before publishing
|
||||
import { openTool } from "../../src/tools/open"
|
||||
import { closeTool } from "../../src/tools/close"
|
||||
export { openTool, closeTool }
|
||||
```
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user