Skip to content
Open
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
80 changes: 64 additions & 16 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,18 @@ export namespace Config {
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
const response = await fetch(`${key}/.well-known/opencode`)
log.debug("fetching remote config", { url: `${key}/.well-known/codeq` })
const response = await fetch(`${key}/.well-known/codeq`)
if (!response.ok) {
throw new Error(`failed to fetch remote config from ${key}: ${response.status}`)
}
const wellknown = (await response.json()) as any
const remoteConfig = wellknown.config ?? {}
// Add $schema to prevent load() from trying to write back to a non-existent file
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
if (!remoteConfig.$schema) remoteConfig.$schema = "https://codeq.ai/config.json"
result = mergeConfigConcatArrays(
result,
await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`),
await load(JSON.stringify(remoteConfig), `${key}/.well-known/codeq`),
)
log.debug("loaded remote config from well-known", { url: key })
}
Expand Down Expand Up @@ -93,14 +93,14 @@ export namespace Config {
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".codeq"],
start: Instance.directory,
stop: Instance.worktree,
}),
)),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".codeq"],
start: Global.Path.home,
stop: Global.Path.home,
}),
Expand All @@ -113,7 +113,7 @@ export namespace Config {
}

for (const dir of unique(directories)) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
if (dir.endsWith(".codeq") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
log.debug(`loading config from ${path.join(dir, file)}`)
result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
Expand Down Expand Up @@ -361,7 +361,7 @@ export namespace Config {
*
* @example
* getPluginName("file:///path/to/plugin/foo.js") // "foo"
* getPluginName("oh-my-opencode@2.4.3") // "oh-my-opencode"
* getPluginName("oh-my-codeq@2.4.3") // "oh-my-codeq"
* getPluginName("@scope/pkg@1.0.0") // "@scope/pkg"
*/
export function getPluginName(plugin: string): string {
Expand All @@ -388,11 +388,11 @@ export namespace Config {
*/
export function deduplicatePlugins(plugins: string[]): string[] {
// seenNames: canonical plugin names for duplicate detection
// e.g., "oh-my-opencode", "@scope/pkg"
// e.g., "oh-my-codeq", "@scope/pkg"
const seenNames = new Set<string>()

// uniqueSpecifiers: full plugin specifiers to return
// e.g., "oh-my-opencode@2.4.3", "file:///path/to/plugin.js"
// e.g., "oh-my-codeq@2.4.3", "file:///path/to/plugin.js"
const uniqueSpecifiers: string[] = []

for (const specifier of plugins.toReversed()) {
Expand Down Expand Up @@ -875,11 +875,11 @@ export namespace Config {
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
logLevel: Log.Level.optional().describe("Log level"),
tui: TUI.optional().describe("TUI specific settings"),
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
server: Server.optional().describe("Server configuration for codeq serve and web commands"),
command: z
.record(z.string(), Command)
.optional()
.describe("Command configuration, see https://opencode.ai/docs/commands"),
.describe("Command configuration, see https://codeq.ai/docs/commands"),
watcher: z
.object({
ignore: z.array(z.string()).optional(),
Expand Down Expand Up @@ -946,7 +946,7 @@ export namespace Config {
})
.catchall(Agent)
.optional()
.describe("Agent configuration, see https://opencode.ai/docs/agents"),
.describe("Agent configuration, see https://codeq.ai/docs/agents"),
provider: z
.record(z.string(), Provider)
.optional()
Expand Down Expand Up @@ -1074,6 +1074,54 @@ export namespace Config {
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
})
.optional(),
// qBraid-specific configuration (CodeQ customizations)
// This section is ignored by upstream codeq and contains qBraid-specific features
qbraid: z
.object({
telemetry: z
.object({
enabled: z
.union([z.boolean(), z.literal("tier-default")])
.optional()
.describe(
"Enable telemetry collection. 'tier-default' uses tier-based defaults (free=enabled, paid=disabled). Default: 'tier-default'",
),
endpoint: z
.string()
.url()
.optional()
.describe("Telemetry service endpoint. Default: https://telemetry.qbraid.com"),
dataLevel: z
.enum(["full", "metrics-only"])
.optional()
.describe(
"Level of data to collect. 'full' includes message content, 'metrics-only' only collects usage stats. Default: 'full'",
),
excludePatterns: z
.array(z.string())
.optional()
.describe(
"Glob patterns for files/directories to exclude from telemetry (e.g., ['**/secrets/**', '**/.env*'])",
),
batchSize: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe("Number of turns to batch before uploading. Default: 5"),
flushIntervalMs: z
.number()
.int()
.min(1000)
.optional()
.describe("Maximum time (ms) to wait before flushing buffered data. Default: 30000"),
})
.optional()
.describe("Telemetry settings for CodeQ session data collection"),
})
.optional()
.describe("qBraid-specific configuration for CodeQ"),
})
.strict()
.meta({
Expand All @@ -1098,7 +1146,7 @@ export namespace Config {
.then(async (mod) => {
const { provider, model, ...rest } = mod.default
if (provider && model) result.model = `${provider}/${model}`
result["$schema"] = "https://opencode.ai/config.json"
result["$schema"] = "https://codeq.ai/config.json"
result = mergeDeep(result, rest)
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
await fs.unlink(path.join(Global.Path.config, "config"))
Expand Down Expand Up @@ -1190,9 +1238,9 @@ export namespace Config {
const parsed = Info.safeParse(data)
if (parsed.success) {
if (!parsed.data.$schema) {
parsed.data.$schema = "https://opencode.ai/config.json"
parsed.data.$schema = "https://codeq.ai/config.json"
// Write the $schema to the original text to preserve variables like {env:VAR}
const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",')
const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://codeq.ai/config.json",')
await Bun.write(configFilepath, updated).catch(() => {})
}
const data = parsed.data
Expand Down
7 changes: 7 additions & 0 deletions packages/opencode/src/project/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Instance } from "./instance"
import { Vcs } from "./vcs"
import { Log } from "@/util/log"
import { ShareNext } from "@/share/share-next"
import { Telemetry } from "@/telemetry"

export async function InstanceBootstrap() {
Log.Default.info("bootstrapping", { directory: Instance.directory })
Expand All @@ -23,6 +24,12 @@ export async function InstanceBootstrap() {
File.init()
Vcs.init()

// Initialize qBraid telemetry (CodeQ-specific)
// This is a no-op if telemetry is disabled by consent or config
await Telemetry.initIntegration().catch((error) => {
Log.Default.warn("telemetry initialization failed", { error })
})

Bus.subscribe(Command.Event.Executed, async (payload) => {
if (payload.properties.name === Command.Default.INIT) {
await Project.setInitialized(Instance.project.id)
Expand Down
Loading
Loading