diff --git a/models.json b/models.json index db50960..32929fa 100644 --- a/models.json +++ b/models.json @@ -336,5 +336,114 @@ "context": 1000000, "output": 131072 } + }, + { + "id": "MiniMaxAI/MiniMax-M3", + "name": "MiniMax M3", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.3, + "output": 1.2, + "cache_read": 0.06 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "moonshotai/Kimi-K2.7-Code", + "name": "Kimi K2.7 Code", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.95, + "output": 4, + "cache_read": 0.16 + }, + "limit": { + "context": 256000, + "output": 131072 + } + }, + { + "id": "Qwen/Qwen3.7-Plus", + "name": "Qwen 3.7 Plus", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.5, + "output": 3, + "cache_read": 0.1 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "stepfun/Step-3.7-Flash", + "name": "Step 3.7 Flash", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.1, + "output": 0.3, + "cache_read": 0.02 + }, + "limit": { + "context": 256000, + "output": 131072 + } + }, + { + "id": "nvidia/nemotron-3-ultra-550b-a55b", + "name": "Nemotron 3 Ultra", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.35, + "output": 1.4 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "xiaomi/mimo-v2.5", + "name": "MiMo V2.5", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.4, + "output": 1.6 + }, + "limit": { + "context": 1000000, + "output": 131072 + } + }, + { + "id": "xiaomi/mimo-v2.5-pro", + "name": "MiMo V2.5 Pro", + "tier": "open-source", + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0.8, + "output": 3.2 + }, + "limit": { + "context": 1000000, + "output": 131072 + } } ] diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000..94cd06c --- /dev/null +++ b/src/context.ts @@ -0,0 +1,152 @@ +import { readdirSync, statSync, readFileSync, existsSync } from "fs" +import { join, relative } from "path" +import { execSync } from "child_process" + +interface GitContext { + isGitRepo: boolean + currentBranch: string + mainBranch: string + gitStatus: string + recentCommits: string[] +} + +export interface ProjectContext { + structure: string[] + git: GitContext +} + +const DEFAULT_SKIP = new Set([ + "node_modules", + ".git", + "dist", + "build", + ".next", + ".turbo", + "coverage", + ".nyc_output", + "tmp", + "temp", + ".cache", + ".DS_Store", + "__pycache__", + ".venv", + "venv", + ".idea", + ".vscode", + "target", + "out", +]) + +const MAX_FILES = 200 +const MAX_DEPTH = 5 + +function parseGitignore(rootDir: string): Set { + const p = join(rootDir, ".gitignore") + if (!existsSync(p)) return new Set() + try { + const patterns = new Set() + for (const line of readFileSync(p, "utf-8").split("\n")) { + const t = line.trim() + if (t && !t.startsWith("#")) patterns.add(t) + } + return patterns + } catch { + return new Set() + } +} + +function shouldSkip(name: string, relPath: string, gitignore: Set): boolean { + if (DEFAULT_SKIP.has(name)) return true + for (const pat of gitignore) { + const p = pat.endsWith("/") ? pat.slice(0, -1) : pat + if (p === relPath || relPath.startsWith(p + "/") || p === name) return true + if (pat.includes("*")) { + const re = new RegExp("^" + pat.replace(/\./g, "\\.").replace(/\*/g, "[^/]*") + "$") + if (re.test(name)) return true + } + } + return false +} + +function gatherStructure(rootDir: string): string[] { + const entries: string[] = [] + const gitignore = parseGitignore(rootDir) + + function walk(dir: string, depth: number) { + if (entries.length >= MAX_FILES || depth > MAX_DEPTH) return + + let items: string[] + try { + items = readdirSync(dir) + } catch { + return + } + + for (const name of items.sort()) { + const fullPath = join(dir, name) + const relPath = relative(rootDir, fullPath) + if (shouldSkip(name, relPath, gitignore)) continue + + let st + try { + st = statSync(fullPath) + } catch { + continue + } + + if (st.isDirectory()) { + entries.push(relPath + "/") + walk(fullPath, depth + 1) + } else if (st.isFile()) { + entries.push(relPath) + } + } + } + + walk(rootDir, 0) + return entries +} + +function runGit(cmd: string, cwd: string): string { + try { + return execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe", maxBuffer: 1024 * 1024 }).trim() + } catch { + return "" + } +} + +function gatherGit(rootDir: string): GitContext { + try { + execSync("git rev-parse --is-inside-work-tree", { cwd: rootDir, stdio: "pipe" }) + } catch { + return { isGitRepo: false, currentBranch: "", mainBranch: "", gitStatus: "", recentCommits: [] } + } + + const currentBranch = runGit("git branch --show-current", rootDir) + + let mainBranch = "" + for (const cand of ["main", "master"]) { + try { + execSync(`git rev-parse --verify ${cand}`, { cwd: rootDir, stdio: "pipe" }) + mainBranch = cand + break + } catch { /* not found */ } + } + + const gitStatus = runGit("git status --short", rootDir) + + let recentCommits: string[] = [] + const log = runGit('git log --oneline -10 --format="%h %s (%an, %ad)" --date=short', rootDir) + if (log) { + recentCommits = log.split("\n") + } + + return { isGitRepo: true, currentBranch, mainBranch, gitStatus, recentCommits } +} + +export function gatherContext(rootDir: string = process.cwd()): ProjectContext { + return { + structure: gatherStructure(rootDir), + git: gatherGit(rootDir), + } +} diff --git a/src/convert.ts b/src/convert.ts index aa4e3e6..442f36e 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -8,6 +8,7 @@ import type { LanguageModelV3ToolResultPart, LanguageModelV3ToolResultOutput, } from "@ai-sdk/provider" +import type { ProjectContext } from "./context.js" type CCMessage = | { role: "user"; content: string | unknown[] } @@ -174,6 +175,7 @@ function convertTools( export function buildRequest( modelId: string, options: LanguageModelV3CallOptions, + context?: ProjectContext, ): CCRequestEnvelope { let systemPrompt = "" const messages: CCMessage[] = [] @@ -200,21 +202,20 @@ export function buildRequest( if (options.topP !== undefined) params.top_p = options.topP if (options.topK !== undefined) params.top_k = options.topK + const ctx = context return { config: { workingDir: process.cwd() ?? "/", date: new Date().toISOString().split("T")[0] ?? "", environment: `${process.platform}-${process.arch}`, - // Stub: opencode does not expose project structure context - structure: [], - isGitRepo: false, - currentBranch: "", - mainBranch: "", - gitStatus: "", - recentCommits: [], + structure: ctx?.structure ?? [], + isGitRepo: ctx?.git.isGitRepo ?? false, + currentBranch: ctx?.git.currentBranch ?? "", + mainBranch: ctx?.git.mainBranch ?? "", + gitStatus: ctx?.git.gitStatus ?? "", + recentCommits: ctx?.git.recentCommits ?? [], }, memory: "", - // Stub: taste/memory/permissionMode are Command Code CLI features not exposed via provider API taste: "", skills: null, permissionMode: "standard", diff --git a/src/model.ts b/src/model.ts index 8d0cbe7..1b1bfa7 100644 --- a/src/model.ts +++ b/src/model.ts @@ -9,6 +9,7 @@ import type { } from "@ai-sdk/provider" import { buildRequest } from "./convert.js" import { parseStreamEvents } from "./stream.js" +import { gatherContext } from "./context.js" const DEFAULT_BASE_URL = "https://api.commandcode.ai" // x-command-code-version must match the Command Code CLI version for API compatibility @@ -49,7 +50,8 @@ export class CommandCodeLanguageModel implements LanguageModelV3 { } async doStream(options: LanguageModelV3CallOptions): Promise { - const body = buildRequest(this.modelId, options) + const context = gatherContext() + const body = buildRequest(this.modelId, options, context) const requestBody = JSON.stringify(body) const controller = new AbortController()