@textcortex/zenocode 0.1.2 → 0.1.3

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.
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ buildWrapperBinMap,
5
+ mapBrandedBinaryPackageName,
6
+ } from "./build-branded-opencode.mjs";
7
+
8
+ test("mapBrandedBinaryPackageName scopes runtime binaries under zenocode", () => {
9
+ assert.equal(
10
+ mapBrandedBinaryPackageName("opencode-darwin-arm64", "@textcortex/zenocode-ai"),
11
+ "@textcortex/zenocode-darwin-arm64",
12
+ );
13
+ assert.equal(
14
+ mapBrandedBinaryPackageName("opencode-linux-x64-baseline-musl", "@textcortex/zenocode-ai"),
15
+ "@textcortex/zenocode-linux-x64-baseline-musl",
16
+ );
17
+ });
18
+
19
+ test("buildWrapperBinMap includes package, opencode, and zenocode entrypoints", () => {
20
+ assert.deepEqual(buildWrapperBinMap("@textcortex/zenocode-ai", "zenocode"), {
21
+ "zenocode-ai": "./bin/opencode",
22
+ opencode: "./bin/opencode",
23
+ zenocode: "./bin/opencode",
24
+ });
25
+ });
@@ -7,7 +7,10 @@ import path from "node:path";
7
7
  import process from "node:process";
8
8
  import readline from "node:readline/promises";
9
9
  import { fileURLToPath } from "node:url";
10
- import { padBinaryReplacement, patchOpenCodeVersionFooterText } from "./branding-patch.mjs";
10
+ import {
11
+ patchZenocodeBinaryText,
12
+ zenocodeLogo,
13
+ } from "./branding-patch.mjs";
11
14
 
12
15
  const currentFilePath = realpathSync(fileURLToPath(import.meta.url));
13
16
  const __dirname = path.dirname(currentFilePath);
@@ -66,19 +69,22 @@ const credentialFiles = [
66
69
  path.join(os.homedir(), ".credentials.json"),
67
70
  ...devCredentialFiles,
68
71
  ].filter((value, index, values) => values.indexOf(value) === index);
69
- const opencodeLogoSnippet = `var logo = {
70
- left: [" ", "\\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2584", "\\u2588__\\u2588 \\u2588__\\u2588 \\u2588^^^ \\u2588__\\u2588", "\\u2580\\u2580\\u2580\\u2580 \\u2588\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580~~\\u2580"],
71
- right: [" \\u2584 ", "\\u2588\\u2580\\u2580\\u2580 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588 \\u2588\\u2580\\u2580\\u2588", "\\u2588___ \\u2588__\\u2588 \\u2588__\\u2588 \\u2588^^^", "\\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580 \\u2580\\u2580\\u2580\\u2580"]
72
- };
73
- var marks = "_^~";`;
74
- const zenocodeTextLogoSnippetCore = `var logo = {
75
- left: [" ", " Zenocode ", " Zenocode ", " "],
76
- right: [" ", " ", " ", " "]
77
- };
78
- var marks = "_^~";`;
79
- const zenocodeBanner = `
80
- Z E N O C O D E
81
- `;
72
+
73
+ function _decodeEscapedLogoLine(line) {
74
+ return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, codePoint) =>
75
+ String.fromCharCode(Number.parseInt(codePoint, 16)),
76
+ );
77
+ }
78
+
79
+ export function buildZenocodeBanner() {
80
+ const bannerLines = zenocodeLogo.left
81
+ .map((leftLine, index) => {
82
+ const rightLine = zenocodeLogo.right[index] || "";
83
+ return `${_decodeEscapedLogoLine(leftLine)} ${_decodeEscapedLogoLine(rightLine)}`.trimEnd();
84
+ })
85
+ .filter((line) => line.trim());
86
+ return `\n${bannerLines.join("\n")}\n`;
87
+ }
82
88
  const privateDirectoryMode = 0o700;
83
89
  const privateFileMode = 0o600;
84
90
 
@@ -503,16 +509,28 @@ async function runLoginCommand(baseUrl, args) {
503
509
  }
504
510
 
505
511
  async function runLogoutCommand() {
506
- try {
507
- await fs.unlink(runtimeCredentialsPath);
508
- console.log("Zenocode credentials removed.");
509
- } catch (error) {
510
- if (error?.code === "ENOENT") {
511
- console.log("No local Zenocode credentials found.");
512
- return;
512
+ let removedAnyCredentials = false;
513
+ for (const credentialsPath of [
514
+ runtimeCredentialsPath,
515
+ legacyRuntimeCredentialsPath,
516
+ ].filter((value, index, values) => values.indexOf(value) === index)) {
517
+ try {
518
+ await fs.unlink(credentialsPath);
519
+ removedAnyCredentials = true;
520
+ } catch (error) {
521
+ if (error?.code === "ENOENT") {
522
+ continue;
523
+ }
524
+ throw error;
513
525
  }
514
- throw error;
515
526
  }
527
+
528
+ if (removedAnyCredentials) {
529
+ console.log("Zenocode credentials removed.");
530
+ return;
531
+ }
532
+
533
+ console.log("No local Zenocode credentials found.");
516
534
  }
517
535
 
518
536
  async function runChild(command, args, options) {
@@ -527,8 +545,130 @@ async function runChild(command, args, options) {
527
545
  });
528
546
  }
529
547
 
530
- async function runBrandedBinary(args, options) {
531
- const result = await runChild(opencodeBinaryPath, args, options);
548
+ function _buildScriptCommand(runtimeBinaryPath, args, transcriptPath) {
549
+ if (process.platform === "darwin") {
550
+ return {
551
+ command: "script",
552
+ args: ["-q", "-F", "-t", "0", transcriptPath, runtimeBinaryPath, ...args],
553
+ };
554
+ }
555
+ if (process.platform === "linux") {
556
+ const shellCommand = [runtimeBinaryPath, ...args]
557
+ .map((value) => `'${String(value).replaceAll("'", `'\\''`)}'`)
558
+ .join(" ");
559
+ return {
560
+ command: "script",
561
+ args: ["-q", "-f", "-c", shellCommand, transcriptPath],
562
+ };
563
+ }
564
+ return null;
565
+ }
566
+
567
+ async function _readTranscriptUpdate(transcriptPath, offset) {
568
+ let handle;
569
+ try {
570
+ handle = await fs.open(transcriptPath, "r");
571
+ } catch (error) {
572
+ if (error?.code === "ENOENT") {
573
+ return { offset, text: "" };
574
+ }
575
+ throw error;
576
+ }
577
+
578
+ try {
579
+ const stats = await handle.stat();
580
+ if (stats.size <= offset) {
581
+ return { offset, text: "" };
582
+ }
583
+
584
+ const length = stats.size - offset;
585
+ const buffer = Buffer.alloc(length);
586
+ await handle.read(buffer, 0, length, offset);
587
+ return { offset: stats.size, text: buffer.toString("utf-8") };
588
+ } finally {
589
+ await handle.close();
590
+ }
591
+ }
592
+
593
+ async function _terminateChildProcess(child, exitPromise) {
594
+ if (child.exitCode !== null || child.signalCode !== null) {
595
+ return exitPromise;
596
+ }
597
+
598
+ child.kill("SIGTERM");
599
+ const exited = await Promise.race([exitPromise, sleep(1_500).then(() => null)]);
600
+ if (exited) {
601
+ return exited;
602
+ }
603
+
604
+ child.kill("SIGKILL");
605
+ return exitPromise;
606
+ }
607
+
608
+ async function _runRuntimeWithTranscriptMonitor(runtimeBinaryPath, args, options) {
609
+ const transcriptDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-runtime-"));
610
+ const transcriptPath = path.join(transcriptDir, "session.log");
611
+ const command = _buildScriptCommand(runtimeBinaryPath, args, transcriptPath);
612
+ if (!command) {
613
+ const result = await runChild(runtimeBinaryPath, args, { ...options, stdio: "inherit" });
614
+ return { ...result, expiredSession: false };
615
+ }
616
+
617
+ try {
618
+ const child = spawn(command.command, command.args, {
619
+ ...options,
620
+ stdio: "inherit",
621
+ });
622
+ const exitPromise = new Promise((resolve, reject) => {
623
+ child.on("error", reject);
624
+ child.on("exit", (code, signal) => resolve({ code, signal, expiredSession: false }));
625
+ });
626
+ let transcriptOffset = 0;
627
+ let transcriptTail = "";
628
+
629
+ while (true) {
630
+ const outcome = await Promise.race([
631
+ exitPromise.then((result) => ({ type: "exit", result })),
632
+ sleep(250).then(() => ({ type: "poll" })),
633
+ ]);
634
+
635
+ const update = await _readTranscriptUpdate(transcriptPath, transcriptOffset);
636
+ transcriptOffset = update.offset;
637
+ if (update.text) {
638
+ transcriptTail = _appendTranscriptTail(transcriptTail, update.text);
639
+ }
640
+
641
+ if (canRecoverRuntimeSessionFromTranscript(transcriptTail)) {
642
+ await _terminateChildProcess(child, exitPromise);
643
+ return { expiredSession: true };
644
+ }
645
+
646
+ if (outcome.type === "exit") {
647
+ return canRecoverRuntimeSessionFromTranscript(transcriptTail)
648
+ ? { expiredSession: true }
649
+ : outcome.result;
650
+ }
651
+ }
652
+ } catch (error) {
653
+ if (error?.code === "ENOENT") {
654
+ const result = await runChild(runtimeBinaryPath, args, { ...options, stdio: "inherit" });
655
+ return { ...result, expiredSession: false };
656
+ }
657
+ throw error;
658
+ } finally {
659
+ await fs.rm(transcriptDir, { recursive: true, force: true });
660
+ }
661
+ }
662
+
663
+ async function runRuntimeBinary(runtimeBinaryPath, args, options, monitorRuntimeSession) {
664
+ if (!monitorRuntimeSession) {
665
+ const result = await runChild(runtimeBinaryPath, args, { ...options, stdio: "inherit" });
666
+ return { ...result, expiredSession: false };
667
+ }
668
+ return _runRuntimeWithTranscriptMonitor(runtimeBinaryPath, args, options);
669
+ }
670
+
671
+ function exitWithChildResult(result) {
532
672
  if (result.signal) {
533
673
  process.kill(process.pid, result.signal);
534
674
  return;
@@ -553,14 +693,6 @@ function shouldPatchOpencodeRuntimePackage(packageName) {
553
693
  );
554
694
  }
555
695
 
556
- function _buildPaddedLogoSnippet(coreSnippet) {
557
- return padBinaryReplacement(opencodeLogoSnippet, coreSnippet);
558
- }
559
-
560
- function _buildZenocodeLogoSnippet() {
561
- return _buildPaddedLogoSnippet(zenocodeTextLogoSnippetCore);
562
- }
563
-
564
696
  async function _pathExists(pathToCheck) {
565
697
  try {
566
698
  await fs.access(pathToCheck);
@@ -688,26 +820,6 @@ async function _adHocSignBinary(binaryPath) {
688
820
  }
689
821
  }
690
822
 
691
- function _patchLogoSnippetText(text) {
692
- const replacement = _buildZenocodeLogoSnippet();
693
- if (!replacement || text.includes(replacement)) {
694
- return { patched: false, text };
695
- }
696
-
697
- const patchTargets = [opencodeLogoSnippet].filter(Boolean);
698
- let logoOffset = -1;
699
- for (const target of patchTargets) {
700
- logoOffset = text.indexOf(target);
701
- if (logoOffset !== -1) break;
702
- }
703
- if (logoOffset === -1) {
704
- return { patched: false, text };
705
- }
706
-
707
- const nextText = `${text.slice(0, logoOffset)}${replacement}${text.slice(logoOffset + replacement.length)}`;
708
- return { patched: true, text: nextText };
709
- }
710
-
711
823
  async function _patchOpencodeBinaryBranding(binaryPath) {
712
824
  let buffer;
713
825
  try {
@@ -718,19 +830,9 @@ async function _patchOpencodeBinaryBranding(binaryPath) {
718
830
 
719
831
  const originalLength = buffer.length;
720
832
  let text = buffer.toString("latin1");
721
- let patched = false;
722
-
723
- const logoPatch = _patchLogoSnippetText(text);
724
- if (logoPatch.patched) {
725
- text = logoPatch.text;
726
- patched = true;
727
- }
728
-
729
- const footerPatch = patchOpenCodeVersionFooterText(text);
730
- if (footerPatch.patched) {
731
- text = footerPatch.text;
732
- patched = true;
733
- }
833
+ const patch = patchZenocodeBinaryText(text);
834
+ text = patch.text;
835
+ const patched = patch.patched;
734
836
 
735
837
  if (!patched) {
736
838
  return false;
@@ -859,6 +961,88 @@ async function runPackageLauncher(packageName, args, options) {
859
961
  );
860
962
  }
861
963
 
964
+ async function resolvePinnedRuntimeBinary(packageName, options) {
965
+ const runners = [
966
+ { command: _runnerCommand("pnpm"), args: ["dlx", packageName] },
967
+ { command: _runnerCommand("npx"), args: ["--yes", packageName] },
968
+ ];
969
+ const missingRunners = [];
970
+
971
+ for (const runner of runners) {
972
+ try {
973
+ let pinnedRuntimePath = await _ensurePatchedOpencodeDlxBinaries(packageName, runner, options);
974
+ if (!pinnedRuntimePath && shouldPatchOpencodeRuntimePackage(packageName)) {
975
+ const existingPinnedPath = _runtimeBrandedBinaryPath();
976
+ if (await _pathExists(existingPinnedPath)) {
977
+ pinnedRuntimePath = existingPinnedPath;
978
+ }
979
+ }
980
+
981
+ if (pinnedRuntimePath) {
982
+ return pinnedRuntimePath;
983
+ }
984
+ } catch (error) {
985
+ if (error?.code === "ENOENT") {
986
+ missingRunners.push(runner.command);
987
+ continue;
988
+ }
989
+ throw error;
990
+ }
991
+ }
992
+
993
+ if (missingRunners.length === runners.length) {
994
+ throw new Error(
995
+ `No package runner found (${missingRunners.join(", ")}). Install pnpm or npm, or set ZENOCODE_OPENCODE_BIN_PATH/CODECORTEX_OPENCODE_BIN_PATH.`,
996
+ );
997
+ }
998
+
999
+ return null;
1000
+ }
1001
+
1002
+ export async function runRuntimeWithSessionRecovery({
1003
+ args,
1004
+ baseUrl,
1005
+ token,
1006
+ childOptions,
1007
+ canAutoLoginRuntime,
1008
+ runLogin = runLoginCommand,
1009
+ resolveTokenFn = resolveToken,
1010
+ resolveStoredBaseUrlFn = resolveStoredBaseUrl,
1011
+ prepareRuntimeFn = prepareRuntime,
1012
+ launchRuntimeFn,
1013
+ }) {
1014
+ let activeBaseUrl = baseUrl;
1015
+ let activeToken = token;
1016
+
1017
+ while (true) {
1018
+ const result = await launchRuntimeFn({
1019
+ args,
1020
+ childOptions: {
1021
+ ...childOptions,
1022
+ env: {
1023
+ ...(childOptions.env || {}),
1024
+ TEXTCORTEX_API_KEY: activeToken,
1025
+ },
1026
+ },
1027
+ token: activeToken,
1028
+ });
1029
+
1030
+ if (!result.expiredSession) {
1031
+ return result;
1032
+ }
1033
+
1034
+ if (!canAutoLoginRuntime) {
1035
+ return result;
1036
+ }
1037
+
1038
+ console.log("Zenocode session expired. Starting login flow...\n");
1039
+ await runLogin(activeBaseUrl, buildAutoLoginArgs());
1040
+ activeToken = await resolveTokenFn();
1041
+ activeBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrlFn()) || activeBaseUrl;
1042
+ await prepareRuntimeFn(activeBaseUrl, activeToken);
1043
+ }
1044
+ }
1045
+
862
1046
  async function packageExistsOnNpm(packageName) {
863
1047
  const controller = new AbortController();
864
1048
  const timeout = setTimeout(() => controller.abort(), 4_000);
@@ -925,7 +1109,14 @@ function maybeRenderBanner(args) {
925
1109
  if (!shouldRenderBanner(args)) {
926
1110
  return;
927
1111
  }
928
- console.log(`${zenocodeBanner}\n`);
1112
+ console.log(`${buildZenocodeBanner()}\n`);
1113
+ }
1114
+
1115
+ function buildAutoLoginArgs() {
1116
+ return process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
1117
+ process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === "1"
1118
+ ? ["--no-launch-browser"]
1119
+ : [];
929
1120
  }
930
1121
 
931
1122
  function isMissingTokenError(error) {
@@ -954,6 +1145,45 @@ function canAutoLogin(args) {
954
1145
  return true;
955
1146
  }
956
1147
 
1148
+ const ansiSequencePattern =
1149
+ /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
1150
+
1151
+ function _stripTerminalControlCharacters(text) {
1152
+ let normalized = "";
1153
+ for (const char of text) {
1154
+ if (char === "\b" || char === "\u007f") {
1155
+ normalized = normalized.slice(0, -1);
1156
+ continue;
1157
+ }
1158
+ if (char === "\r") {
1159
+ continue;
1160
+ }
1161
+ if (char < " " && char !== "\n" && char !== "\t") {
1162
+ continue;
1163
+ }
1164
+ normalized += char;
1165
+ }
1166
+ return normalized;
1167
+ }
1168
+
1169
+ export function normalizeRuntimeTranscript(text) {
1170
+ return _stripTerminalControlCharacters(text.replaceAll(ansiSequencePattern, ""));
1171
+ }
1172
+
1173
+ export function canRecoverRuntimeSessionFromTranscript(text) {
1174
+ const normalized = normalizeRuntimeTranscript(text);
1175
+ return (
1176
+ /Unauthorized:\s*\{\s*"detail"\s*:\s*"Token has expired"\s*\}/i.test(normalized) ||
1177
+ /authentication token may be missing or expired/i.test(normalized) ||
1178
+ /server returned 401 after successful authentication/i.test(normalized)
1179
+ );
1180
+ }
1181
+
1182
+ function _appendTranscriptTail(tail, nextChunk, maxLength = 32_768) {
1183
+ const combined = `${tail}${normalizeRuntimeTranscript(nextChunk)}`;
1184
+ return combined.length > maxLength ? combined.slice(-maxLength) : combined;
1185
+ }
1186
+
957
1187
  function shouldAttemptAutoLogin(error, args) {
958
1188
  if (!isMissingTokenError(error)) return false;
959
1189
  return canAutoLogin(args);
@@ -969,12 +1199,7 @@ async function resolveTokenWithAutoLogin(baseUrl, args) {
969
1199
  }
970
1200
 
971
1201
  console.log("No local Zenocode credentials found. Starting login flow...\n");
972
- const loginArgs =
973
- process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
974
- process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === "1"
975
- ? ["--no-launch-browser"]
976
- : [];
977
- await runLoginCommand(baseUrl, loginArgs);
1202
+ await runLoginCommand(baseUrl, buildAutoLoginArgs());
978
1203
  const token = await resolveToken();
979
1204
  const persistedBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrl()) || baseUrl;
980
1205
  return { token, baseUrl: persistedBaseUrl };
@@ -994,12 +1219,7 @@ async function prepareRuntimeWithAutoLogin(baseUrl, token, args) {
994
1219
  }
995
1220
 
996
1221
  console.log("Zenocode session expired. Starting login flow...\n");
997
- const loginArgs =
998
- process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
999
- process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === "1"
1000
- ? ["--no-launch-browser"]
1001
- : [];
1002
- await runLoginCommand(baseUrl, loginArgs);
1222
+ await runLoginCommand(baseUrl, buildAutoLoginArgs());
1003
1223
  const refreshedToken = await resolveToken();
1004
1224
  const refreshedBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrl()) || baseUrl;
1005
1225
  const model = await prepareRuntime(refreshedBaseUrl, refreshedToken);
@@ -1047,7 +1267,6 @@ async function main() {
1047
1267
 
1048
1268
  const childOptions = {
1049
1269
  cwd: process.cwd(),
1050
- stdio: "inherit",
1051
1270
  env: {
1052
1271
  ...process.env,
1053
1272
  OPENCODE_MODELS_PATH: modelsPath,
@@ -1055,13 +1274,37 @@ async function main() {
1055
1274
  TEXTCORTEX_API_KEY: token,
1056
1275
  },
1057
1276
  };
1277
+ const monitorRuntimeSession = canAutoLogin(passthrough);
1058
1278
 
1059
1279
  if (opencodeBinaryPath) {
1060
- await runBrandedBinary(passthrough, childOptions);
1280
+ const result = await runRuntimeWithSessionRecovery({
1281
+ args: passthrough,
1282
+ baseUrl: runtime.baseUrl,
1283
+ token,
1284
+ childOptions,
1285
+ canAutoLoginRuntime: monitorRuntimeSession,
1286
+ launchRuntimeFn: ({ args, childOptions }) =>
1287
+ runRuntimeBinary(opencodeBinaryPath, args, childOptions, monitorRuntimeSession),
1288
+ });
1289
+ exitWithChildResult(result);
1061
1290
  return;
1062
1291
  }
1063
1292
 
1064
1293
  const launchPackage = await resolveLaunchPackage();
1294
+ const pinnedRuntimePath = await resolvePinnedRuntimeBinary(launchPackage, childOptions);
1295
+ if (pinnedRuntimePath) {
1296
+ const result = await runRuntimeWithSessionRecovery({
1297
+ args: passthrough,
1298
+ baseUrl: runtime.baseUrl,
1299
+ token,
1300
+ childOptions,
1301
+ canAutoLoginRuntime: monitorRuntimeSession,
1302
+ launchRuntimeFn: ({ args, childOptions }) =>
1303
+ runRuntimeBinary(pinnedRuntimePath, args, childOptions, monitorRuntimeSession),
1304
+ });
1305
+ exitWithChildResult(result);
1306
+ return;
1307
+ }
1065
1308
  await runPackageLauncher(launchPackage, passthrough, childOptions);
1066
1309
  }
1067
1310