@bitkyc08/opencodex 2.16.0 → 2.17.0

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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-CZwbOse7.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-DOKr6RBR.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DUCH59lJ.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -62,7 +62,11 @@ export interface ExportCommandDeps extends RuntimeApiDeps {
62
62
  * `/api/models` row plus the modality list Pi consumes. The launcher's row type predates
63
63
  * the Pi exporter and stops at the fields OpenCode needs.
64
64
  */
65
- type ExportProxyModelRow = OpencodeProxyModelRow & { inputModalities?: string[] };
65
+ type ExportProxyModelRow = OpencodeProxyModelRow & {
66
+ inputModalities?: string[];
67
+ reasoningEfforts?: string[];
68
+ defaultReasoningEffort?: string;
69
+ };
66
70
 
67
71
  /** Same authoritativeness rule the serializers apply, for the degraded-count line. */
68
72
  function hasContextLimit(model: ExportModel): boolean {
@@ -83,12 +87,21 @@ export function exportModelsFromProxyRows(
83
87
  rows: readonly ExportProxyModelRow[],
84
88
  config: OcxConfig,
85
89
  ): ExportModel[] {
86
- const modalities = new Map<string, string[]>();
90
+ const metadata = new Map<string, Pick<ExportModel, "inputModalities" | "reasoningEfforts" | "defaultReasoningEffort">>();
87
91
  for (const row of rows) {
88
92
  const namespaced = row.namespaced?.trim();
89
- if (namespaced && Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
90
- if (!modalities.has(namespaced)) modalities.set(namespaced, [...row.inputModalities]);
91
- }
93
+ if (!namespaced || metadata.has(namespaced)) continue;
94
+ metadata.set(namespaced, {
95
+ ...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
96
+ ? { inputModalities: [...row.inputModalities] }
97
+ : {}),
98
+ ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
99
+ ? { reasoningEfforts: [...row.reasoningEfforts] }
100
+ : {}),
101
+ ...(typeof row.defaultReasoningEffort === "string" && row.defaultReasoningEffort.length > 0
102
+ ? { defaultReasoningEffort: row.defaultReasoningEffort }
103
+ : {}),
104
+ });
92
105
  }
93
106
  return opencodeCatalogFromProxyRows(rows, config).map(entry => {
94
107
  const model: ExportModel = {
@@ -99,8 +112,7 @@ export function exportModelsFromProxyRows(
99
112
  if (entry.native) model.native = true;
100
113
  if (entry.displayName) model.displayName = entry.displayName;
101
114
  if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
102
- const input = modalities.get(entry.namespaced);
103
- if (input) model.inputModalities = input;
115
+ Object.assign(model, metadata.get(entry.namespaced));
104
116
  return model;
105
117
  });
106
118
  }
package/src/cli/help.ts CHANGED
@@ -58,7 +58,7 @@ Usage:
58
58
  ocx memory [--json] Alias of ocx observe memory
59
59
  ocx api-key <sub> Alias of ocx access key
60
60
  ocx access <sub> External API keys and endpoint information
61
- ocx export --client <id> Print a client config wired to the running proxy (7 clients)
61
+ ocx export --client <id> Print a client config wired to the running proxy (8 clients)
62
62
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
63
63
  ocx grok <sub> Grok Build model selection and apply
64
64
  ocx system <sub> Runtime settings, startup, sync, and updates
@@ -216,8 +216,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
216
216
  { name: "api-key", usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
217
217
  {
218
218
  name: "export",
219
- usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
220
- summary: "Print a client config (opencode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.",
219
+ usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh> [--json] [--out <path>] [--force]",
220
+ summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness) wired to the running proxy.",
221
221
  details: [
222
222
  "--json prints the generated document as JSON on stdout; use --out for the client's native format.",
223
223
  "--out <path> writes the native config there and refuses to replace an existing file without --force.",
@@ -21,9 +21,10 @@
21
21
  */
22
22
  import { homedir } from "node:os";
23
23
  import { existsSync } from "node:fs";
24
- import { isAbsolute, join } from "node:path";
24
+ import { isAbsolute, join, resolve } from "node:path";
25
25
  import { shouldInjectApiAuthHeader } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
+ import { providerCodexAccountMode } from "../providers/registry";
27
28
  import { probeHostname } from "../server/proxy-liveness";
28
29
  import type { OcxConfig } from "../types";
29
30
 
@@ -376,6 +377,28 @@ export function gajaeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
376
377
  return join(gajaeHomeDir(env, home), "agent", "models.yml");
377
378
  }
378
379
 
380
+ /** DSH_HOME uses the raw nonblank value; trimming it would name a different path. */
381
+ export function dshHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
382
+ const raw = env.DSH_HOME;
383
+ if (raw === undefined || raw.trim().length === 0) return join(home, ".dsh");
384
+ if (raw === "~") return home;
385
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) return join(home, raw.slice(2));
386
+ if (!isAbsolute(raw)) {
387
+ throw new ClientPathError(
388
+ `DSH_HOME must be an absolute path or start with ~; "${raw}" depends on the working directory, `
389
+ + "so opencodex and DSH would disagree about which settings file it names.",
390
+ );
391
+ }
392
+ // DSH calls node:path.resolve after tilde expansion. Preserve the raw value
393
+ // for the decision above, then normalize the absolute spelling the same way
394
+ // so both processes bind ownership and locks to one path string.
395
+ return resolve(raw);
396
+ }
397
+
398
+ export function dshConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
399
+ return join(dshHomeDir(env, home), "settings.yaml");
400
+ }
401
+
379
402
  /**
380
403
  * One proxy-routed model destined for a client config. Deliberately narrower than
381
404
  * `CatalogModel` so a serializer cannot reach for a field that does not survive the
@@ -414,7 +437,8 @@ export type ExportClientId =
414
437
  | "hermes"
415
438
  | "openclaw"
416
439
  | "kimi"
417
- | "gajae";
440
+ | "gajae"
441
+ | "dsh";
418
442
 
419
443
  export interface ExportClientSpec {
420
444
  id: ExportClientId;
@@ -463,7 +487,8 @@ export interface ExportClientSpec {
463
487
  */
464
488
  function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
465
489
  if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
466
- return Math.floor(contextWindow);
490
+ const integer = Math.floor(contextWindow);
491
+ return integer > 0 ? integer : undefined;
467
492
  }
468
493
  return undefined;
469
494
  }
@@ -522,6 +547,18 @@ function inputModalitiesForClient(
522
547
  return kept.length > 0 ? kept : null;
523
548
  }
524
549
 
550
+ /** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */
551
+ function dshInputModalities(modalities: readonly string[] | undefined): string[] | null {
552
+ const declared = modalities ?? [];
553
+ if (declared.length === 0) return ["text"];
554
+ const kept: string[] = [];
555
+ for (const value of declared) {
556
+ if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value);
557
+ }
558
+ if (kept.length > 0) return kept;
559
+ return declared.every(value => value === "audio") ? null : ["text"];
560
+ }
561
+
525
562
  /**
526
563
  * Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
527
564
  * provider suffix is what makes two same-named models from different upstreams
@@ -770,6 +807,31 @@ export interface GajaeGeneratedConfig {
770
807
  providers: Record<string, GajaeProviderBlock>;
771
808
  }
772
809
 
810
+ export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
811
+ export type DshWireReasoningEffort = DshReasoningEffort | "ultra";
812
+
813
+ export interface DshModelEntry {
814
+ id: string;
815
+ name: string;
816
+ input: string[];
817
+ contextWindow?: number;
818
+ reasoningEfforts?: Partial<Record<DshReasoningEffort, DshWireReasoningEffort>>;
819
+ }
820
+
821
+ export interface DshProviderBlock {
822
+ displayName: "OpenCodex";
823
+ api: "openai-responses";
824
+ baseURL: string;
825
+ headers: { Authorization: "Bearer ocx_data_dsh" };
826
+ models: DshModelEntry[];
827
+ }
828
+
829
+ export interface DshGeneratedConfig {
830
+ "llm-pi-ai": {
831
+ providers: Record<string, DshProviderBlock>;
832
+ };
833
+ }
834
+
773
835
  /**
774
836
  * Pi's `~/.pi/agent/models.json` shape. `models` is an ARRAY (identity lives in `id`),
775
837
  * unlike OpenCode's keyed object.
@@ -975,6 +1037,84 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
975
1037
  };
976
1038
  }
977
1039
 
1040
+ const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"];
1041
+
1042
+ function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] {
1043
+ const offered = new Set<string>();
1044
+ for (const raw of model.reasoningEfforts ?? []) {
1045
+ const effort = raw.trim().toLowerCase();
1046
+ if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort);
1047
+ }
1048
+ if (offered.size === 0) return undefined;
1049
+ const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = [];
1050
+ for (const effort of DSH_EFFORT_ORDER) {
1051
+ if (effort !== "max") {
1052
+ if (offered.has(effort)) entries.push([effort, effort]);
1053
+ continue;
1054
+ }
1055
+ // DSH's key is the selectable level; the value is what it sends on the
1056
+ // wire. Preserve OpenCodex's `ultra` spelling when that is the only
1057
+ // highest effort, exactly like the rc.6 `max: ultra` contract.
1058
+ if (offered.has("max")) entries.push(["max", "max"]);
1059
+ else if (offered.has("ultra")) entries.push(["max", "ultra"]);
1060
+ }
1061
+ return Object.fromEntries(entries);
1062
+ }
1063
+
1064
+ function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean {
1065
+ const combos = (config as { combos?: unknown }).combos;
1066
+ if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false;
1067
+ const combo = (combos as Record<string, unknown>)[model.id];
1068
+ if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false;
1069
+ const targets = (combo as { targets?: unknown }).targets;
1070
+ if (!Array.isArray(targets) || targets.length === 0) return false;
1071
+ return targets.every(target => {
1072
+ if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
1073
+ const provider = (target as { provider?: unknown }).provider;
1074
+ const modelId = (target as { model?: unknown }).model;
1075
+ return typeof provider === "string"
1076
+ && provider.length > 0
1077
+ && provider === provider.trim()
1078
+ && provider !== "openai"
1079
+ && typeof modelId === "string"
1080
+ && modelId.length > 0
1081
+ && modelId === modelId.trim();
1082
+ });
1083
+ }
1084
+
1085
+ function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig {
1086
+ const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct";
1087
+ const models: DshModelEntry[] = [];
1088
+ for (const model of normalizeExportModels(ctx.models)) {
1089
+ if (direct && (model.native === true || model.provider === "openai")) continue;
1090
+ if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue;
1091
+ const input = dshInputModalities(model.inputModalities);
1092
+ if (input === null) continue;
1093
+ const contextWindow = authoritativeContextWindow(model.contextWindow);
1094
+ const reasoningEfforts = dshReasoningEfforts(model);
1095
+ models.push({
1096
+ id: model.namespaced,
1097
+ name: exportModelLabel(model),
1098
+ input,
1099
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
1100
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
1101
+ });
1102
+ }
1103
+ return {
1104
+ "llm-pi-ai": {
1105
+ providers: {
1106
+ [OPENCODE_PROVIDER_ID]: {
1107
+ displayName: "OpenCodex",
1108
+ api: "openai-responses",
1109
+ baseURL: ctx.baseUrl,
1110
+ headers: { Authorization: "Bearer ocx_data_dsh" },
1111
+ models,
1112
+ },
1113
+ },
1114
+ },
1115
+ };
1116
+ }
1117
+
978
1118
  /**
979
1119
  * Per-client model counts, read back off the SERIALIZED document rather than
980
1120
  * recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about
@@ -1019,6 +1159,11 @@ function summarizeGajae(document: unknown): { modelCount: number; modelsWithoutL
1019
1159
  return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
1020
1160
  }
1021
1161
 
1162
+ function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
1163
+ const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
1164
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
1165
+ }
1166
+
1022
1167
  /** One fragment at `path`, built from this client's own document. */
1023
1168
  function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
1024
1169
  return { clientId, fragments: [{ path, value }] };
@@ -1070,6 +1215,11 @@ function buildGajaeContribution(ctx: ExportContext): ManagedContribution {
1070
1215
  return singleFragment("gajae", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
1071
1216
  }
1072
1217
 
1218
+ function buildDshContribution(ctx: ExportContext): ManagedContribution {
1219
+ const doc = buildDshClientConfig(ctx);
1220
+ return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
1221
+ }
1222
+
1073
1223
  export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1074
1224
  opencode: {
1075
1225
  id: "opencode",
@@ -1167,6 +1317,18 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1167
1317
  // strict schema with no header field, so the dedicated header has nowhere to go
1168
1318
  loopbackOnly: true,
1169
1319
  },
1320
+ dsh: {
1321
+ id: "dsh",
1322
+ filename: "settings.yaml",
1323
+ destination: env => dshConfigPath(env),
1324
+ apiKeyEnv: "",
1325
+ exportHint: "DSH uses a non-secret loopback bearer placeholder in settings.yaml; loopback needs no key.",
1326
+ build: buildDshClientConfig,
1327
+ format: "yaml",
1328
+ summarize: summarizeDsh,
1329
+ buildContribution: buildDshContribution,
1330
+ loopbackOnly: true,
1331
+ },
1170
1332
  };
1171
1333
 
1172
1334
  export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "5c2d417ce3cb466651385bca4d8fee522e0bdf1c6d8c2fc1f84b4cf54574aee1"
13
+ "sha256": "b8810736c02b3cf34dbd0119082924f56a48e435cd9a0768c68c428f8e128cda"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -454,11 +454,11 @@
454
454
  },
455
455
  {
456
456
  "path": "src/cli/export-command.ts",
457
- "sha256": "c00b92f6cdaa188334e1635aa98b161bfbb2ce269f4435c4cfacff263478264d"
457
+ "sha256": "55489975589c8a411cab9d16835390f46a2059968c35344503aff9ffc4bc6e42"
458
458
  },
459
459
  {
460
460
  "path": "src/cli/help.ts",
461
- "sha256": "a696a035ce31447412ff5f025d07019ed445dd12c253c8a83ac5ee6c4679d1cb"
461
+ "sha256": "91c302e213e4d40d5612aee041c5a938d74a06c9a55310eccd45d2ceb08bce4a"
462
462
  },
463
463
  {
464
464
  "path": "src/cli/index.ts",
@@ -514,7 +514,7 @@
514
514
  },
515
515
  {
516
516
  "path": "src/cli/registry.ts",
517
- "sha256": "cf65a3022a6b8c97f7926f6057b69c47a9cdc71540798420fc5a06b861e64588"
517
+ "sha256": "1eb67cf2d3fb58a0b380d4a8cf71fee1ac3ac2123df6f52a4ee878da0e8a60db"
518
518
  },
519
519
  {
520
520
  "path": "src/cli/root.ts",
@@ -558,7 +558,7 @@
558
558
  },
559
559
  {
560
560
  "path": "src/clients/config-export.ts",
561
- "sha256": "65344216431c824d1e38d4cee44d955ac4d6115e19e3ff784582bfabec2df70a"
561
+ "sha256": "4631957993e4d31bc41ecd56791ffed1e6356e3dd9d5107aca1bc9e4c5297de8"
562
562
  },
563
563
  {
564
564
  "path": "src/codex/account-id.ts",
@@ -1054,7 +1054,7 @@
1054
1054
  },
1055
1055
  {
1056
1056
  "path": "src/integrations/omp-yaml-source.ts",
1057
- "sha256": "58a8769edc6c1f409f2851955e52dba3cdb5e30938d6b7dc19e59b900731d219"
1057
+ "sha256": "ba30982af0520ffe5c841b7254ab6d73e2a9fb53636f3c5096c9fa6ccfc41005"
1058
1058
  },
1059
1059
  {
1060
1060
  "path": "src/integrations/ownership.ts",
@@ -1062,7 +1062,7 @@
1062
1062
  },
1063
1063
  {
1064
1064
  "path": "src/integrations/registry.ts",
1065
- "sha256": "5666d2e9e1a9086074cd99a9ad66cb6c649a470182b3c7bad4e8cc785b3d87f2"
1065
+ "sha256": "299064969a383a4202e5c243d52d7b8f677a675db04a0661f66be01ae74e25d6"
1066
1066
  },
1067
1067
  {
1068
1068
  "path": "src/integrations/serialize.ts",
@@ -1070,15 +1070,19 @@
1070
1070
  },
1071
1071
  {
1072
1072
  "path": "src/integrations/state.ts",
1073
- "sha256": "e2446f333ab3b57eebc120bf674f37aff6210cde7cf45628e7ff0ca77ca4d4f0"
1073
+ "sha256": "2b31611ea745ae22d4292719c2deb453efe6d5ea0cecba62d6d4352a30744e77"
1074
1074
  },
1075
1075
  {
1076
1076
  "path": "src/integrations/store.ts",
1077
1077
  "sha256": "195fe1af41fdaf567bfdaef5c457ee2452a77c43fc13475fccc7968676a2b199"
1078
1078
  },
1079
+ {
1080
+ "path": "src/integrations/writer-lock.ts",
1081
+ "sha256": "4e133b69db94784460351a2ce947172fd6499235029fdf866fa3eff2afc477b5"
1082
+ },
1079
1083
  {
1080
1084
  "path": "src/integrations/writer.ts",
1081
- "sha256": "92cc7c85a6bffa520f0fce57890f45758c712f69a128451be60368444d5ff725"
1085
+ "sha256": "788709e9e0505cc717f5c015110d8fa17246dc5b33185fafc99d8136236f1606"
1082
1086
  },
1083
1087
  {
1084
1088
  "path": "src/lab/artifacts/sanitize.ts",
@@ -1634,7 +1638,7 @@
1634
1638
  },
1635
1639
  {
1636
1640
  "path": "src/lib/shadow-call.ts",
1637
- "sha256": "c7e58016fa10385efb4acf23660859f7739afd945c25f593fc774da550316197"
1641
+ "sha256": "2e55cf6e560b18337ac1911b38f20a27c1bd985a65e9496783f2a7cf85014bc6"
1638
1642
  },
1639
1643
  {
1640
1644
  "path": "src/lib/sidecar-tracker.ts",
@@ -2190,7 +2194,7 @@
2190
2194
  },
2191
2195
  {
2192
2196
  "path": "src/server/management/integration-routes.ts",
2193
- "sha256": "37d1cdb0e3f993c0cc190e7079dea20ba6daafcfcde314f4f37fdc93bcdcbf79"
2197
+ "sha256": "a049e2dd40f82793c33b7ac584f14ae6ead7e2524a5b1ee3a218caddc9a3bd86"
2194
2198
  },
2195
2199
  {
2196
2200
  "path": "src/server/management/lab-automation-routes.ts",
@@ -2358,7 +2362,7 @@
2358
2362
  },
2359
2363
  {
2360
2364
  "path": "src/server/responses/core.ts",
2361
- "sha256": "0817eb37ba0ce9425afd9b9794eccab1095b7563d217652562cb281479f1df48"
2365
+ "sha256": "b8e42c1e0be113256d7318ee6dc3dc4cb7d045867e9e5412ccdfd64fa07111cb"
2362
2366
  },
2363
2367
  {
2364
2368
  "path": "src/server/responses/encrypted-payload.ts",
@@ -2506,7 +2510,7 @@
2506
2510
  },
2507
2511
  {
2508
2512
  "path": "src/types.ts",
2509
- "sha256": "33f4ee11228bb8faca03c57fdd5000512f70153c2fd446f88d78b0a43056d330"
2513
+ "sha256": "b04403f46247c96634d0c8c6344c1ecc44979980b0456c28140e3aa1da4ab6ca"
2510
2514
  },
2511
2515
  {
2512
2516
  "path": "src/update/badge.ts",