@textcortex/zenocode 0.1.11 → 0.1.12

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
@@ -21,13 +21,14 @@ npm install -g @textcortex/zenocode
21
21
 
22
22
  ```bash
23
23
  zenocode login --email you@company.com
24
- zenocode
25
24
  ```
26
25
 
27
26
  Use your work email when logging in so Zenocode can route you to the correct onboarding and SSO flow for your workspace domain, for example `companyA.textcortex.com`.
28
27
 
29
28
  If you skip `--email`, Zenocode will ask for it interactively during login.
30
29
 
30
+ The first `zenocode login` launches Zenocode automatically after browser authentication succeeds. In later terminal sessions, start it again with `zenocode`.
31
+
31
32
  If you already have an API key, you can also start Zenocode by setting `TEXTCORTEX_API_KEY` or `TEXTCORTEX_API_TOKEN`.
32
33
 
33
34
  ## Built For Security And Compliance
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/zenocode",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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",
@@ -29,6 +29,7 @@ const legacyRuntimeCredentialsPath = path.join(
29
29
  const logoutMarkerPath = path.join(runtimeDir, "logout-marker.json");
30
30
  const modelsPath = path.join(runtimeDir, "models.json");
31
31
  const configPath = path.join(runtimeDir, "opencode.jsonc");
32
+ const tuiConfigPath = path.join(runtimeDir, "opencode.tui.json");
32
33
  const localBaseUrlDefault = "http://127.0.0.1:8080";
33
34
  const cloudBaseUrlDefault = "https://api.textcortex.com";
34
35
  const localBaseUrlFlags = new Set(["--local", "--localhost"]);
@@ -394,6 +395,8 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
394
395
  return {
395
396
  $schema: "https://opencode.ai/config.json",
396
397
  enabled_providers: [providerID],
398
+ // Keep the legacy theme key populated for older OpenCode builds.
399
+ theme: "system",
397
400
  model: `${providerID}/${model}`,
398
401
  small_model: `${providerID}/${smallModel}`,
399
402
  provider: {
@@ -415,6 +418,13 @@ export function buildOpenCodeConfig({ baseUrl, providerID, model, smallModel })
415
418
  };
416
419
  }
417
420
 
421
+ export function buildOpenCodeTuiConfig() {
422
+ return {
423
+ $schema: "https://opencode.ai/tui.json",
424
+ theme: "system",
425
+ };
426
+ }
427
+
418
428
  function unwrapData(payload) {
419
429
  if (payload && typeof payload === "object" && payload.data && typeof payload.data === "object") {
420
430
  return payload.data;
@@ -482,10 +492,12 @@ async function prepareRuntime(baseUrl, token) {
482
492
  model,
483
493
  smallModel,
484
494
  });
495
+ const tuiConfig = buildOpenCodeTuiConfig();
485
496
 
486
497
  await fs.mkdir(runtimeDir, { recursive: true });
487
498
  await fs.writeFile(modelsPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
488
499
  await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
500
+ await fs.writeFile(tuiConfigPath, `${JSON.stringify(tuiConfig, null, 2)}\n`, "utf-8");
489
501
  return model;
490
502
  }
491
503
 
@@ -1578,7 +1590,7 @@ async function main() {
1578
1590
  }
1579
1591
 
1580
1592
  const preferLocalhost = hasLocalBaseUrlFlag(passthrough);
1581
- const runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1593
+ let runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1582
1594
  const storedBaseUrl = await resolveStoredBaseUrl();
1583
1595
  const baseUrl = resolveTextCortexBaseUrl({
1584
1596
  envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
@@ -1596,7 +1608,7 @@ async function main() {
1596
1608
  runtimeArgs.slice(1),
1597
1609
  { preferLocalhost },
1598
1610
  );
1599
- return;
1611
+ runtimeArgs = [];
1600
1612
  }
1601
1613
 
1602
1614
  if (subcommand === "logout") {
@@ -1626,6 +1638,7 @@ async function main() {
1626
1638
  ...process.env,
1627
1639
  OPENCODE_MODELS_PATH: modelsPath,
1628
1640
  OPENCODE_CONFIG: configPath,
1641
+ OPENCODE_TUI_CONFIG: tuiConfigPath,
1629
1642
  TEXTCORTEX_API_KEY: token,
1630
1643
  },
1631
1644
  };
@@ -134,6 +134,7 @@ test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plug
134
134
 
135
135
  assert.deepEqual(config.enabled_providers, ["textcortex"]);
136
136
  assert.equal(config.model, "textcortex/kimi-k2-5-thinking");
137
+ assert.equal(config.theme, "system");
137
138
  assert.ok(config.provider.openai);
138
139
  assert.deepEqual(config.provider.openai.models, {});
139
140
  });
@@ -581,6 +582,167 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
581
582
  assert.equal(savedCredentials.refresh_token, "fresh-refresh");
582
583
  });
583
584
 
585
+ test("login launches the runtime immediately with system TUI theming", async (t) => {
586
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-login-"));
587
+ const zenocodeHome = path.join(tempDir, ".zenocode");
588
+ const runtimeLogPath = path.join(tempDir, "runtime-log.json");
589
+ const fakeRuntimePath = path.join(tempDir, "fake-opencode");
590
+ const scriptPath = new URL("./run-zenocode.mjs", import.meta.url);
591
+
592
+ t.after(async () => {
593
+ await fs.rm(tempDir, { recursive: true, force: true });
594
+ });
595
+
596
+ await fs.mkdir(zenocodeHome, { recursive: true });
597
+ await fs.writeFile(
598
+ fakeRuntimePath,
599
+ `#!/usr/bin/env node
600
+ const fs = require("node:fs/promises");
601
+
602
+ async function main() {
603
+ const payload = {
604
+ args: process.argv.slice(2),
605
+ env: {
606
+ OPENCODE_CONFIG: process.env.OPENCODE_CONFIG,
607
+ OPENCODE_TUI_CONFIG: process.env.OPENCODE_TUI_CONFIG,
608
+ TEXTCORTEX_API_KEY: process.env.TEXTCORTEX_API_KEY,
609
+ },
610
+ };
611
+ await fs.writeFile(process.env.RUNTIME_LOG_PATH, JSON.stringify(payload), "utf-8");
612
+ }
613
+
614
+ main().catch((error) => {
615
+ console.error(error instanceof Error ? error.message : String(error));
616
+ process.exit(1);
617
+ });
618
+ `,
619
+ "utf-8",
620
+ );
621
+ await fs.chmod(fakeRuntimePath, 0o755);
622
+
623
+ const server = http.createServer((req, res) => {
624
+ if (
625
+ req.method === "POST" &&
626
+ req.url === "/internal/v1/fastapi/zenocode/oauth2/initiate"
627
+ ) {
628
+ res.writeHead(200, { "Content-Type": "application/json" });
629
+ res.end(
630
+ JSON.stringify({
631
+ data: {
632
+ device_code: "device-code",
633
+ user_code: "ABCD-1234",
634
+ verification_url_complete: "https://textcortex.example/verify",
635
+ interval: 0,
636
+ expires_in: 30,
637
+ },
638
+ }),
639
+ );
640
+ return;
641
+ }
642
+
643
+ if (
644
+ req.method === "POST" &&
645
+ req.url === "/internal/v1/fastapi/zenocode/oauth2/token"
646
+ ) {
647
+ res.writeHead(200, { "Content-Type": "application/json" });
648
+ res.end(
649
+ JSON.stringify({
650
+ data: {
651
+ access_token: "fresh-access",
652
+ refresh_token: "fresh-refresh",
653
+ auth_id: "auth_123",
654
+ },
655
+ }),
656
+ );
657
+ return;
658
+ }
659
+
660
+ if (
661
+ req.method === "GET" &&
662
+ req.url === "/internal/v1/fastapi/zenocode/models/api.json"
663
+ ) {
664
+ res.writeHead(200, { "Content-Type": "application/json" });
665
+ res.end(
666
+ JSON.stringify({
667
+ textcortex: {
668
+ models: {
669
+ "kimi-k2-5-thinking": {},
670
+ "glm-5": {},
671
+ },
672
+ },
673
+ }),
674
+ );
675
+ return;
676
+ }
677
+
678
+ res.writeHead(404, { "Content-Type": "application/json" });
679
+ res.end(JSON.stringify({ detail: "not found" }));
680
+ });
681
+
682
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
683
+ const address = server.address();
684
+ const baseUrl = `http://127.0.0.1:${address.port}`;
685
+
686
+ t.after(async () => {
687
+ await new Promise((resolve, reject) =>
688
+ server.close((error) => (error ? reject(error) : resolve())),
689
+ );
690
+ });
691
+
692
+ const result = await new Promise((resolve, reject) => {
693
+ const child = spawn(
694
+ process.execPath,
695
+ [
696
+ scriptPath.pathname,
697
+ "login",
698
+ "--email",
699
+ "person@example.com",
700
+ "--no-launch-browser",
701
+ ],
702
+ {
703
+ cwd: tempDir,
704
+ env: {
705
+ ...process.env,
706
+ ZENOCODE_HOME: zenocodeHome,
707
+ ZENOCODE_NO_BANNER: "1",
708
+ ZENOCODE_OPENCODE_BIN_PATH: fakeRuntimePath,
709
+ TEXTCORTEX_BASE_URL: baseUrl,
710
+ RUNTIME_LOG_PATH: runtimeLogPath,
711
+ },
712
+ stdio: ["ignore", "pipe", "pipe"],
713
+ },
714
+ );
715
+ let stdout = "";
716
+ let stderr = "";
717
+ child.stdout.on("data", (chunk) => {
718
+ stdout += String(chunk);
719
+ });
720
+ child.stderr.on("data", (chunk) => {
721
+ stderr += String(chunk);
722
+ });
723
+ child.on("error", reject);
724
+ child.on("exit", (code, signal) => resolve({ code, signal, stdout, stderr }));
725
+ });
726
+
727
+ assert.equal(result.code, 0);
728
+ assert.match(result.stdout, /Login successful for auth_123/);
729
+ assert.match(result.stdout, /Zenocode config ready at/);
730
+
731
+ const runtimeInvocation = JSON.parse(await fs.readFile(runtimeLogPath, "utf-8"));
732
+ assert.deepEqual(runtimeInvocation.args, []);
733
+ assert.equal(runtimeInvocation.env.TEXTCORTEX_API_KEY, "fresh-access");
734
+
735
+ const opencodeConfig = JSON.parse(
736
+ await fs.readFile(runtimeInvocation.env.OPENCODE_CONFIG, "utf-8"),
737
+ );
738
+ assert.equal(opencodeConfig.theme, "system");
739
+
740
+ const tuiConfig = JSON.parse(
741
+ await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),
742
+ );
743
+ assert.equal(tuiConfig.theme, "system");
744
+ });
745
+
584
746
  test("logout removes runtime credentials and blocks shared fallback credentials", async (t) => {
585
747
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-logout-"));
586
748
  const zenocodeHome = path.join(tempDir, ".zenocode");