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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,36 @@ Schema autocomplete supported:
}
```

### JSONC Support

The `oh-my-opencode` configuration file supports JSONC (JSON with Comments):
- Line comments: `// comment`
- Block comments: `/* comment */`
- Trailing commas: `{ "key": "value", }`

When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc` takes priority.

**Example with comments:**

```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",

// Enable Google Gemini via Antigravity OAuth
"google_auth": false,

/* Agent overrides - customize models for specific tasks */
"agents": {
"oracle": {
"model": "openai/gpt-5.2" // GPT for strategic reasoning
},
"explore": {
"model": "opencode/grok-code" // Free & fast for exploration
},
},
}
```

### Google Auth

**Recommended**: Use the external [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin. It provides multi-account load balancing, more models (including Claude via Antigravity), and active maintenance. See [Installation > Google Gemini](#google-gemini-antigravity-oauth).
Expand Down
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@opencode-ai/sdk": "^1.0.162",
"commander": "^14.0.2",
"hono": "^4.10.4",
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"xdg-basedir": "^5.1.0",
Expand Down Expand Up @@ -110,6 +111,8 @@

"jose": ["[email protected]", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],

"jsonc-parser": ["[email protected]", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],

"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],

"picomatch": ["[email protected]", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@opencode-ai/sdk": "^1.0.162",
"commander": "^14.0.2",
"hono": "^4.10.4",
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"xdg-basedir": "^5.1.0",
Expand Down
78 changes: 4 additions & 74 deletions src/cli/config-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import { parseJsonc } from "../shared"
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"

const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
Expand Down Expand Up @@ -39,80 +40,10 @@ export function detectConfigFormat(): { format: ConfigFormat; path: string } {
return { format: "none", path: OPENCODE_JSON }
}

function stripJsoncComments(content: string): string {
let result = ""
let i = 0
let inString = false
let escape = false

while (i < content.length) {
const char = content[i]

if (escape) {
result += char
escape = false
i++
continue
}

if (char === "\\") {
result += char
escape = true
i++
continue
}

if (char === '"' && !inString) {
inString = true
result += char
i++
continue
}

if (char === '"' && inString) {
inString = false
result += char
i++
continue
}

if (inString) {
result += char
i++
continue
}

// Outside string - check for comments
if (char === "/" && content[i + 1] === "/") {
// Line comment - skip to end of line
while (i < content.length && content[i] !== "\n") {
i++
}
continue
}

if (char === "/" && content[i + 1] === "*") {
// Block comment - skip to */
i += 2
while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) {
i++
}
i += 2
continue
}

result += char
i++
}

return result.replace(/,(\s*[}\]])/g, "$1")
}

function parseConfig(path: string, isJsonc: boolean): OpenCodeConfig | null {
try {
const content = readFileSync(path, "utf-8")
const cleaned = isJsonc ? stripJsoncComments(content) : content
return JSON.parse(cleaned) as OpenCodeConfig
return parseJsonc<OpenCodeConfig>(content)
} catch {
return null
}
Expand Down Expand Up @@ -252,8 +183,7 @@ export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult

if (existsSync(OMO_CONFIG)) {
const content = readFileSync(OMO_CONFIG, "utf-8")
const cleaned = stripJsoncComments(content)
const existing = JSON.parse(cleaned) as Record<string, unknown>
const existing = parseJsonc<Record<string, unknown>>(content)
delete existing.agents
const merged = deepMerge(existing, newConfig)
writeFileSync(OMO_CONFIG, JSON.stringify(merged, null, 2) + "\n")
Expand Down Expand Up @@ -484,7 +414,7 @@ export function detectCurrentConfig(): DetectedConfig {

try {
const content = readFileSync(OMO_CONFIG, "utf-8")
const omoConfig = JSON.parse(stripJsoncComments(content)) as OmoConfigData
const omoConfig = parseJsonc<OmoConfigData>(content)

const agents = omoConfig.agents ?? {}

Expand Down
26 changes: 11 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { builtinTools, createCallOmoAgent, createBackgroundTools, createLookAt,
import { BackgroundManager } from "./features/background-agent";
import { createBuiltinMcps } from "./mcp";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
import { log, deepMerge, getUserConfigDir, addConfigLoadError } from "./shared";
import { log, deepMerge, getUserConfigDir, addConfigLoadError, parseJsonc, detectConfigFile } from "./shared";
import { PLAN_SYSTEM_PROMPT, PLAN_PERMISSION } from "./agents/plan-prompt";
import * as fs from "fs";
import * as path from "path";
Expand Down Expand Up @@ -119,7 +119,7 @@ function loadConfigFromPath(configPath: string, ctx: any): OhMyOpenCodeConfig |
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, "utf-8");
const rawConfig = JSON.parse(content);
const rawConfig = parseJsonc<Record<string, unknown>>(content);

migrateConfigFile(configPath, rawConfig);

Expand Down Expand Up @@ -201,19 +201,15 @@ function mergeConfigs(
}

function loadPluginConfig(directory: string, ctx: any): OhMyOpenCodeConfig {
// User-level config path (OS-specific)
const userConfigPath = path.join(
getUserConfigDir(),
"opencode",
"oh-my-opencode.json"
);

// Project-level config path
const projectConfigPath = path.join(
directory,
".opencode",
"oh-my-opencode.json"
);
// User-level config path (OS-specific) - prefer .jsonc over .json
const userBasePath = path.join(getUserConfigDir(), "opencode", "oh-my-opencode");
const userDetected = detectConfigFile(userBasePath);
const userConfigPath = userDetected.format !== "none" ? userDetected.path : userBasePath + ".json";

// Project-level config path - prefer .jsonc over .json
const projectBasePath = path.join(directory, ".opencode", "oh-my-opencode");
const projectDetected = detectConfigFile(projectBasePath);
const projectConfigPath = projectDetected.format !== "none" ? projectDetected.path : projectBasePath + ".json";

// Load user config first (base)
let config: OhMyOpenCodeConfig = loadConfigFromPath(userConfigPath, ctx) ?? {};
Expand Down
1 change: 1 addition & 0 deletions src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./config-path"
export * from "./data-path"
export * from "./config-errors"
export * from "./claude-config-dir"
export * from "./jsonc-parser"
Loading