@textcortex/zenocode 0.1.12 → 0.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/zenocode",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Secure, EU-hosted coding agent for TextCortex customers that runs in your terminal, edits files, runs scripts, and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -139,21 +139,55 @@ export function patchOpenCodeVersionFooterText(text) {
139
139
  return { patched, text: next };
140
140
  }
141
141
 
142
+ export function patchOpenCodeDisplayNameText(text) {
143
+ const replacements = [
144
+ ['TD("Uninstall OpenCode")', 'TD("Uninstall Zenocode")'],
145
+ ['"name":"OpenCode"', '"name":"Zenocode"'],
146
+ ['"short_name":"OpenCode"', '"short_name":"Zenocode"'],
147
+ ];
148
+
149
+ let patched = false;
150
+ let nextText = text;
151
+ for (const [target, replacement] of replacements) {
152
+ if (!nextText.includes(target)) {
153
+ continue;
154
+ }
155
+ nextText = nextText.replaceAll(target, replacement);
156
+ patched = true;
157
+ }
158
+
159
+ return { patched, text: nextText };
160
+ }
161
+
142
162
  export function patchZenocodeBinaryText(text) {
143
163
  let patched = false;
144
164
  let nextText = text;
165
+ const patches = {
166
+ logo: false,
167
+ footer: false,
168
+ displayName: false,
169
+ };
145
170
 
146
171
  const logoPatch = patchLogoSnippetText(nextText);
147
172
  if (logoPatch.patched) {
148
173
  nextText = logoPatch.text;
149
174
  patched = true;
175
+ patches.logo = true;
150
176
  }
151
177
 
152
178
  const footerPatch = patchOpenCodeVersionFooterText(nextText);
153
179
  if (footerPatch.patched) {
154
180
  nextText = footerPatch.text;
155
181
  patched = true;
182
+ patches.footer = true;
156
183
  }
157
184
 
158
- return { patched, text: nextText };
185
+ const displayNamePatch = patchOpenCodeDisplayNameText(nextText);
186
+ if (displayNamePatch.patched) {
187
+ nextText = displayNamePatch.text;
188
+ patched = true;
189
+ patches.displayName = true;
190
+ }
191
+
192
+ return { patched, patches, text: nextText };
159
193
  }
@@ -1,6 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { padBinaryReplacement, patchOpenCodeVersionFooterText } from "./branding-patch.mjs";
3
+ import {
4
+ padBinaryReplacement,
5
+ patchOpenCodeDisplayNameText,
6
+ patchOpenCodeVersionFooterText,
7
+ patchZenocodeBinaryText,
8
+ } from "./branding-patch.mjs";
4
9
 
5
10
  test("padBinaryReplacement keeps binary length stable", () => {
6
11
  const padded = padBinaryReplacement("abcdef", "abc");
@@ -54,3 +59,31 @@ test("patchOpenCodeVersionFooterText is a no-op when already branded", () => {
54
59
  assert.equal(result.patched, false);
55
60
  assert.equal(result.text, branded);
56
61
  });
62
+
63
+ test("patchOpenCodeDisplayNameText rewrites current compiled display names", () => {
64
+ const text = [
65
+ 'TD("Uninstall OpenCode");"name":"OpenCode","short_name":"OpenCode"',
66
+ '"Powered by OpenCode"',
67
+ ].join(";");
68
+
69
+ const result = patchOpenCodeDisplayNameText(text);
70
+
71
+ assert.equal(result.patched, true);
72
+ assert.equal(result.text.length, text.length);
73
+ assert.equal(
74
+ result.text,
75
+ 'TD("Uninstall Zenocode");"name":"Zenocode","short_name":"Zenocode";"Powered by OpenCode"',
76
+ );
77
+ });
78
+
79
+ test("patchZenocodeBinaryText reports display-name-only branding separately", () => {
80
+ const result = patchZenocodeBinaryText('"name":"OpenCode"');
81
+
82
+ assert.equal(result.patched, true);
83
+ assert.deepEqual(result.patches, {
84
+ logo: false,
85
+ footer: false,
86
+ displayName: true,
87
+ });
88
+ assert.equal(result.text, '"name":"Zenocode"');
89
+ });
@@ -7,17 +7,27 @@ import path from "node:path";
7
7
  import process from "node:process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { patchZenocodeBinaryText } from "./branding-patch.mjs";
10
+ import { openCodeRef, openCodeVersion } from "./opencode-version.mjs";
10
11
 
11
12
  const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
12
13
  const __dirname = path.dirname(currentFilePath);
13
14
  const appRoot = path.resolve(__dirname, "..");
14
15
  const defaultOutputDir = path.join(appRoot, ".zenocode", "brand-build");
16
+ export const defaultOpenCodeForkUrl = "https://github.com/sst/opencode";
17
+ export const defaultOpenCodeRef = openCodeRef;
18
+ export const defaultOpenCodeVersion = openCodeVersion;
19
+ export const defaultOpenCodeBuildArgs = ["--skip-install"];
20
+ const displayNameOnlyBrandingRefs = new Set([defaultOpenCodeRef]);
15
21
 
16
22
  const forkUrl =
17
23
  process.env.ZENOCODE_OPENCODE_FORK_URL ||
18
24
  process.env.CODECORTEX_OPENCODE_FORK_URL ||
19
- "https://github.com/anomalyco/opencode";
20
- const forkRef = (process.env.ZENOCODE_OPENCODE_REF || process.env.CODECORTEX_OPENCODE_REF || "").trim();
25
+ defaultOpenCodeForkUrl;
26
+ const forkRef = (
27
+ process.env.ZENOCODE_OPENCODE_REF ||
28
+ process.env.CODECORTEX_OPENCODE_REF ||
29
+ defaultOpenCodeRef
30
+ ).trim();
21
31
  const outputDir =
22
32
  process.env.ZENOCODE_BRANDED_OUTPUT_DIR ||
23
33
  process.env.CODECORTEX_BRANDED_OUTPUT_DIR ||
@@ -36,16 +46,25 @@ const publishTag =
36
46
  process.env.ZENOCODE_PUBLISH_TAG ||
37
47
  process.env.CODECORTEX_PUBLISH_TAG ||
38
48
  "latest";
39
- const buildArgs = (
49
+ const configuredBuildArgs = (
40
50
  process.env.ZENOCODE_OPENCODE_BUILD_ARGS ||
41
51
  process.env.CODECORTEX_OPENCODE_BUILD_ARGS ||
42
52
  ""
43
- )
44
- .trim()
45
- .split(/\s+/)
46
- .filter(Boolean);
53
+ ).trim();
54
+ const buildArgs = configuredBuildArgs
55
+ ? configuredBuildArgs.split(/\s+/).filter(Boolean)
56
+ : defaultOpenCodeBuildArgs;
47
57
  const cliArgs = process.argv.slice(2);
48
58
 
59
+ export function deriveOpenCodeVersionFromRef(ref, fallbackVersion = defaultOpenCodeVersion) {
60
+ const match = ref.match(/^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?$/);
61
+ return match?.[1] || fallbackVersion;
62
+ }
63
+
64
+ export function allowsDisplayNameOnlyBranding(ref) {
65
+ return displayNameOnlyBrandingRefs.has(ref);
66
+ }
67
+
49
68
  function _command(name) {
50
69
  if (process.platform === "win32" && ["npm", "pnpm", "npx"].includes(name)) {
51
70
  return `${name}.cmd`;
@@ -270,6 +289,13 @@ function _binaryFilename() {
270
289
  return process.platform === "win32" ? "opencode.exe" : "opencode";
271
290
  }
272
291
 
292
+ async function adHocSignBinary(binaryPath) {
293
+ if (process.platform !== "darwin") return;
294
+ await run("codesign", ["--force", "--sign", "-", binaryPath], {
295
+ stdio: "ignore",
296
+ });
297
+ }
298
+
273
299
  async function patchBinaryAtPath(binaryPath) {
274
300
  const buffer = await fs.readFile(binaryPath);
275
301
  const originalLength = buffer.length;
@@ -277,6 +303,12 @@ async function patchBinaryAtPath(binaryPath) {
277
303
  if (!patch.patched) {
278
304
  throw new Error(`Branding patch did not match binary ${binaryPath}`);
279
305
  }
306
+ const patchedMandatoryBranding = patch.patches.logo || patch.patches.footer;
307
+ if (!patchedMandatoryBranding && !allowsDisplayNameOnlyBranding(forkRef)) {
308
+ throw new Error(
309
+ `Branding patch only matched display names for ${binaryPath}; update the logo/footer patch signatures before building ${forkRef}.`,
310
+ );
311
+ }
280
312
  const nextBuffer = Buffer.from(patch.text, "latin1");
281
313
  if (nextBuffer.length !== originalLength) {
282
314
  throw new Error(`Branding patch changed binary length for ${binaryPath}`);
@@ -285,6 +317,7 @@ async function patchBinaryAtPath(binaryPath) {
285
317
  if (process.platform !== "win32") {
286
318
  await fs.chmod(binaryPath, 0o755);
287
319
  }
320
+ await adHocSignBinary(binaryPath);
288
321
  }
289
322
 
290
323
  function _buildWrapperExecutable({ runtimePackageName, binName }) {
@@ -636,7 +669,16 @@ async function main() {
636
669
  await run(
637
670
  bunCommand,
638
671
  ["./packages/opencode/script/build.ts", "--single", ...buildArgs],
639
- { cwd: checkoutDir },
672
+ {
673
+ cwd: checkoutDir,
674
+ env: {
675
+ ...process.env,
676
+ OPENCODE_CHANNEL: process.env.OPENCODE_CHANNEL || "latest",
677
+ OPENCODE_VERSION:
678
+ process.env.OPENCODE_VERSION ||
679
+ deriveOpenCodeVersionFromRef(forkRef, defaultOpenCodeVersion),
680
+ },
681
+ },
640
682
  );
641
683
 
642
684
  await fs.mkdir(artifactDir, { recursive: true });
@@ -3,9 +3,33 @@ import test from "node:test";
3
3
  import {
4
4
  buildPublishCommandArgs,
5
5
  buildWrapperBinMap,
6
+ allowsDisplayNameOnlyBranding,
7
+ defaultOpenCodeBuildArgs,
8
+ defaultOpenCodeForkUrl,
9
+ defaultOpenCodeRef,
10
+ defaultOpenCodeVersion,
11
+ deriveOpenCodeVersionFromRef,
6
12
  mapBrandedBinaryPackageName,
7
13
  } from "./build-branded-opencode.mjs";
8
14
 
15
+ test("branded OpenCode build defaults to the latest stable upstream release", () => {
16
+ assert.equal(defaultOpenCodeForkUrl, "https://github.com/sst/opencode");
17
+ assert.equal(defaultOpenCodeRef, "v1.17.6");
18
+ assert.equal(defaultOpenCodeVersion, "1.17.6");
19
+ assert.deepEqual(defaultOpenCodeBuildArgs, ["--skip-install"]);
20
+ });
21
+
22
+ test("deriveOpenCodeVersionFromRef follows explicit tag overrides", () => {
23
+ assert.equal(deriveOpenCodeVersionFromRef("v1.17.5"), "1.17.5");
24
+ assert.equal(deriveOpenCodeVersionFromRef("1.18.0-beta.1"), "1.18.0-beta.1");
25
+ assert.equal(deriveOpenCodeVersionFromRef("main", "1.17.4"), "1.17.4");
26
+ });
27
+
28
+ test("display-name-only branding is limited to reviewed upstream refs", () => {
29
+ assert.equal(allowsDisplayNameOnlyBranding(defaultOpenCodeRef), true);
30
+ assert.equal(allowsDisplayNameOnlyBranding("v1.17.7"), false);
31
+ });
32
+
9
33
  test("mapBrandedBinaryPackageName scopes runtime binaries under zenocode", () => {
10
34
  assert.equal(
11
35
  mapBrandedBinaryPackageName("opencode-darwin-arm64", "@textcortex/zenocode-ai"),
@@ -0,0 +1,4 @@
1
+ export const openCodePackageName = "opencode-ai";
2
+ export const openCodeVersion = "1.17.6";
3
+ export const openCodeRef = `v${openCodeVersion}`;
4
+ export const openCodePackageSpec = `${openCodePackageName}@${openCodeVersion}`;
@@ -11,6 +11,10 @@ import {
11
11
  patchZenocodeBinaryText,
12
12
  zenocodeLogo,
13
13
  } from "./branding-patch.mjs";
14
+ import {
15
+ openCodePackageName,
16
+ openCodePackageSpec,
17
+ } from "./opencode-version.mjs";
14
18
 
15
19
  const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
16
20
  const __dirname = path.dirname(currentFilePath);
@@ -40,19 +44,21 @@ const configuredOpencodePackage =
40
44
  process.env.CODECORTEX_OPENCODE_PACKAGE ||
41
45
  process.env.OPENCODE_PACKAGE ||
42
46
  null;
43
- const defaultBrandedOpencodePackage = "@textcortex/zenocode-ai";
47
+ const defaultBrandedOpencodePackage = openCodePackageSpec;
44
48
  const legacyBrandedOpencodePackage = "@textcortex/opencode-ai";
45
- const fallbackOpencodePackage = "opencode-ai";
49
+ const fallbackOpencodePackage = openCodePackageName;
46
50
  const opencodePackage = configuredOpencodePackage || defaultBrandedOpencodePackage;
47
51
  const opencodeBinaryPath =
48
52
  process.env.ZENOCODE_OPENCODE_BIN_PATH ||
49
53
  process.env.CODECORTEX_OPENCODE_BIN_PATH ||
50
54
  "";
51
- const oauthInitiatePath = "/internal/v1/fastapi/zenocode/oauth2/initiate";
52
- const oauthTokenPath = "/internal/v1/fastapi/zenocode/oauth2/token";
55
+ const oauthInitiatePath = "/internal/v2/fastapi/zenocode/oauth2/initiate";
56
+ const oauthTokenPath = "/internal/v2/fastapi/zenocode/oauth2/token";
53
57
  const defaultOrder = [
58
+ "minimax-m3-thinking",
59
+ "kimi-k2-6",
54
60
  "kimi-k2-5-thinking",
55
- "glm-5",
61
+ "glm-5-1",
56
62
  "gpt-5-2",
57
63
  "gpt-5-1",
58
64
  "gpt-5",
@@ -403,7 +409,7 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
403
409
  [providerID]: {
404
410
  name: "Zenocode",
405
411
  options: {
406
- baseURL: new URL("/internal/v1/fastapi/zenocode/v1", baseUrl).toString(),
412
+ baseURL: new URL("/internal/v2/fastapi/zenocode/v1", baseUrl).toString(),
407
413
  },
408
414
  },
409
415
  // Older fallback opencode-ai builds can load the Codex auth plugin when
@@ -464,7 +470,7 @@ async function requestJson(url, init) {
464
470
  }
465
471
 
466
472
  async function prepareRuntime(baseUrl, token) {
467
- const modelsUrl = new URL("/internal/v1/fastapi/zenocode/models/api.json", baseUrl).toString();
473
+ const modelsUrl = new URL("/internal/v2/fastapi/zenocode/models/api.json", baseUrl).toString();
468
474
  const { response, payload, text } = await requestJson(modelsUrl, {
469
475
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
470
476
  });
@@ -954,11 +960,26 @@ function _runtimeBrandedBinaryPath() {
954
960
  return path.join(runtimeDir, "bin", binaryName);
955
961
  }
956
962
 
963
+ export function packageNameFromSpecifier(packageSpecifier) {
964
+ if (!packageSpecifier.includes("@")) {
965
+ return packageSpecifier;
966
+ }
967
+ if (!packageSpecifier.startsWith("@")) {
968
+ return packageSpecifier.split("@")[0];
969
+ }
970
+
971
+ const versionSeparator = packageSpecifier.lastIndexOf("@");
972
+ return versionSeparator > packageSpecifier.indexOf("/")
973
+ ? packageSpecifier.slice(0, versionSeparator)
974
+ : packageSpecifier;
975
+ }
976
+
957
977
  function shouldPatchOpencodeRuntimePackage(packageName) {
978
+ const runtimePackageName = packageNameFromSpecifier(packageName);
958
979
  return (
959
- packageName.endsWith("/opencode-ai") ||
960
- packageName.endsWith("/zenocode-ai") ||
961
- packageName === "opencode-ai"
980
+ runtimePackageName.endsWith("/opencode-ai") ||
981
+ runtimePackageName.endsWith("/zenocode-ai") ||
982
+ runtimePackageName === openCodePackageName
962
983
  );
963
984
  }
964
985
 
@@ -999,6 +1020,88 @@ async function _collectPnpmDlxPnpmDirs(rootDir) {
999
1020
  return pnpmDirs;
1000
1021
  }
1001
1022
 
1023
+ async function _collectNodeModulesDirs(rootDir) {
1024
+ const queue = [{ dir: rootDir, depth: 0 }];
1025
+ const nodeModulesDirs = [];
1026
+
1027
+ while (queue.length) {
1028
+ const current = queue.shift();
1029
+ const nodeModulesPath = path.join(current.dir, "node_modules");
1030
+ if (await _pathExists(nodeModulesPath)) {
1031
+ nodeModulesDirs.push(nodeModulesPath);
1032
+ }
1033
+
1034
+ if (current.depth >= 3) continue;
1035
+ let entries = [];
1036
+ try {
1037
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
1038
+ } catch {
1039
+ continue;
1040
+ }
1041
+ for (const entry of entries) {
1042
+ if (!entry.isDirectory()) continue;
1043
+ if (entry.name === "node_modules") continue;
1044
+ queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
1045
+ }
1046
+ }
1047
+
1048
+ return nodeModulesDirs;
1049
+ }
1050
+
1051
+ async function _collectOpencodeBinariesFromNodeModules(nodeModulesPath) {
1052
+ const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
1053
+ const hiddenCachedBinaryName = ".opencode";
1054
+ const candidates = [];
1055
+
1056
+ let topLevel = [];
1057
+ try {
1058
+ topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
1059
+ } catch {
1060
+ return candidates;
1061
+ }
1062
+
1063
+ for (const moduleEntry of topLevel) {
1064
+ if (!moduleEntry.isDirectory()) continue;
1065
+
1066
+ if (moduleEntry.name === openCodePackageName) {
1067
+ const cachedPath = path.join(nodeModulesPath, openCodePackageName, "bin", hiddenCachedBinaryName);
1068
+ if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1069
+ continue;
1070
+ }
1071
+
1072
+ if (moduleEntry.name.startsWith("opencode-")) {
1073
+ const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
1074
+ if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1075
+ continue;
1076
+ }
1077
+
1078
+ if (!moduleEntry.name.startsWith("@")) continue;
1079
+ const scopePath = path.join(nodeModulesPath, moduleEntry.name);
1080
+ let scopedPackages = [];
1081
+ try {
1082
+ scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
1083
+ } catch {
1084
+ continue;
1085
+ }
1086
+ for (const scopedPackage of scopedPackages) {
1087
+ if (!scopedPackage.isDirectory()) continue;
1088
+ const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
1089
+
1090
+ if (scopedPackage.name === "opencode-ai") {
1091
+ const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
1092
+ if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1093
+ continue;
1094
+ }
1095
+
1096
+ if (!scopedPackage.name.startsWith("opencode-")) continue;
1097
+ const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
1098
+ if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1099
+ }
1100
+ }
1101
+
1102
+ return candidates;
1103
+ }
1104
+
1002
1105
  async function _collectPnpmDlxOpencodeBinaries() {
1003
1106
  const dlxRoots = [
1004
1107
  path.join(os.homedir(), "Library", "Caches", "pnpm", "dlx"),
@@ -1006,8 +1109,6 @@ async function _collectPnpmDlxOpencodeBinaries() {
1006
1109
  path.join(process.env.LOCALAPPDATA || "", "pnpm", "dlx"),
1007
1110
  ].filter(Boolean);
1008
1111
 
1009
- const binaryName = process.platform === "win32" ? "opencode.exe" : "opencode";
1010
- const hiddenCachedBinaryName = ".opencode";
1011
1112
  const candidates = [];
1012
1113
 
1013
1114
  for (const root of dlxRoots) {
@@ -1026,51 +1127,7 @@ async function _collectPnpmDlxOpencodeBinaries() {
1026
1127
  const nodeModulesPath = path.join(pnpmDir, entry.name, "node_modules");
1027
1128
  if (!(await _pathExists(nodeModulesPath))) continue;
1028
1129
 
1029
- let topLevel = [];
1030
- try {
1031
- topLevel = await fs.readdir(nodeModulesPath, { withFileTypes: true });
1032
- } catch {
1033
- continue;
1034
- }
1035
-
1036
- for (const moduleEntry of topLevel) {
1037
- if (!moduleEntry.isDirectory()) continue;
1038
-
1039
- if (moduleEntry.name === "opencode-ai") {
1040
- const cachedPath = path.join(nodeModulesPath, "opencode-ai", "bin", hiddenCachedBinaryName);
1041
- if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1042
- continue;
1043
- }
1044
-
1045
- if (moduleEntry.name.startsWith("opencode-")) {
1046
- const binaryPath = path.join(nodeModulesPath, moduleEntry.name, "bin", binaryName);
1047
- if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1048
- continue;
1049
- }
1050
-
1051
- if (!moduleEntry.name.startsWith("@")) continue;
1052
- const scopePath = path.join(nodeModulesPath, moduleEntry.name);
1053
- let scopedPackages = [];
1054
- try {
1055
- scopedPackages = await fs.readdir(scopePath, { withFileTypes: true });
1056
- } catch {
1057
- continue;
1058
- }
1059
- for (const scopedPackage of scopedPackages) {
1060
- if (!scopedPackage.isDirectory()) continue;
1061
- const scopedNodeModulesPath = path.join(scopePath, scopedPackage.name);
1062
-
1063
- if (scopedPackage.name === "opencode-ai") {
1064
- const cachedPath = path.join(scopedNodeModulesPath, "bin", hiddenCachedBinaryName);
1065
- if (await _pathExists(cachedPath)) candidates.push(cachedPath);
1066
- continue;
1067
- }
1068
-
1069
- if (!scopedPackage.name.startsWith("opencode-")) continue;
1070
- const binaryPath = path.join(scopedNodeModulesPath, "bin", binaryName);
1071
- if (await _pathExists(binaryPath)) candidates.push(binaryPath);
1072
- }
1073
- }
1130
+ candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesPath));
1074
1131
  }
1075
1132
  }
1076
1133
  }
@@ -1078,6 +1135,44 @@ async function _collectPnpmDlxOpencodeBinaries() {
1078
1135
  return [...new Set(candidates)];
1079
1136
  }
1080
1137
 
1138
+ async function _collectNpxOpencodeBinaries() {
1139
+ const npxRoots = [
1140
+ path.join(os.homedir(), ".npm", "_npx"),
1141
+ path.join(process.env.LOCALAPPDATA || "", "npm-cache", "_npx"),
1142
+ ].filter(Boolean);
1143
+ const candidates = [];
1144
+
1145
+ for (const root of npxRoots) {
1146
+ if (!(await _pathExists(root))) continue;
1147
+ const nodeModulesDirs = await _collectNodeModulesDirs(root);
1148
+ for (const nodeModulesDir of nodeModulesDirs) {
1149
+ candidates.push(...await _collectOpencodeBinariesFromNodeModules(nodeModulesDir));
1150
+ }
1151
+ }
1152
+
1153
+ return [...new Set(candidates)];
1154
+ }
1155
+
1156
+ function _buildPackageWarmupArgs(packageName, runner) {
1157
+ if (runner.command === _runnerCommand("pnpm")) {
1158
+ return ["dlx", packageName, "--version"];
1159
+ }
1160
+ if (runner.command === _runnerCommand("npx")) {
1161
+ return ["--yes", packageName, "--version"];
1162
+ }
1163
+ return null;
1164
+ }
1165
+
1166
+ async function _collectRunnerOpencodeBinaries(runner) {
1167
+ if (runner.command === _runnerCommand("pnpm")) {
1168
+ return _collectPnpmDlxOpencodeBinaries();
1169
+ }
1170
+ if (runner.command === _runnerCommand("npx")) {
1171
+ return _collectNpxOpencodeBinaries();
1172
+ }
1173
+ return [];
1174
+ }
1175
+
1081
1176
  async function _adHocSignBinary(binaryPath) {
1082
1177
  if (process.platform !== "darwin") return;
1083
1178
  try {
@@ -1143,9 +1238,6 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
1143
1238
  if (!shouldPatchOpencodeRuntimePackage(packageName)) {
1144
1239
  return null;
1145
1240
  }
1146
- if (runner.command !== _runnerCommand("pnpm")) {
1147
- return null;
1148
- }
1149
1241
  if (
1150
1242
  process.env.ZENOCODE_DISABLE_OPENCODE_LOGO_PATCH === "1" ||
1151
1243
  process.env.CODECORTEX_DISABLE_OPENCODE_LOGO_PATCH === "1"
@@ -1153,19 +1245,18 @@ async function _ensurePatchedOpencodeDlxBinaries(packageName, runner, options) {
1153
1245
  return null;
1154
1246
  }
1155
1247
 
1156
- let binaryCandidates = await _collectPnpmDlxOpencodeBinaries();
1157
- if (!binaryCandidates.length) {
1158
- // Warm cache so we have a concrete binary to patch.
1159
- try {
1160
- await runChild(runner.command, ["dlx", packageName, "--version"], {
1248
+ const warmupArgs = _buildPackageWarmupArgs(packageName, runner);
1249
+ try {
1250
+ if (warmupArgs) {
1251
+ await runChild(runner.command, warmupArgs, {
1161
1252
  ...options,
1162
1253
  stdio: "ignore",
1163
1254
  });
1164
- } catch {
1165
- // ignore warm-up failures and continue with best effort patching
1166
1255
  }
1167
- binaryCandidates = await _collectPnpmDlxOpencodeBinaries();
1256
+ } catch {
1257
+ // ignore warm-up failures and continue with best effort patching
1168
1258
  }
1259
+ const binaryCandidates = await _collectRunnerOpencodeBinaries(runner);
1169
1260
 
1170
1261
  if (!binaryCandidates.length) {
1171
1262
  return null;
@@ -1198,6 +1289,22 @@ export function buildPackageLauncherChildOptions(options, pinnedRuntimePath) {
1198
1289
  };
1199
1290
  }
1200
1291
 
1292
+ export function buildPackageLauncherInvocation({ runner, args, pinnedRuntimePath }) {
1293
+ if (pinnedRuntimePath) {
1294
+ return {
1295
+ command: pinnedRuntimePath,
1296
+ args,
1297
+ usePinnedRuntime: true,
1298
+ };
1299
+ }
1300
+
1301
+ return {
1302
+ command: runner.command,
1303
+ args: runner.args,
1304
+ usePinnedRuntime: false,
1305
+ };
1306
+ }
1307
+
1201
1308
  async function runPackageLauncher(packageName, args, options) {
1202
1309
  const runners = [
1203
1310
  { command: _runnerCommand("pnpm"), args: ["dlx", packageName, ...args] },
@@ -1215,9 +1322,17 @@ async function runPackageLauncher(packageName, args, options) {
1215
1322
  }
1216
1323
  }
1217
1324
 
1218
- const childOptions = buildPackageLauncherChildOptions(options, pinnedRuntimePath);
1325
+ const invocation = buildPackageLauncherInvocation({
1326
+ runner,
1327
+ args,
1328
+ pinnedRuntimePath,
1329
+ });
1330
+ const childOptions = buildPackageLauncherChildOptions(
1331
+ options,
1332
+ invocation.usePinnedRuntime ? null : pinnedRuntimePath,
1333
+ );
1219
1334
 
1220
- const result = await runChild(runner.command, runner.args, childOptions);
1335
+ const result = await runChild(invocation.command, invocation.args, childOptions);
1221
1336
  if (result.signal) {
1222
1337
  process.kill(process.pid, result.signal);
1223
1338
  return;
@@ -1363,10 +1478,11 @@ export async function runRuntimeWithSessionRecovery({
1363
1478
  }
1364
1479
 
1365
1480
  async function packageExistsOnNpm(packageName) {
1481
+ const packageSpecName = packageNameFromSpecifier(packageName);
1366
1482
  const controller = new AbortController();
1367
1483
  const timeout = setTimeout(() => controller.abort(), 4_000);
1368
1484
  try {
1369
- const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
1485
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageSpecName)}`, {
1370
1486
  method: "GET",
1371
1487
  headers: { Accept: "application/json" },
1372
1488
  signal: controller.signal,
@@ -1424,6 +1540,11 @@ function shouldRenderBanner(args) {
1424
1540
  return !args.some((arg) => suppressFlags.has(arg));
1425
1541
  }
1426
1542
 
1543
+ export function shouldBypassZenocodePreparation(args) {
1544
+ const metadataFlags = new Set(["--help", "-h", "--version", "-v", "completion"]);
1545
+ return metadataFlags.has(args[0]);
1546
+ }
1547
+
1427
1548
  function maybeRenderBanner(args) {
1428
1549
  if (!shouldRenderBanner(args)) {
1429
1550
  return;
@@ -1617,6 +1738,15 @@ async function main() {
1617
1738
  }
1618
1739
 
1619
1740
  maybeRenderBanner(runtimeArgs);
1741
+ if (shouldBypassZenocodePreparation(runtimeArgs)) {
1742
+ const launchPackage = await resolveLaunchPackage();
1743
+ await runPackageLauncher(launchPackage, runtimeArgs, {
1744
+ cwd: process.cwd(),
1745
+ env: { ...process.env },
1746
+ });
1747
+ return;
1748
+ }
1749
+
1620
1750
  const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
1621
1751
  preferLocalhost,
1622
1752
  });
@@ -11,32 +11,56 @@ import {
11
11
  } from "./branding-patch.mjs";
12
12
  import {
13
13
  buildOpenCodeConfig,
14
+ buildPackageLauncherInvocation,
14
15
  buildPackageLauncherChildOptions,
15
16
  canRecoverRuntimeSessionFromTranscript,
16
17
  buildZenocodeBanner,
17
18
  chooseDefaults,
18
19
  hasLocalBaseUrlFlag,
20
+ packageNameFromSpecifier,
19
21
  resolveLoginBaseUrl,
20
22
  resolveLoginSuccessIdentifier,
21
23
  resolveTextCortexBaseUrl,
22
24
  runRuntimeWithSessionRecovery,
25
+ shouldBypassZenocodePreparation,
23
26
  shouldFallbackLoginToCloud,
24
27
  writePrivateJsonFile,
25
28
  } from "./run-zenocode.mjs";
26
29
 
27
- test("chooseDefaults prefers kimi k2.5 thinking for Zenocode", () => {
30
+ test("chooseDefaults prefers MiniMax M3 thinking for Zenocode", () => {
28
31
  const defaults = chooseDefaults({
29
- "glm-5": {},
30
- "kimi-k2-5-thinking": {},
32
+ "glm-5-1": {},
33
+ "kimi-k2-6": {},
34
+ "minimax-m3-thinking": {},
31
35
  "gpt-5-2": {},
32
36
  });
33
37
 
34
38
  assert.deepEqual(defaults, {
35
- model: "kimi-k2-5-thinking",
36
- smallModel: "kimi-k2-5-thinking",
39
+ model: "minimax-m3-thinking",
40
+ smallModel: "minimax-m3-thinking",
37
41
  });
38
42
  });
39
43
 
44
+ test("packageNameFromSpecifier strips npm versions without breaking scopes", () => {
45
+ assert.equal(packageNameFromSpecifier("opencode-ai@1.17.6"), "opencode-ai");
46
+ assert.equal(
47
+ packageNameFromSpecifier("@textcortex/zenocode-ai@1.17.6"),
48
+ "@textcortex/zenocode-ai",
49
+ );
50
+ assert.equal(
51
+ packageNameFromSpecifier("@textcortex/zenocode-ai"),
52
+ "@textcortex/zenocode-ai",
53
+ );
54
+ });
55
+
56
+ test("shouldBypassZenocodePreparation skips auth for runtime metadata commands", () => {
57
+ assert.equal(shouldBypassZenocodePreparation(["--version"]), true);
58
+ assert.equal(shouldBypassZenocodePreparation(["completion", "zsh"]), true);
59
+ assert.equal(shouldBypassZenocodePreparation(["run"]), false);
60
+ assert.equal(shouldBypassZenocodePreparation(["run", "implement", "completion"]), false);
61
+ assert.equal(shouldBypassZenocodePreparation(["run", "--help"]), false);
62
+ });
63
+
40
64
  test("resolveTextCortexBaseUrl defaults to the cloud API for packaged usage", () => {
41
65
  assert.equal(
42
66
  resolveTextCortexBaseUrl({
@@ -128,12 +152,13 @@ test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plug
128
152
  const config = buildOpenCodeConfig({
129
153
  baseUrl: "http://127.0.0.1:8080",
130
154
  providerID: "textcortex",
131
- model: "kimi-k2-5-thinking",
132
- smallModel: "kimi-k2-5-thinking",
155
+ model: "minimax-m3-thinking",
156
+ smallModel: "gpt-5-2",
133
157
  });
134
158
 
135
159
  assert.deepEqual(config.enabled_providers, ["textcortex"]);
136
- assert.equal(config.model, "textcortex/kimi-k2-5-thinking");
160
+ assert.equal(config.model, "textcortex/minimax-m3-thinking");
161
+ assert.equal(config.small_model, "textcortex/gpt-5-2");
137
162
  assert.equal(config.theme, "system");
138
163
  assert.ok(config.provider.openai);
139
164
  assert.deepEqual(config.provider.openai.models, {});
@@ -156,6 +181,40 @@ test("buildPackageLauncherChildOptions keeps fallback package launchers attached
156
181
  });
157
182
  });
158
183
 
184
+ test("buildPackageLauncherInvocation runs pinned runtimes directly", () => {
185
+ const invocation = buildPackageLauncherInvocation({
186
+ runner: {
187
+ command: "npx",
188
+ args: ["--yes", "opencode-ai@1.17.6", "--version"],
189
+ },
190
+ args: ["--version"],
191
+ pinnedRuntimePath: "/tmp/zenocode-runtime",
192
+ });
193
+
194
+ assert.deepEqual(invocation, {
195
+ command: "/tmp/zenocode-runtime",
196
+ args: ["--version"],
197
+ usePinnedRuntime: true,
198
+ });
199
+ });
200
+
201
+ test("buildPackageLauncherInvocation uses package runners without a pinned runtime", () => {
202
+ const invocation = buildPackageLauncherInvocation({
203
+ runner: {
204
+ command: "pnpm",
205
+ args: ["dlx", "opencode-ai@1.17.6", "--version"],
206
+ },
207
+ args: ["--version"],
208
+ pinnedRuntimePath: null,
209
+ });
210
+
211
+ assert.deepEqual(invocation, {
212
+ command: "pnpm",
213
+ args: ["dlx", "opencode-ai@1.17.6", "--version"],
214
+ usePinnedRuntime: false,
215
+ });
216
+ });
217
+
159
218
  test("buildZenocodeBanner renders block logo art instead of plain text", () => {
160
219
  const banner = buildZenocodeBanner();
161
220
 
@@ -510,7 +569,7 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
510
569
 
511
570
  if (
512
571
  req.method === "GET" &&
513
- req.url === "/internal/v1/fastapi/zenocode/models/api.json"
572
+ req.url === "/internal/v2/fastapi/zenocode/models/api.json"
514
573
  ) {
515
574
  modelAuthHeader = req.headers.authorization || null;
516
575
  if (modelAuthHeader === "Bearer expired-access") {
@@ -524,8 +583,10 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
524
583
  JSON.stringify({
525
584
  textcortex: {
526
585
  models: {
586
+ "minimax-m3-thinking": {},
587
+ "kimi-k2-6": {},
527
588
  "kimi-k2-5-thinking": {},
528
- "glm-5": {},
589
+ "glm-5-1": {},
529
590
  },
530
591
  },
531
592
  }),
@@ -580,6 +641,11 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
580
641
  const savedCredentials = JSON.parse(await fs.readFile(credentialsPath, "utf-8"));
581
642
  assert.equal(savedCredentials.access_token, "fresh-access");
582
643
  assert.equal(savedCredentials.refresh_token, "fresh-refresh");
644
+
645
+ const savedConfig = JSON.parse(
646
+ await fs.readFile(path.join(zenocodeHome, "opencode.jsonc"), "utf-8"),
647
+ );
648
+ assert.equal(savedConfig.model, "textcortex/minimax-m3-thinking");
583
649
  });
584
650
 
585
651
  test("login launches the runtime immediately with system TUI theming", async (t) => {
@@ -623,7 +689,7 @@ main().catch((error) => {
623
689
  const server = http.createServer((req, res) => {
624
690
  if (
625
691
  req.method === "POST" &&
626
- req.url === "/internal/v1/fastapi/zenocode/oauth2/initiate"
692
+ req.url === "/internal/v2/fastapi/zenocode/oauth2/initiate"
627
693
  ) {
628
694
  res.writeHead(200, { "Content-Type": "application/json" });
629
695
  res.end(
@@ -642,7 +708,7 @@ main().catch((error) => {
642
708
 
643
709
  if (
644
710
  req.method === "POST" &&
645
- req.url === "/internal/v1/fastapi/zenocode/oauth2/token"
711
+ req.url === "/internal/v2/fastapi/zenocode/oauth2/token"
646
712
  ) {
647
713
  res.writeHead(200, { "Content-Type": "application/json" });
648
714
  res.end(
@@ -659,15 +725,17 @@ main().catch((error) => {
659
725
 
660
726
  if (
661
727
  req.method === "GET" &&
662
- req.url === "/internal/v1/fastapi/zenocode/models/api.json"
728
+ req.url === "/internal/v2/fastapi/zenocode/models/api.json"
663
729
  ) {
664
730
  res.writeHead(200, { "Content-Type": "application/json" });
665
731
  res.end(
666
732
  JSON.stringify({
667
733
  textcortex: {
668
734
  models: {
735
+ "minimax-m3-thinking": {},
736
+ "kimi-k2-6": {},
669
737
  "kimi-k2-5-thinking": {},
670
- "glm-5": {},
738
+ "glm-5-1": {},
671
739
  },
672
740
  },
673
741
  }),
@@ -736,6 +804,7 @@ main().catch((error) => {
736
804
  await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, "utf-8"),
737
805
  );
738
806
  assert.equal(opencodeConfig.theme, "system");
807
+ assert.equal(opencodeConfig.model, "textcortex/minimax-m3-thinking");
739
808
 
740
809
  const tuiConfig = JSON.parse(
741
810
  await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),