theclawbay 0.3.37 → 0.3.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ theclawbay setup --api-key <apiKey>
18
18
 
19
19
  In an interactive terminal, setup shows one picker for Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, Windows Trae, and Zo.
20
20
  Each row includes a terminal-friendly icon plus the tool's official site, and you can toggle items with arrow keys plus `Enter` or by pressing `1-9` and `0`.
21
+ Re-running setup also migrates legacy OpenCode and Kilo patches to the current reasoning-capable provider path and refreshes OpenClaw model metadata.
21
22
 
22
23
  ## Optional
23
24
 
@@ -12,6 +12,7 @@ const base_command_1 = require("../lib/base-command");
12
12
  const codex_auth_seeding_1 = require("../lib/codex-auth-seeding");
13
13
  const codex_history_migration_1 = require("../lib/codex-history-migration");
14
14
  const codex_model_cache_migration_1 = require("../lib/codex-model-cache-migration");
15
+ const supported_models_1 = require("../lib/supported-models");
15
16
  const paths_1 = require("../lib/config/paths");
16
17
  const OPENAI_PROVIDER_ID = "openai";
17
18
  const DEFAULT_PROVIDER_ID = "theclawbay";
@@ -29,6 +30,8 @@ const CLINE_GLOBAL_STATE_PATH = node_path_1.default.join(node_os_1.default.homed
29
30
  const CLINE_SECRETS_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".cline", "data", "secrets.json");
30
31
  const AIDER_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".aider.conf.yml");
31
32
  const CLINE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "cline.restore.json");
33
+ const OPENCODE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "opencode.restore.json");
34
+ const KILO_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "kilo.restore.json");
32
35
  const ROO_SETTINGS_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "roo-settings.restore.json");
33
36
  const ROO_IMPORT_FILE = node_path_1.default.join(paths_1.theclawbayConfigDir, "roo-code-settings.json");
34
37
  const MIGRATION_STATE_FILE = node_path_1.default.join(paths_1.codexDir, "theclawbay.migration.json");
@@ -39,6 +42,7 @@ const HISTORY_PROVIDER_NEUTRALIZE_SOURCES = new Set([
39
42
  ]);
40
43
  const HISTORY_PROVIDER_DB_MIGRATE_SOURCES = [DEFAULT_PROVIDER_ID, WAN_PROVIDER_ID];
41
44
  const THECLAWBAY_OPENAI_PROXY_SUFFIX = "/api/codex-auth/v1/proxy/v1";
45
+ const SUPPORTED_MODEL_IDS = new Set((0, supported_models_1.getSupportedModelIds)());
42
46
  const CONTINUE_MODEL_NAME = "The Claw Bay";
43
47
  const TRAE_PATCH_MARKER = "theclawbay-trae-patch";
44
48
  const TRAE_BUNDLE_BACKUP_SUFFIX = ".theclawbay-managed-backup";
@@ -162,6 +166,57 @@ function objectRecordOr(value, fallback) {
162
166
  function isTheClawBayOpenAiCompatibleBaseUrl(value) {
163
167
  return typeof value === "string" && value.trim().endsWith(THECLAWBAY_OPENAI_PROXY_SUFFIX);
164
168
  }
169
+ function uniqueStrings(values) {
170
+ const output = [];
171
+ const seen = new Set();
172
+ for (const value of values) {
173
+ const normalized = value.trim();
174
+ if (!normalized || seen.has(normalized))
175
+ continue;
176
+ seen.add(normalized);
177
+ output.push(normalized);
178
+ }
179
+ return output;
180
+ }
181
+ function xdgConfigHomeDir() {
182
+ const raw = process.env.XDG_CONFIG_HOME?.trim();
183
+ if (raw)
184
+ return raw;
185
+ return node_path_1.default.join(node_os_1.default.homedir(), ".config");
186
+ }
187
+ function configDirCandidates(appName) {
188
+ const home = node_os_1.default.homedir();
189
+ const dirs = [node_path_1.default.join(xdgConfigHomeDir(), appName)];
190
+ if (node_os_1.default.platform() === "darwin") {
191
+ dirs.push(node_path_1.default.join(home, "Library", "Application Support", appName));
192
+ }
193
+ else if (node_os_1.default.platform() === "win32") {
194
+ dirs.push(node_path_1.default.join(roamingAppDataDir(), appName));
195
+ }
196
+ return uniqueStrings(dirs);
197
+ }
198
+ function openCodeConfigCleanupTargets() {
199
+ const home = node_os_1.default.homedir();
200
+ const dirs = configDirCandidates("opencode");
201
+ const candidates = [];
202
+ for (const dir of dirs) {
203
+ candidates.push(node_path_1.default.join(dir, "opencode.json"));
204
+ candidates.push(node_path_1.default.join(dir, ".opencode.json"));
205
+ candidates.push(node_path_1.default.join(dir, "config.json"));
206
+ }
207
+ candidates.push(node_path_1.default.join(home, ".opencode.json"));
208
+ return uniqueStrings(candidates);
209
+ }
210
+ function kiloConfigCleanupTargets() {
211
+ const home = node_os_1.default.homedir();
212
+ const dirs = uniqueStrings([...configDirCandidates("kilo"), node_path_1.default.join(home, ".kilo")]);
213
+ const candidates = [];
214
+ for (const dir of dirs) {
215
+ candidates.push(node_path_1.default.join(dir, "opencode.json"));
216
+ candidates.push(node_path_1.default.join(dir, "config.json"));
217
+ }
218
+ return uniqueStrings(candidates);
219
+ }
165
220
  async function readJsonObjectFile(filePath) {
166
221
  const existingRaw = await readFileIfExists(filePath);
167
222
  if (existingRaw === null || !existingRaw.trim())
@@ -179,6 +234,18 @@ async function writeJsonFile(filePath, value, mode) {
179
234
  if (mode !== undefined)
180
235
  await promises_1.default.chmod(filePath, mode);
181
236
  }
237
+ function isManagedOpenCodeModel(value) {
238
+ if (typeof value !== "string")
239
+ return false;
240
+ if (!value.startsWith(`${OPENAI_PROVIDER_ID}/`))
241
+ return false;
242
+ return SUPPORTED_MODEL_IDS.has(value.slice(`${OPENAI_PROVIDER_ID}/`.length));
243
+ }
244
+ function isManagedOpenCodeProvider(value) {
245
+ const provider = objectRecordOr(value, {});
246
+ const options = objectRecordOr(provider.options, {});
247
+ return isTheClawBayOpenAiCompatibleBaseUrl(options.baseURL);
248
+ }
182
249
  function roamingAppDataDir() {
183
250
  if (process.env.APPDATA?.trim())
184
251
  return process.env.APPDATA;
@@ -433,67 +500,189 @@ async function cleanupOpenClawConfig() {
433
500
  await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
434
501
  return true;
435
502
  }
436
- async function cleanupOpenCodeConfig() {
437
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
438
- const existingRaw = await readFileIfExists(configPath);
439
- if (existingRaw === null || !existingRaw.trim())
440
- return false;
441
- let doc;
442
- try {
443
- doc = objectRecordOr(JSON.parse(existingRaw), {});
444
- }
445
- catch {
446
- return false;
447
- }
448
- let changed = false;
449
- const providerRoot = objectRecordOr(doc.provider, {});
450
- for (const id of [DEFAULT_PROVIDER_ID]) {
451
- if (id in providerRoot) {
452
- delete providerRoot[id];
453
- changed = true;
503
+ function normalizeOpenCodeRestoreSnapshot(snapshot, fallbackConfigPath) {
504
+ if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot))
505
+ return null;
506
+ const obj = snapshot;
507
+ if (obj.version === 2 && Array.isArray(obj.targets)) {
508
+ const targets = [];
509
+ for (const entry of obj.targets) {
510
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
511
+ continue;
512
+ const candidate = entry;
513
+ const configPath = typeof candidate.configPath === "string" ? candidate.configPath.trim() : "";
514
+ if (!configPath)
515
+ continue;
516
+ targets.push({
517
+ configPath,
518
+ existed: candidate.existed === true,
519
+ openAiProvider: typeof candidate.openAiProvider === "object" &&
520
+ candidate.openAiProvider !== null &&
521
+ !Array.isArray(candidate.openAiProvider)
522
+ ? candidate.openAiProvider
523
+ : null,
524
+ model: typeof candidate.model === "string" ? candidate.model : null,
525
+ schema: typeof candidate.schema === "string" ? candidate.schema : null,
526
+ plugin: "plugin" in candidate
527
+ ? Array.isArray(candidate.plugin)
528
+ ? candidate.plugin
529
+ : null
530
+ : undefined,
531
+ });
454
532
  }
533
+ return { version: 2, targets };
455
534
  }
456
- doc.provider = providerRoot;
457
- const model = doc.model;
458
- if (typeof model === "string" && model.startsWith(`${DEFAULT_PROVIDER_ID}/`)) {
459
- delete doc.model;
460
- changed = true;
461
- }
462
- if (!changed)
463
- return false;
464
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
465
- return true;
535
+ const looksLikeV1 = "openAiProvider" in obj || "model" in obj || "schema" in obj;
536
+ if (!looksLikeV1)
537
+ return null;
538
+ return {
539
+ version: 2,
540
+ targets: [
541
+ {
542
+ configPath: fallbackConfigPath,
543
+ existed: true,
544
+ openAiProvider: typeof obj.openAiProvider === "object" && obj.openAiProvider !== null && !Array.isArray(obj.openAiProvider)
545
+ ? obj.openAiProvider
546
+ : null,
547
+ model: typeof obj.model === "string" ? obj.model : null,
548
+ schema: typeof obj.schema === "string" ? obj.schema : null,
549
+ },
550
+ ],
551
+ };
466
552
  }
467
- async function cleanupKiloConfig() {
468
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json");
469
- const existingRaw = await readFileIfExists(configPath);
553
+ async function readOpenCodeRestoreSnapshot(restoreStatePath, fallbackConfigPath) {
554
+ const existingRaw = await readFileIfExists(restoreStatePath);
470
555
  if (existingRaw === null || !existingRaw.trim())
471
- return false;
472
- let doc;
556
+ return null;
473
557
  try {
474
- doc = objectRecordOr(JSON.parse(existingRaw), {});
558
+ return normalizeOpenCodeRestoreSnapshot(JSON.parse(existingRaw), fallbackConfigPath);
475
559
  }
476
560
  catch {
477
- return false;
561
+ return null;
478
562
  }
563
+ }
564
+ async function cleanupOpenCodeFamilyConfigs(params) {
565
+ const configPaths = uniqueStrings(params.configPaths);
566
+ const fallbackConfigPath = configPaths[0] ?? node_path_1.default.join(xdgConfigHomeDir(), "opencode", "opencode.json");
567
+ const snapshot = await readOpenCodeRestoreSnapshot(params.restoreStatePath, fallbackConfigPath);
568
+ const snapshotTargets = snapshot?.targets ?? [];
479
569
  let changed = false;
480
- const providerRoot = objectRecordOr(doc.provider, {});
481
- for (const id of [DEFAULT_PROVIDER_ID]) {
482
- if (id in providerRoot) {
483
- delete providerRoot[id];
570
+ const restoreConfig = async (target) => {
571
+ if (!target.existed) {
572
+ changed = (await removeFileIfExists(target.configPath)) || changed;
573
+ return;
574
+ }
575
+ const existingRaw = await readFileIfExists(target.configPath);
576
+ if (existingRaw === null || !existingRaw.trim()) {
577
+ return;
578
+ }
579
+ let doc;
580
+ try {
581
+ doc = objectRecordOr(JSON.parse(existingRaw), {});
582
+ }
583
+ catch {
584
+ return;
585
+ }
586
+ const providerRoot = objectRecordOr(doc.provider, {});
587
+ if (DEFAULT_PROVIDER_ID in providerRoot) {
588
+ delete providerRoot[DEFAULT_PROVIDER_ID];
589
+ changed = true;
590
+ }
591
+ if (Object.prototype.hasOwnProperty.call(target, "plugin")) {
592
+ if (target.plugin === null) {
593
+ if ("plugin" in doc) {
594
+ delete doc.plugin;
595
+ changed = true;
596
+ }
597
+ }
598
+ else {
599
+ doc.plugin = target.plugin;
600
+ changed = true;
601
+ }
602
+ }
603
+ if (target.openAiProvider) {
604
+ providerRoot[OPENAI_PROVIDER_ID] = target.openAiProvider;
605
+ }
606
+ else if (OPENAI_PROVIDER_ID in providerRoot) {
607
+ delete providerRoot[OPENAI_PROVIDER_ID];
608
+ }
609
+ doc.provider = providerRoot;
610
+ if (target.model === null) {
611
+ if (isManagedOpenCodeModel(doc.model) || (typeof doc.model === "string" && doc.model.startsWith(`${DEFAULT_PROVIDER_ID}/`))) {
612
+ delete doc.model;
613
+ changed = true;
614
+ }
615
+ }
616
+ else {
617
+ doc.model = target.model;
484
618
  changed = true;
485
619
  }
620
+ if (target.schema === null) {
621
+ if (typeof doc.$schema === "string") {
622
+ delete doc.$schema;
623
+ changed = true;
624
+ }
625
+ }
626
+ else {
627
+ doc.$schema = target.schema;
628
+ changed = true;
629
+ }
630
+ await promises_1.default.writeFile(target.configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
631
+ };
632
+ if (snapshotTargets.length > 0) {
633
+ for (const target of snapshotTargets) {
634
+ await restoreConfig(target);
635
+ }
636
+ await removeFileIfExists(params.restoreStatePath);
637
+ return changed;
486
638
  }
487
- doc.provider = providerRoot;
488
- const model = doc.model;
489
- if (typeof model === "string" && model.startsWith(`${DEFAULT_PROVIDER_ID}/`)) {
490
- delete doc.model;
639
+ for (const configPath of configPaths) {
640
+ const existingRaw = await readFileIfExists(configPath);
641
+ if (existingRaw === null || !existingRaw.trim())
642
+ continue;
643
+ let doc;
644
+ try {
645
+ doc = objectRecordOr(JSON.parse(existingRaw), {});
646
+ }
647
+ catch {
648
+ continue;
649
+ }
650
+ let fileChanged = false;
651
+ const providerRoot = objectRecordOr(doc.provider, {});
652
+ if (DEFAULT_PROVIDER_ID in providerRoot) {
653
+ delete providerRoot[DEFAULT_PROVIDER_ID];
654
+ fileChanged = true;
655
+ }
656
+ const model = doc.model;
657
+ if (typeof model === "string" &&
658
+ (model.startsWith(`${DEFAULT_PROVIDER_ID}/`) || isManagedOpenCodeModel(model))) {
659
+ delete doc.model;
660
+ fileChanged = true;
661
+ }
662
+ if (isManagedOpenCodeProvider(providerRoot[OPENAI_PROVIDER_ID])) {
663
+ delete providerRoot[OPENAI_PROVIDER_ID];
664
+ fileChanged = true;
665
+ }
666
+ if (!fileChanged)
667
+ continue;
491
668
  changed = true;
669
+ doc.provider = providerRoot;
670
+ await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
492
671
  }
493
- if (!changed)
494
- return false;
495
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
496
- return true;
672
+ await removeFileIfExists(params.restoreStatePath);
673
+ return changed;
674
+ }
675
+ async function cleanupOpenCodeConfig() {
676
+ return cleanupOpenCodeFamilyConfigs({
677
+ configPaths: openCodeConfigCleanupTargets(),
678
+ restoreStatePath: OPENCODE_RESTORE_STATE_PATH,
679
+ });
680
+ }
681
+ async function cleanupKiloConfig() {
682
+ return cleanupOpenCodeFamilyConfigs({
683
+ configPaths: kiloConfigCleanupTargets(),
684
+ restoreStatePath: KILO_RESTORE_STATE_PATH,
685
+ });
497
686
  }
498
687
  async function cleanupTraeBundle() {
499
688
  const bundlePath = traeBundlePath();
@@ -43,6 +43,8 @@ const CLINE_GLOBAL_STATE_PATH = node_path_1.default.join(node_os_1.default.homed
43
43
  const CLINE_SECRETS_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".cline", "data", "secrets.json");
44
44
  const AIDER_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".aider.conf.yml");
45
45
  const CLINE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "cline.restore.json");
46
+ const OPENCODE_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "opencode.restore.json");
47
+ const KILO_RESTORE_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "kilo.restore.json");
46
48
  const ROO_SETTINGS_STATE_PATH = node_path_1.default.join(paths_1.theclawbayStateDir, "roo-settings.restore.json");
47
49
  const ROO_IMPORT_FILE = node_path_1.default.join(paths_1.theclawbayConfigDir, "roo-code-settings.json");
48
50
  const MIGRATION_STATE_FILE = node_path_1.default.join(paths_1.codexDir, "theclawbay.migration.json");
@@ -65,6 +67,9 @@ const TRAE_PATCH_MARKER = "theclawbay-trae-patch";
65
67
  const TRAE_BUNDLE_BACKUP_SUFFIX = ".theclawbay-managed-backup";
66
68
  const TRAE_TARGET_FUNCTION_START = "async setOriginModelListMapAndCache(e,t=!0){";
67
69
  const TRAE_TARGET_FUNCTION_END = "}async refreshModelListConfig";
70
+ const OPENAI_PROVIDER_ID = "openai";
71
+ const OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
72
+ const KILO_CONFIG_SCHEMA_URL = "https://kilo.ai/config.json";
68
73
  const ZO_CONFIG_NAME_PREFIX = "The Claw Bay";
69
74
  const ZO_COOKIE_HOST = ".zo.computer";
70
75
  const ZO_ACCESS_TOKEN_COOKIE = "access_token";
@@ -437,6 +442,56 @@ function openAiCompatibleProxyUrl(backendUrl) {
437
442
  function isTheClawBayOpenAiCompatibleBaseUrl(value) {
438
443
  return typeof value === "string" && value.trim().endsWith(THECLAWBAY_OPENAI_PROXY_SUFFIX);
439
444
  }
445
+ function uniqueStrings(values) {
446
+ const output = [];
447
+ const seen = new Set();
448
+ for (const value of values) {
449
+ const normalized = value.trim();
450
+ if (!normalized || seen.has(normalized))
451
+ continue;
452
+ seen.add(normalized);
453
+ output.push(normalized);
454
+ }
455
+ return output;
456
+ }
457
+ function xdgConfigHomeDir() {
458
+ const raw = process.env.XDG_CONFIG_HOME?.trim();
459
+ if (raw)
460
+ return raw;
461
+ return node_path_1.default.join(node_os_1.default.homedir(), ".config");
462
+ }
463
+ function configFileCandidatesForDir(dir, basenames) {
464
+ return basenames.map((basename) => node_path_1.default.join(dir, basename));
465
+ }
466
+ function findProjectConfigFile(params) {
467
+ let current = node_path_1.default.resolve(params.startDir);
468
+ const root = node_path_1.default.parse(current).root;
469
+ while (true) {
470
+ for (const candidate of configFileCandidatesForDir(current, params.basenames)) {
471
+ if ((0, node_fs_1.existsSync)(candidate))
472
+ return candidate;
473
+ }
474
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(current, ".git")))
475
+ return null;
476
+ if (current === root)
477
+ return null;
478
+ const parent = node_path_1.default.dirname(current);
479
+ if (parent === current)
480
+ return null;
481
+ current = parent;
482
+ }
483
+ }
484
+ function configDirCandidates(appName) {
485
+ const home = node_os_1.default.homedir();
486
+ const dirs = [node_path_1.default.join(xdgConfigHomeDir(), appName)];
487
+ if (node_os_1.default.platform() === "darwin") {
488
+ dirs.push(node_path_1.default.join(home, "Library", "Application Support", appName));
489
+ }
490
+ else if (node_os_1.default.platform() === "win32") {
491
+ dirs.push(node_path_1.default.join(roamingAppDataDir(), appName));
492
+ }
493
+ return uniqueStrings(dirs);
494
+ }
440
495
  async function readFileIfExists(filePath) {
441
496
  try {
442
497
  return await promises_1.default.readFile(filePath, "utf8");
@@ -1203,7 +1258,7 @@ async function patchOpenClawConfigFile(params) {
1203
1258
  baseUrl: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1204
1259
  apiKey: params.apiKey,
1205
1260
  api: "openai-responses",
1206
- models: params.models,
1261
+ models: buildOpenClawModels(params.models),
1207
1262
  };
1208
1263
  const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".openclaw", "openclaw.json");
1209
1264
  await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
@@ -1235,6 +1290,9 @@ async function patchOpenClawConfigFile(params) {
1235
1290
  const defaultsRoot = objectRecordOr(agentsRoot.defaults, {});
1236
1291
  const modelRoot = objectRecordOr(defaultsRoot.model, {});
1237
1292
  modelRoot.primary = resolveOpenClawPrimaryModel(params);
1293
+ if (typeof defaultsRoot.thinkingDefault !== "string") {
1294
+ defaultsRoot.thinkingDefault = "medium";
1295
+ }
1238
1296
  defaultsRoot.model = modelRoot;
1239
1297
  agentsRoot.defaults = defaultsRoot;
1240
1298
  doc.agents = agentsRoot;
@@ -1251,87 +1309,302 @@ function objectRecordOr(value, fallback) {
1251
1309
  }
1252
1310
  return fallback;
1253
1311
  }
1254
- function openCodeModelsObject(models) {
1312
+ function modelContextLimit(modelId) {
1313
+ if (modelId === "gpt-5.4")
1314
+ return 1050000;
1315
+ return 272000;
1316
+ }
1317
+ function modelOutputLimit(modelId) {
1318
+ if (modelId === "gpt-5.4")
1319
+ return 128000;
1320
+ return 65536;
1321
+ }
1322
+ function buildOpenClawModels(models) {
1323
+ const supportedModelMap = new Map((0, supported_models_1.getSupportedModels)().map((model) => [model.id, model]));
1324
+ return models
1325
+ .filter((model) => Boolean(model.id))
1326
+ .map((model) => {
1327
+ const pricing = supportedModelMap.get(model.id);
1328
+ return {
1329
+ id: model.id,
1330
+ name: model.name || model.id,
1331
+ api: "openai-responses",
1332
+ reasoning: true,
1333
+ input: ["text", "image"],
1334
+ cost: {
1335
+ input: pricing?.inputPer1M ?? 0,
1336
+ output: pricing?.outputPer1M ?? 0,
1337
+ cacheRead: pricing?.cachedInputPer1M ?? 0,
1338
+ cacheWrite: pricing?.inputPer1M ?? 0,
1339
+ },
1340
+ contextWindow: modelContextLimit(model.id),
1341
+ maxTokens: modelOutputLimit(model.id),
1342
+ };
1343
+ });
1344
+ }
1345
+ function buildOpenCodeModelConfig(model) {
1346
+ return {
1347
+ name: model.name || model.id,
1348
+ reasoning: true,
1349
+ tool_call: true,
1350
+ attachment: true,
1351
+ modalities: {
1352
+ input: ["text", "image"],
1353
+ output: ["text"],
1354
+ },
1355
+ limit: {
1356
+ context: modelContextLimit(model.id),
1357
+ output: modelOutputLimit(model.id),
1358
+ },
1359
+ };
1360
+ }
1361
+ function buildOpenCodeModelsObject(models) {
1255
1362
  const result = {};
1256
1363
  for (const model of models) {
1257
1364
  if (!model.id)
1258
1365
  continue;
1259
- result[model.id] = { name: model.name || model.id };
1366
+ result[model.id] = buildOpenCodeModelConfig(model);
1260
1367
  }
1261
1368
  return result;
1262
1369
  }
1263
- async function writeOpenCodeConfig(params) {
1264
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "opencode", "opencode.json");
1265
- await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
1266
- let existingRaw = "";
1267
- try {
1268
- existingRaw = await promises_1.default.readFile(configPath, "utf8");
1269
- }
1270
- catch (error) {
1271
- const err = error;
1272
- if (err.code !== "ENOENT")
1273
- throw error;
1274
- }
1275
- let doc = {};
1276
- if (existingRaw.trim()) {
1277
- try {
1278
- doc = objectRecordOr(JSON.parse(existingRaw), {});
1279
- }
1280
- catch {
1281
- throw new Error(`invalid JSON in OpenCode config: ${configPath}`);
1370
+ function normalizeOpenCodeRestoreSnapshot(snapshot, fallbackConfigPath) {
1371
+ if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot))
1372
+ return null;
1373
+ const obj = snapshot;
1374
+ if (obj.version === 2 && Array.isArray(obj.targets)) {
1375
+ const targets = [];
1376
+ for (const entry of obj.targets) {
1377
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
1378
+ continue;
1379
+ const candidate = entry;
1380
+ const configPath = typeof candidate.configPath === "string" ? candidate.configPath.trim() : "";
1381
+ if (!configPath)
1382
+ continue;
1383
+ targets.push({
1384
+ configPath,
1385
+ existed: candidate.existed === true,
1386
+ openAiProvider: typeof candidate.openAiProvider === "object" &&
1387
+ candidate.openAiProvider !== null &&
1388
+ !Array.isArray(candidate.openAiProvider)
1389
+ ? candidate.openAiProvider
1390
+ : null,
1391
+ model: typeof candidate.model === "string" ? candidate.model : null,
1392
+ schema: typeof candidate.schema === "string" ? candidate.schema : null,
1393
+ plugin: "plugin" in candidate
1394
+ ? Array.isArray(candidate.plugin)
1395
+ ? candidate.plugin
1396
+ : null
1397
+ : undefined,
1398
+ });
1282
1399
  }
1400
+ return { version: 2, targets };
1283
1401
  }
1284
- const providerRoot = objectRecordOr(doc.provider, {});
1285
- providerRoot[DEFAULT_PROVIDER_ID] = {
1286
- npm: "@ai-sdk/openai-compatible",
1287
- name: DEFAULT_PROVIDER_ID,
1288
- options: {
1289
- baseURL: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1290
- apiKey: params.apiKey,
1291
- },
1292
- models: openCodeModelsObject(params.models),
1402
+ const looksLikeV1 = "openAiProvider" in obj || "model" in obj || "schema" in obj;
1403
+ if (!looksLikeV1)
1404
+ return null;
1405
+ return {
1406
+ version: 2,
1407
+ targets: [
1408
+ {
1409
+ configPath: fallbackConfigPath,
1410
+ existed: true,
1411
+ openAiProvider: typeof obj.openAiProvider === "object" && obj.openAiProvider !== null && !Array.isArray(obj.openAiProvider)
1412
+ ? obj.openAiProvider
1413
+ : null,
1414
+ model: typeof obj.model === "string" ? obj.model : null,
1415
+ schema: typeof obj.schema === "string" ? obj.schema : null,
1416
+ },
1417
+ ],
1293
1418
  };
1294
- doc.provider = providerRoot;
1295
- doc.model = `${DEFAULT_PROVIDER_ID}/${params.model}`;
1296
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1297
- return configPath;
1298
1419
  }
1299
- async function writeKiloConfig(params) {
1300
- const configPath = node_path_1.default.join(node_os_1.default.homedir(), ".config", "kilo", "opencode.json");
1301
- await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
1302
- let existingRaw = "";
1420
+ async function readOpenCodeRestoreSnapshot(restoreStatePath, fallbackConfigPath) {
1421
+ const existingRaw = await readFileIfExists(restoreStatePath);
1422
+ if (existingRaw === null || !existingRaw.trim())
1423
+ return null;
1303
1424
  try {
1304
- existingRaw = await promises_1.default.readFile(configPath, "utf8");
1425
+ return normalizeOpenCodeRestoreSnapshot(JSON.parse(existingRaw), fallbackConfigPath);
1305
1426
  }
1306
- catch (error) {
1307
- const err = error;
1308
- if (err.code !== "ENOENT")
1309
- throw error;
1427
+ catch {
1428
+ return null;
1310
1429
  }
1311
- let doc = {};
1312
- if (existingRaw.trim()) {
1430
+ }
1431
+ function openCodeConfigFileCandidates() {
1432
+ const home = node_os_1.default.homedir();
1433
+ const dirs = configDirCandidates("opencode");
1434
+ const candidates = [];
1435
+ for (const dir of dirs) {
1436
+ candidates.push(node_path_1.default.join(dir, "opencode.json"));
1437
+ candidates.push(node_path_1.default.join(dir, ".opencode.json"));
1438
+ candidates.push(node_path_1.default.join(dir, "config.json"));
1439
+ }
1440
+ candidates.push(node_path_1.default.join(home, ".opencode.json"));
1441
+ return uniqueStrings(candidates);
1442
+ }
1443
+ function findOpenCodeProjectConfigFile() {
1444
+ return findProjectConfigFile({
1445
+ startDir: process.cwd(),
1446
+ basenames: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
1447
+ });
1448
+ }
1449
+ function isOpenCodeAuthPlugin(value) {
1450
+ if (typeof value !== "string")
1451
+ return false;
1452
+ const normalized = value.trim();
1453
+ if (!normalized)
1454
+ return false;
1455
+ return normalized.startsWith("opencode-openai-codex-auth");
1456
+ }
1457
+ function filteredOpenCodePlugins(value) {
1458
+ if (!Array.isArray(value))
1459
+ return { next: value, changed: false };
1460
+ const filtered = value.filter((entry) => !isOpenCodeAuthPlugin(entry));
1461
+ if (filtered.length === value.length)
1462
+ return { next: value, changed: false };
1463
+ return { next: filtered, changed: true };
1464
+ }
1465
+ function kiloConfigFileCandidates() {
1466
+ const home = node_os_1.default.homedir();
1467
+ const dirs = uniqueStrings([...configDirCandidates("kilo"), node_path_1.default.join(home, ".kilo")]);
1468
+ const candidates = [];
1469
+ for (const dir of dirs) {
1470
+ candidates.push(node_path_1.default.join(dir, "opencode.json"));
1471
+ candidates.push(node_path_1.default.join(dir, "config.json"));
1472
+ }
1473
+ return uniqueStrings(candidates);
1474
+ }
1475
+ function resolveOpenCodeConfigTargets() {
1476
+ const candidates = openCodeConfigFileCandidates();
1477
+ const existing = candidates.filter((candidate) => (0, node_fs_1.existsSync)(candidate));
1478
+ if (existing.length)
1479
+ return existing;
1480
+ const primaryDir = configDirCandidates("opencode")[0] ?? node_path_1.default.join(xdgConfigHomeDir(), "opencode");
1481
+ return uniqueStrings([node_path_1.default.join(primaryDir, "opencode.json"), node_path_1.default.join(primaryDir, ".opencode.json")]);
1482
+ }
1483
+ function resolveKiloConfigTargets() {
1484
+ const candidates = kiloConfigFileCandidates();
1485
+ const existing = candidates.filter((candidate) => (0, node_fs_1.existsSync)(candidate));
1486
+ if (existing.length)
1487
+ return existing;
1488
+ const home = node_os_1.default.homedir();
1489
+ const primaryDir = uniqueStrings([...configDirCandidates("kilo"), node_path_1.default.join(home, ".kilo")])[0] ??
1490
+ node_path_1.default.join(xdgConfigHomeDir(), "kilo");
1491
+ return uniqueStrings([node_path_1.default.join(primaryDir, "opencode.json")]);
1492
+ }
1493
+ function isManagedOpenCodeProvider(provider) {
1494
+ const options = objectRecordOr(provider.options, {});
1495
+ return isTheClawBayOpenAiCompatibleBaseUrl(options.baseURL);
1496
+ }
1497
+ function isManagedOpenCodeModel(value, supportedModelIds) {
1498
+ if (typeof value !== "string")
1499
+ return false;
1500
+ if (!value.startsWith(`${OPENAI_PROVIDER_ID}/`))
1501
+ return false;
1502
+ return supportedModelIds.has(value.slice(`${OPENAI_PROVIDER_ID}/`.length));
1503
+ }
1504
+ async function writeOpenCodeFamilyConfig(params) {
1505
+ const normalizedConfigPaths = uniqueStrings(params.configPaths);
1506
+ const fallbackConfigPath = normalizedConfigPaths[0] ?? node_path_1.default.join(xdgConfigHomeDir(), "opencode", "opencode.json");
1507
+ const restoreState = (await readOpenCodeRestoreSnapshot(params.restoreStatePath, fallbackConfigPath)) ??
1508
+ {
1509
+ version: 2,
1510
+ targets: [],
1511
+ };
1512
+ const supportedModelIds = new Set(params.models.map((entry) => entry.id).filter(Boolean));
1513
+ const restoreHasTarget = (configPath) => restoreState.targets.some((entry) => entry.configPath === configPath);
1514
+ let restoreStateChanged = false;
1515
+ const ensureRestorePluginSnapshot = (configPath, plugin) => {
1516
+ const target = restoreState.targets.find((entry) => entry.configPath === configPath);
1517
+ if (!target)
1518
+ return;
1519
+ if (Object.prototype.hasOwnProperty.call(target, "plugin"))
1520
+ return;
1521
+ target.plugin = Array.isArray(plugin) ? plugin : null;
1522
+ restoreStateChanged = true;
1523
+ };
1524
+ for (const configPath of normalizedConfigPaths) {
1525
+ await promises_1.default.mkdir(node_path_1.default.dirname(configPath), { recursive: true });
1526
+ let existed = true;
1527
+ let existingRaw = "";
1313
1528
  try {
1314
- doc = objectRecordOr(JSON.parse(existingRaw), {});
1529
+ existingRaw = await promises_1.default.readFile(configPath, "utf8");
1315
1530
  }
1316
- catch {
1317
- throw new Error(`invalid JSON in Kilo config: ${configPath}`);
1531
+ catch (error) {
1532
+ const err = error;
1533
+ if (err.code !== "ENOENT")
1534
+ throw error;
1535
+ existed = false;
1536
+ }
1537
+ let doc = {};
1538
+ if (existingRaw.trim()) {
1539
+ try {
1540
+ doc = objectRecordOr(JSON.parse(existingRaw), {});
1541
+ }
1542
+ catch {
1543
+ throw new Error(`invalid JSON in config: ${configPath}`);
1544
+ }
1545
+ }
1546
+ const providerRoot = objectRecordOr(doc.provider, {});
1547
+ const openAiProvider = objectRecordOr(providerRoot[OPENAI_PROVIDER_ID], {});
1548
+ const alreadyManaged = isManagedOpenCodeProvider(openAiProvider) && isManagedOpenCodeModel(doc.model, supportedModelIds);
1549
+ if (!alreadyManaged && !restoreHasTarget(configPath)) {
1550
+ restoreState.targets.push({
1551
+ configPath,
1552
+ existed,
1553
+ openAiProvider: Object.keys(openAiProvider).length > 0 ? openAiProvider : null,
1554
+ model: typeof doc.model === "string" && !doc.model.startsWith(`${DEFAULT_PROVIDER_ID}/`)
1555
+ ? doc.model
1556
+ : null,
1557
+ schema: typeof doc.$schema === "string" ? doc.$schema : null,
1558
+ plugin: Array.isArray(doc.plugin) ? doc.plugin : null,
1559
+ });
1560
+ restoreStateChanged = true;
1561
+ }
1562
+ else if (!alreadyManaged) {
1563
+ ensureRestorePluginSnapshot(configPath, doc.plugin);
1564
+ }
1565
+ const pluginFilter = filteredOpenCodePlugins(doc.plugin);
1566
+ if (pluginFilter.changed) {
1567
+ doc.plugin = pluginFilter.next;
1568
+ }
1569
+ const managedOpenAiProvider = objectRecordOr(providerRoot[OPENAI_PROVIDER_ID], {});
1570
+ const optionsRoot = objectRecordOr(managedOpenAiProvider.options, {});
1571
+ optionsRoot.baseURL = `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`;
1572
+ optionsRoot.apiKey = params.apiKey;
1573
+ managedOpenAiProvider.options = optionsRoot;
1574
+ managedOpenAiProvider.models = buildOpenCodeModelsObject(params.models);
1575
+ managedOpenAiProvider.whitelist = params.models.map((entry) => entry.id).filter(Boolean);
1576
+ if ("blacklist" in managedOpenAiProvider) {
1577
+ delete managedOpenAiProvider.blacklist;
1578
+ }
1579
+ providerRoot[OPENAI_PROVIDER_ID] = managedOpenAiProvider;
1580
+ if (DEFAULT_PROVIDER_ID in providerRoot) {
1581
+ delete providerRoot[DEFAULT_PROVIDER_ID];
1318
1582
  }
1583
+ doc.$schema = params.schemaUrl;
1584
+ doc.provider = providerRoot;
1585
+ doc.model = `${OPENAI_PROVIDER_ID}/${params.model}`;
1586
+ await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1319
1587
  }
1320
- const providerRoot = objectRecordOr(doc.provider, {});
1321
- providerRoot[DEFAULT_PROVIDER_ID] = {
1322
- npm: "@ai-sdk/openai-compatible",
1323
- name: DEFAULT_PROVIDER_ID,
1324
- options: {
1325
- baseURL: `${trimTrailingSlash(params.backendUrl)}/api/codex-auth/v1/proxy/v1`,
1326
- apiKey: params.apiKey,
1327
- },
1328
- models: openCodeModelsObject(params.models),
1329
- };
1330
- doc.$schema = "https://kilo.ai/config.json";
1331
- doc.provider = providerRoot;
1332
- doc.model = `${DEFAULT_PROVIDER_ID}/${params.model || DEFAULT_KILO_MODEL}`;
1333
- await promises_1.default.writeFile(configPath, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
1334
- return configPath;
1588
+ if (restoreState.targets.length > 0 && restoreStateChanged) {
1589
+ await writeJsonObjectFile(params.restoreStatePath, restoreState, 0o600);
1590
+ }
1591
+ return normalizedConfigPaths;
1592
+ }
1593
+ async function writeOpenCodeConfig(params) {
1594
+ return writeOpenCodeFamilyConfig({
1595
+ configPaths: resolveOpenCodeConfigTargets(),
1596
+ restoreStatePath: OPENCODE_RESTORE_STATE_PATH,
1597
+ schemaUrl: OPENCODE_CONFIG_SCHEMA_URL,
1598
+ ...params,
1599
+ });
1600
+ }
1601
+ async function writeKiloConfig(params) {
1602
+ return writeOpenCodeFamilyConfig({
1603
+ configPaths: resolveKiloConfigTargets(),
1604
+ restoreStatePath: KILO_RESTORE_STATE_PATH,
1605
+ schemaUrl: KILO_CONFIG_SCHEMA_URL,
1606
+ ...params,
1607
+ });
1335
1608
  }
1336
1609
  function traePatchSnippet(params) {
1337
1610
  const patchedModels = params.models.length
@@ -1512,8 +1785,8 @@ class SetupCommand extends base_command_1.BaseCommand {
1512
1785
  let clineConfigPaths = [];
1513
1786
  let openClawConfigPath = null;
1514
1787
  let openClawCliWarning = null;
1515
- let openCodePath = null;
1516
- let kiloConfigPath = null;
1788
+ let openCodeConfigPaths = [];
1789
+ let kiloConfigPaths = [];
1517
1790
  let rooConfigPaths = [];
1518
1791
  let traeBundlePathPatched = null;
1519
1792
  let aiderConfigPath = null;
@@ -1577,7 +1850,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1577
1850
  }
1578
1851
  if (selectedSetupClients.has("opencode")) {
1579
1852
  progress.update("Configuring OpenCode");
1580
- openCodePath = await writeOpenCodeConfig({
1853
+ openCodeConfigPaths = await writeOpenCodeConfig({
1581
1854
  backendUrl,
1582
1855
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1583
1856
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
@@ -1586,7 +1859,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1586
1859
  }
1587
1860
  if (selectedSetupClients.has("kilo")) {
1588
1861
  progress.update("Configuring Kilo Code");
1589
- kiloConfigPath = await writeKiloConfig({
1862
+ kiloConfigPaths = await writeKiloConfig({
1590
1863
  backendUrl,
1591
1864
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1592
1865
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
@@ -1761,7 +2034,15 @@ class SetupCommand extends base_command_1.BaseCommand {
1761
2034
  }
1762
2035
  const openCodeDetected = setupClients.find((client) => client.id === "opencode")?.detected ?? false;
1763
2036
  if (selectedSetupClients.has("opencode")) {
1764
- this.log(`- OpenCode: configured (${openCodePath})`);
2037
+ this.log(`- OpenCode: configured (${openCodeConfigPaths.join(", ")})`);
2038
+ const openCodeProjectConfig = findOpenCodeProjectConfigFile();
2039
+ const openCodeConfigEnv = process.env.OPENCODE_CONFIG?.trim() || "";
2040
+ if (openCodeConfigEnv) {
2041
+ this.log(`- OpenCode note: OPENCODE_CONFIG is set (${openCodeConfigEnv}). OpenCode may load that file and override your global config.`);
2042
+ }
2043
+ if (openCodeProjectConfig) {
2044
+ this.log(`- OpenCode note: found project-level ${node_path_1.default.basename(openCodeProjectConfig)} at ${openCodeProjectConfig} which overrides global config.`);
2045
+ }
1765
2046
  }
1766
2047
  else if (openCodeDetected) {
1767
2048
  this.log("- OpenCode: detected but skipped");
@@ -1770,7 +2051,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1770
2051
  this.log("- OpenCode: not detected (skipped)");
1771
2052
  }
1772
2053
  if (selectedSetupClients.has("kilo")) {
1773
- this.log(`- Kilo Code: configured (${kiloConfigPath})`);
2054
+ this.log(`- Kilo Code: configured (${kiloConfigPaths.join(", ")})`);
1774
2055
  }
1775
2056
  else if (kiloDetected) {
1776
2057
  this.log("- Kilo Code: detected but skipped");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "description": "CLI for connecting Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, experimental Trae, and experimental Zo to The Claw Bay.",
5
5
  "license": "MIT",
6
6
  "bin": {