Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { resolveApiKey } from "./src/auth.js";
import { CommandCodeLanguageModel } from "./src/model.js";

// Re-export the plugin definition so a directory plugin entry (<dir>/index.ts)
// loads the dual V1+V2 plugin. No cycle: plugin.ts only imports from src/.
export { default, server } from "./plugin.js";

export interface CommandCodeProviderOptions {
name?: string;
apiKey?: string;
Expand Down
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,34 @@
"url": "git+https://github.com/BrainerVirus/opencode-commandcode.git"
},
"files": [
"index.ts",
"plugin.ts",
"dist/",
"models.json",
"manifest.json",
"_version.txt",
"README.md",
"CHANGELOG.md",
"index.ts",
"plugin.ts",
"src/"
],
"type": "module",
"main": "index.ts",
"types": "index.ts",
"main": "./dist/plugin.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./index.ts"
"types": "./dist/index.d.ts",
"import": "./dist/plugin.js"
},
"./server": {
"import": "./plugin.ts"
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "bun scripts/build-plugin.ts && bunx tsc -p tsconfig.json --emitDeclarationOnly",
"lint": "oxlint --deny-warnings src scripts plugin.ts index.ts tests",
"lint:fix": "oxlint --fix src scripts plugin.ts index.ts tests",
"format": "oxfmt src scripts plugin.ts index.ts tests package.json tsconfig.json release.config.cjs",
Expand Down
219 changes: 139 additions & 80 deletions plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readFileSync, existsSync } from "fs";
import { homedir } from "os";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { join } from "path";
import {
generateOpencodeModels,
loadCatalogFromLocalCommandCode,
Expand All @@ -14,11 +13,10 @@ import {
pluginStateDir,
} from "./src/startup.js";
import type { CatalogManifest } from "./src/manifest.js";
import { generateV2Models } from "./src/v2models.js";
import { catalogPaths } from "./src/paths.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const MODELS_PATH = join(__dirname, "models.json");
const VERSION_PATH = join(__dirname, "_version.txt");
const MANIFEST_PATH = join(__dirname, "manifest.json");
const { models: MODELS_PATH, manifest: MANIFEST_PATH, version: VERSION_PATH } = catalogPaths();

interface PluginFileConfig {
disableModelSync?: boolean;
Expand Down Expand Up @@ -72,7 +70,94 @@ function readBundledVersion(): string | null {
}
}

export default async function commandcodePlugin() {
type CatalogLoad = {
models: ModelEntry[];
catalogSource: "bundled" | "cache" | "opt-in-local";
commandCodeVersion: string | null;
degraded: boolean;
degradedReason: string | null;
};

function loadCatalogEntries(): CatalogLoad {
const pluginCfg = loadPluginConfig();
const override =
pluginCfg.commandCodePackagePath?.trim() || process.env.COMMANDCODE_PACKAGE_PATH?.trim() || "";

let models: ModelEntry[] = [];
let catalogSource: CatalogLoad["catalogSource"] = "bundled";
let commandCodeVersion: string | null = null;
let degraded = false;
let degradedReason: string | null = null;

if (override) {
const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
if (localCatalog && localCatalog.models.length > 0) {
models = localCatalog.models;
catalogSource = "opt-in-local";
commandCodeVersion = localCatalog.version;
}
}

if (models.length === 0) {
const bundled = loadBundledModels();
if (bundled) {
models = bundled;
catalogSource = "bundled";
commandCodeVersion = readBundledVersion();
const manifest = readBundledManifest();
if (manifest?.status === "degraded" || manifest?.status === "broken") {
degraded = true;
degradedReason =
manifest.status === "broken"
? "bundled catalog marked broken"
: "bundled catalog has models with no listed price";
}
} else {
const cached = readCatalogCache();
if (cached) {
models = cached;
catalogSource = "cache";
degraded = true;
degradedReason = "bundled models.json unreadable; using last-good cache";
} else {
degraded = true;
degradedReason = "no bundled catalog and no cache";
}
}
}

return { models, catalogSource, commandCodeVersion, degraded, degradedReason };
}

function persistCatalogSideEffects(load: CatalogLoad, debug: boolean): void {
if (load.models.length > 0) {
try {
writeCatalogCache(pluginStateDir(), load.models);
} catch {
// ignore cache write
}
}

const summary = {
catalogSource: load.catalogSource,
commandCodeVersion: load.commandCodeVersion,
modelCount: load.models.length,
reasoningModelCount: load.models.filter((m) => m.reasoning).length,
degraded: load.degraded,
degradedReason: load.degradedReason,
};
try {
writeStartupSummary(pluginStateDir(), summary);
} catch {
// ignore
}
if (debug) {
console.warn("[commandcode]", JSON.stringify(summary));
}
}

/** V1 entrypoint (OpenCode 1.18.29+ also accepts it via `server`). */
export async function server() {
return {
config: async (config: Record<string, unknown>) => {
if (!(config as Record<string, unknown>).provider) {
Expand All @@ -85,86 +170,16 @@ export default async function commandcodePlugin() {

const pluginCfg = loadPluginConfig();
const debug = pluginCfg.debugStartupLogs === true;
const override =
pluginCfg.commandCodePackagePath?.trim() ||
process.env.COMMANDCODE_PACKAGE_PATH?.trim() ||
"";

if (!cc.npm) cc.npm = "commandcode-go-opencode-provider";
if (!cc.name) cc.name = "Command Code";
if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"];

if (cc.models) return;

let models: ModelEntry[] = [];
let catalogSource: "bundled" | "cache" | "opt-in-local" = "bundled";
let commandCodeVersion: string | null = null;
let degraded = false;
let degradedReason: string | null = null;

if (override) {
const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
if (localCatalog && localCatalog.models.length > 0) {
models = localCatalog.models;
catalogSource = "opt-in-local";
commandCodeVersion = localCatalog.version;
}
}

if (models.length === 0) {
const bundled = loadBundledModels();
if (bundled) {
models = bundled;
catalogSource = "bundled";
commandCodeVersion = readBundledVersion();
const manifest = readBundledManifest();
if (manifest?.status === "degraded" || manifest?.status === "broken") {
degraded = true;
degradedReason =
manifest.status === "broken"
? "bundled catalog marked broken"
: "bundled catalog has models with no listed price";
}
} else {
const cached = readCatalogCache();
if (cached) {
models = cached;
catalogSource = "cache";
degraded = true;
degradedReason = "bundled models.json unreadable; using last-good cache";
} else {
degraded = true;
degradedReason = "no bundled catalog and no cache";
}
}
}

if (models.length > 0) {
try {
writeCatalogCache(pluginStateDir(), models);
} catch {
// ignore cache write
}
}

cc.models = generateOpencodeModels(models);

const summary = {
catalogSource,
commandCodeVersion,
modelCount: models.length,
reasoningModelCount: models.filter((m) => m.reasoning).length,
degraded,
degradedReason,
};
try {
writeStartupSummary(pluginStateDir(), summary);
} catch {
// ignore
}
if (debug) {
console.warn("[commandcode]", JSON.stringify(summary));
}
const load = loadCatalogEntries();
persistCatalogSideEffects(load, debug);
cc.models = generateOpencodeModels(load.models);
},

auth: {
Expand Down Expand Up @@ -195,3 +210,47 @@ export default async function commandcodePlugin() {
},
};
}

/** V2 setup: provider inventory + models through transforms. Auth stays V1-only for now. */
async function setup(ctx: any): Promise<void> {
const pluginCfg = loadPluginConfig();
const debug = pluginCfg.debugStartupLogs === true;
const load = loadCatalogEntries();
persistCatalogSideEffects(load, debug);
const v2models = generateV2Models(load.models);

await ctx.provider.transform((editor: any) => {
const existing = editor.get("commandcode");
if (!existing) {
editor.add({
info: {
id: "commandcode",
name: "Command Code",
activation: "enabled",
package: "aisdk:@ai-sdk/openai-compatible",
settings: {
baseURL: "https://api.commandcode.ai/provider/v1",
apiKey: "{env:COMMANDCODE_API_KEY}",
},
},
models: v2models,
});
return;
}
editor.update("commandcode", (provider: any) => {
provider.name = provider.name ?? "Command Code";
provider.settings = provider.settings ?? {};
if (typeof provider.settings === "object") {
if (!provider.settings.baseURL)
provider.settings.baseURL = "https://api.commandcode.ai/provider/v1";
if (!provider.settings.apiKey) provider.settings.apiKey = "{env:COMMANDCODE_API_KEY}";
}
});
editor.models.set("commandcode", v2models);
});
}

const definition = { id: "commandcode", setup };

// Dual entry: V2 reads default.id/setup, V1 (>=1.18.29) calls default.server().
export default { ...definition, server };
35 changes: 35 additions & 0 deletions scripts/build-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bun
// Build the self-contained OpenCode plugin entry (dist/plugin.js).
// Bundles src/entry.ts (plugin default + V1 server + AI-SDK factory) for
// node ESM; the `ai` package stays external and node builtins stay external
// via --target node. All other imports are type-only (erased at build).
import { spawnSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import { join } from "node:path";

const ROOT = join(import.meta.dir, "..");

mkdirSync(join(ROOT, "dist"), { recursive: true });

const build = spawnSync(
"bun",
[
"build",
join(ROOT, "src/entry.ts"),
"--outfile",
join(ROOT, "dist/plugin.js"),
"--target",
"node",
"--format",
"esm",
"--external",
"ai",
],
{ encoding: "utf8", cwd: ROOT },
);
if (build.stdout) process.stdout.write(build.stdout);
if (build.stderr) process.stderr.write(build.stderr);
if (build.status !== 0) {
throw new Error(`bun build src/entry.ts failed (exit ${build.status ?? "unknown"})`);
}
console.log("opencode: built dist/plugin.js");
6 changes: 3 additions & 3 deletions scripts/catalog-release-notes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bun
import { readFileSync } from "fs";
import { join } from "path";
import { catalogReleaseNotesFromFiles } from "../src/catalog-release-notes.js";
import type { CatalogManifest } from "../src/manifest.js";
import type { PricedModel } from "../src/catalog-release-notes.js";
import { catalogReleaseNotesFromFiles } from "@/src/catalog-release-notes.js";
import type { CatalogManifest } from "@/src/manifest.js";
import type { PricedModel } from "@/src/catalog-release-notes.js";

const ROOT = join(import.meta.dir, "..");
const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf-8")) as CatalogManifest;
Expand Down
4 changes: 2 additions & 2 deletions scripts/catalog-sync-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { execSync } from "child_process";
import { catalogBreakTitle, renderCatalogBreakBody } from "../src/catalog-break.js";
import { decideCatalogSync } from "../src/publish-policy.js";
import { catalogBreakTitle, renderCatalogBreakBody } from "@/src/catalog-break.js";
import { decideCatalogSync } from "@/src/publish-policy.js";
import { npmLatestVersion, npmPackageVersions } from "./npm-registry.js";

const ROOT = join(import.meta.dir, "..");
Expand Down
2 changes: 1 addition & 1 deletion scripts/cut-changelog.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bun
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { cutKeepAChangelog } from "../src/cut-changelog.ts";
import { cutKeepAChangelog } from "@/src/cut-changelog.ts";

const version = process.argv[2];
const date = process.argv[3] ?? new Date().toISOString().slice(0, 10);
Expand Down
2 changes: 1 addition & 1 deletion scripts/prepare-release-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// untouched; the post-release sync PR commits the stamped versions to main.
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { withPluginVersion, writeManifest, type CatalogManifest } from "../src/manifest.js";
import { withPluginVersion, writeManifest, type CatalogManifest } from "@/src/manifest.js";

const ROOT = join(import.meta.dir, "..");

Expand Down
Loading
Loading