@textcortex/zenocode 0.1.13 → 0.1.14

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
@@ -4,6 +4,12 @@ Zenocode is the secure coding agent for TextCortex customers. It runs in your te
4
4
 
5
5
  Built for teams that need speed, control, and trust, Zenocode combines a terminal-native coding workflow with workspace routing and enterprise security from day one.
6
6
 
7
+ ## Requirements
8
+
9
+ Zenocode is available on the **pay-as-you-go (PAYG)** plan. Your TextCortex account — or the organization seat assigned to you — must be on PAYG. On any other plan (Free, subscription, Business, Unlimited), `zenocode login` and model discovery return **"Zenocode is only available on the pay-as-you-go plan."** Switch your account (or ask your workspace admin to assign you a PAYG seat) before installing.
10
+
11
+ If you already use pay-as-you-go and just ran out of wallet credits, top up under **Settings → Billing** (`/user/dashboard/settings/billing`). If you are not on pay-as-you-go yet, pick a plan on the **plan selection** page (`/billing/plans`). The in-app Zenocode dialog (sidebar → Zenocode) links you to the right one automatically based on your account.
12
+
7
13
  ## What This Package Does
8
14
 
9
15
  - Launches Zenocode from your terminal with the `zenocode` or `codecortex` command
@@ -31,13 +37,17 @@ The first `zenocode login` launches Zenocode automatically after browser authent
31
37
 
32
38
  If you already have an API key, you can also start Zenocode by setting `TEXTCORTEX_API_KEY` or `TEXTCORTEX_API_TOKEN`.
33
39
 
40
+ ## Models
41
+
42
+ Zenocode uses **GPT-5.6 Luna** by default. You can switch to any other model available to your TextCortex workspace from the model picker inside the agent. Available models are discovered automatically from your account, so the default falls back to the next best coding model (for example Kimi K2.7 Code) when Luna is not available to you.
43
+
34
44
  ## Built For Security And Compliance
35
45
 
36
- Zenocode is built for organizations that treat security, compliance, and data sovereignty as non-negotiable.
46
+ Zenocode is built for organizations that treat security, compliance, and data governance as non-negotiable.
37
47
 
38
- - Hosted in the European Union on GDPR-compliant infrastructure
48
+ - TextCortex-managed routing with workspace-aware model availability
39
49
  - SOC 2 and ISO 27001 certified
40
- - Built for European data sovereignty
50
+ - Model region metadata is surfaced for routing and governance decisions
41
51
  - Zero data training: your data is not used for model training or fine-tuning
42
52
  - Optional services or LLMs hosted outside the EU are covered by the EU-US Data Privacy Framework or GDPR Standard Contractual Clauses
43
53
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@textcortex/zenocode",
3
- "version": "0.1.13",
4
- "description": "Secure, EU-hosted coding agent for TextCortex customers that runs in your terminal, edits files, runs scripts, and more.",
3
+ "version": "0.1.14",
4
+ "description": "Secure, TextCortex-managed coding agent for customers that runs in your terminal, edits files, runs scripts, and more.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "agent",
@@ -11,12 +11,17 @@
11
11
  "developer-tools",
12
12
  "textcortex",
13
13
  "secure",
14
- "eu-hosted",
14
+ "enterprise",
15
15
  "gdpr"
16
16
  ],
17
17
  "private": false,
18
18
  "license": "UNLICENSED",
19
19
  "type": "module",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/textcortex/platform.git",
23
+ "directory": "apps/zenocode"
24
+ },
20
25
  "publishConfig": {
21
26
  "access": "public"
22
27
  },
@@ -17,6 +17,10 @@ export const defaultOpenCodeForkUrl = "https://github.com/sst/opencode";
17
17
  export const defaultOpenCodeRef = openCodeRef;
18
18
  export const defaultOpenCodeVersion = openCodeVersion;
19
19
  export const defaultOpenCodeBuildArgs = ["--skip-install"];
20
+ export const trustedPublishingRepository = {
21
+ type: "git",
22
+ url: "git+https://github.com/textcortex/platform.git",
23
+ };
20
24
  const displayNameOnlyBrandingRefs = new Set([defaultOpenCodeRef]);
21
25
 
22
26
  const forkUrl =
@@ -46,6 +50,8 @@ const publishTag =
46
50
  process.env.ZENOCODE_PUBLISH_TAG ||
47
51
  process.env.CODECORTEX_PUBLISH_TAG ||
48
52
  "latest";
53
+ const publishProvenance =
54
+ (process.env.ZENOCODE_NPM_PROVENANCE || process.env.CODECORTEX_NPM_PROVENANCE) === "1";
49
55
  const configuredBuildArgs = (
50
56
  process.env.ZENOCODE_OPENCODE_BUILD_ARGS ||
51
57
  process.env.CODECORTEX_OPENCODE_BUILD_ARGS ||
@@ -226,6 +232,20 @@ async function writeJson(filePath, payload) {
226
232
  await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
227
233
  }
228
234
 
235
+ export function selectLatestTarball(entries) {
236
+ const tarballs = entries.filter((entry) => entry.endsWith(".tgz")).sort((left, right) => {
237
+ const leftVersion = left.match(/-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?\.tgz$/)?.[1];
238
+ const rightVersion = right.match(/-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\+.*)?\.tgz$/)?.[1];
239
+ if (leftVersion && rightVersion) {
240
+ return compareSemver(leftVersion, rightVersion);
241
+ }
242
+ if (leftVersion) return 1;
243
+ if (rightVersion) return -1;
244
+ return left.localeCompare(right);
245
+ });
246
+ return tarballs[tarballs.length - 1] || null;
247
+ }
248
+
229
249
  async function maybePublishedVersion(packageName) {
230
250
  const npmCommand = _command("npm");
231
251
  try {
@@ -249,15 +269,18 @@ async function maybePublishedVersion(packageName) {
249
269
  async function packPackage(packageDir) {
250
270
  const npmCommand = _command("npm");
251
271
  await run(npmCommand, ["pack"], { cwd: packageDir });
252
- const tarballs = (await fs.readdir(packageDir)).filter((entry) => entry.endsWith(".tgz")).sort();
253
- if (!tarballs.length) {
272
+ const tarball = selectLatestTarball(await fs.readdir(packageDir));
273
+ if (!tarball) {
254
274
  throw new Error(`npm pack did not produce a tarball in ${packageDir}`);
255
275
  }
256
- return path.join(packageDir, tarballs[tarballs.length - 1]);
276
+ return path.join(packageDir, tarball);
257
277
  }
258
278
 
259
- export function buildPublishCommandArgs(tarballPath, { tag } = {}) {
279
+ export function buildPublishCommandArgs(tarballPath, { tag, provenance = false } = {}) {
260
280
  const args = ["publish", tarballPath, "--access", "public"];
281
+ if (provenance) {
282
+ args.push("--provenance");
283
+ }
261
284
  if (tag) {
262
285
  args.push("--tag", tag);
263
286
  }
@@ -281,10 +304,68 @@ async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
281
304
  }
282
305
 
283
306
  const npmCommand = _command("npm");
284
- await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag }), { cwd });
307
+ await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag, provenance: publishProvenance }), { cwd });
285
308
  return { published: true, skipped: false };
286
309
  }
287
310
 
311
+ async function readPublishTarget(packageDir) {
312
+ const pkg = await readJson(path.join(packageDir, "package.json"));
313
+ if (typeof pkg?.name !== "string" || typeof pkg?.version !== "string") {
314
+ throw new Error(`Invalid package metadata in ${packageDir}`);
315
+ }
316
+ const tarball = selectLatestTarball(await fs.readdir(packageDir));
317
+ if (!tarball) {
318
+ throw new Error(`Missing npm tarball in ${packageDir}`);
319
+ }
320
+ return {
321
+ packageName: pkg.name,
322
+ version: pkg.version,
323
+ tarballPath: path.join(packageDir, tarball),
324
+ cwd: packageDir,
325
+ };
326
+ }
327
+
328
+ async function publishExistingPackages(artifactDir) {
329
+ const packageRoot = path.join(artifactDir, "npm-binaries");
330
+ const entries = await fs.readdir(packageRoot, { withFileTypes: true });
331
+ const binaryPackageDirs = entries
332
+ .filter((entry) => entry.isDirectory())
333
+ .map((entry) => path.join(packageRoot, entry.name))
334
+ .sort();
335
+ if (!binaryPackageDirs.length) {
336
+ throw new Error(`No existing binary packages found under ${packageRoot}`);
337
+ }
338
+
339
+ const targets = [];
340
+ for (const packageDir of binaryPackageDirs) {
341
+ targets.push(await readPublishTarget(packageDir));
342
+ }
343
+ targets.push(await readPublishTarget(path.join(artifactDir, "npm-package")));
344
+
345
+ const published = [];
346
+ const skipped = [];
347
+ for (const target of targets) {
348
+ const result = await publishTarballIfNeeded(
349
+ target.packageName,
350
+ target.version,
351
+ target.tarballPath,
352
+ target.cwd,
353
+ );
354
+ if (result.published) {
355
+ published.push(target.packageName);
356
+ } else if (result.skipped) {
357
+ skipped.push(target.packageName);
358
+ }
359
+ }
360
+
361
+ if (published.length) {
362
+ console.log(`Published packages: ${published.join(", ")}`);
363
+ }
364
+ if (skipped.length) {
365
+ console.log(`Skipped already-published packages: ${skipped.join(", ")}`);
366
+ }
367
+ }
368
+
288
369
  function _binaryFilename() {
289
370
  return process.platform === "win32" ? "opencode.exe" : "opencode";
290
371
  }
@@ -534,6 +615,7 @@ async function buildWrapperPackage({
534
615
  version,
535
616
  license: "MIT",
536
617
  type: "commonjs",
618
+ repository: trustedPublishingRepository,
537
619
  bin: buildWrapperBinMap(packageName, binName),
538
620
  optionalDependencies: Object.fromEntries(
539
621
  binaryPackages.map((pkg) => [pkg.brandedName, pkg.version]),
@@ -588,6 +670,7 @@ async function prepareBinaryPackages({ distDir, artifactDir, runtimePackageName
588
670
  const targetDir = path.join(packageRoot, brandedName.replace("/", "__"));
589
671
  await fs.cp(sourceDir, targetDir, { recursive: true });
590
672
  pkg.name = brandedName;
673
+ pkg.repository = trustedPublishingRepository;
591
674
  await writeJson(path.join(targetDir, "package.json"), pkg);
592
675
  await patchBinaryAtPath(path.join(targetDir, "bin", _binaryFilename()));
593
676
 
@@ -641,21 +724,29 @@ async function main() {
641
724
  console.log(" ZENOCODE_OPENCODE_BUILD_ARGS Additional args for upstream build script");
642
725
  console.log(" ZENOCODE_PUBLISH=1 Publish packages to npm");
643
726
  console.log(` ZENOCODE_PUBLISH_TAG npm tag (default: ${publishTag})`);
727
+ console.log(" --publish-existing Publish tarballs from the output directory without rebuilding");
644
728
  console.log(" Legacy CODECORTEX_* env names are still accepted for compatibility.");
645
729
  return;
646
730
  }
647
731
 
732
+ const npmCommand = _command("npm");
733
+ await requireCommand(npmCommand);
734
+
735
+ const artifactDir = path.resolve(outputDir);
736
+
737
+ if (cliArgs.includes("--publish-existing")) {
738
+ await publishExistingPackages(artifactDir);
739
+ return;
740
+ }
741
+
648
742
  const gitCommand = _command("git");
649
743
  const bunCommand = _command("bun");
650
- const npmCommand = _command("npm");
651
744
  await requireCommand(gitCommand);
652
745
  await requireCommand(bunCommand);
653
- await requireCommand(npmCommand);
654
746
 
655
747
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "zenocode-opencode-"));
656
748
  const checkoutDir = path.join(tempRoot, "opencode");
657
749
  const distDir = path.join(checkoutDir, "packages", "opencode", "dist");
658
- const artifactDir = path.resolve(outputDir);
659
750
 
660
751
  try {
661
752
  const cloneArgs = ["clone", "--depth=1"];
@@ -10,6 +10,8 @@ import {
10
10
  defaultOpenCodeVersion,
11
11
  deriveOpenCodeVersionFromRef,
12
12
  mapBrandedBinaryPackageName,
13
+ selectLatestTarball,
14
+ trustedPublishingRepository,
13
15
  } from "./build-branded-opencode.mjs";
14
16
 
15
17
  test("branded OpenCode build defaults to the latest stable upstream release", () => {
@@ -49,7 +51,26 @@ test("buildWrapperBinMap includes package, opencode, and zenocode entrypoints",
49
51
  });
50
52
  });
51
53
 
52
- test("buildPublishCommandArgs omits npm provenance for runtime tarball publishing", () => {
54
+ test("generated runtime packages point trusted publishing at this repository", () => {
55
+ assert.deepEqual(trustedPublishingRepository, {
56
+ type: "git",
57
+ url: "git+https://github.com/textcortex/platform.git",
58
+ });
59
+ });
60
+
61
+ test("buildPublishCommandArgs enables npm provenance for trusted publishing", () => {
62
+ assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest", provenance: true }), [
63
+ "publish",
64
+ "package.tgz",
65
+ "--access",
66
+ "public",
67
+ "--provenance",
68
+ "--tag",
69
+ "latest",
70
+ ]);
71
+ });
72
+
73
+ test("buildPublishCommandArgs omits npm provenance unless requested", () => {
53
74
  assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest" }), [
54
75
  "publish",
55
76
  "package.tgz",
@@ -68,3 +89,15 @@ test("buildPublishCommandArgs omits tag arguments when no tag is provided", () =
68
89
  "public",
69
90
  ]);
70
91
  });
92
+
93
+ test("selectLatestTarball returns the sorted newest tarball", () => {
94
+ assert.equal(
95
+ selectLatestTarball([
96
+ "README.md",
97
+ "textcortex-zenocode-linux-x64-1.9.0.tgz",
98
+ "textcortex-zenocode-linux-x64-1.10.0.tgz",
99
+ ]),
100
+ "textcortex-zenocode-linux-x64-1.10.0.tgz",
101
+ );
102
+ assert.equal(selectLatestTarball(["README.md"]), null);
103
+ });
@@ -55,10 +55,18 @@ const opencodeBinaryPath =
55
55
  const oauthInitiatePath = "/internal/v2/fastapi/zenocode/oauth2/initiate";
56
56
  const oauthTokenPath = "/internal/v2/fastapi/zenocode/oauth2/token";
57
57
  const defaultOrder = [
58
+ "gpt-5-6-luna",
59
+ "kimi-k2p7-code-non-thinking",
60
+ "kimi-k2p7-code-thinking",
58
61
  "minimax-m3-thinking",
59
- "kimi-k2-6",
62
+ "kimi-k2-6-non-thinking",
63
+ "kimi-k2-6-thinking",
64
+ "kimi-k2-5",
60
65
  "kimi-k2-5-thinking",
61
- "glm-5-1",
66
+ "glm-5-2-non-thinking",
67
+ "glm-5-2-thinking",
68
+ "glm-5-1-non-thinking",
69
+ "glm-5-1-thinking",
62
70
  "gpt-5-2",
63
71
  "gpt-5-1",
64
72
  "gpt-5",
@@ -393,7 +401,8 @@ export function chooseDefaults(models) {
393
401
  const ids = Object.keys(models);
394
402
  if (!ids.length) throw new Error("No models were returned by the backend.");
395
403
  const model = defaultOrder.find((id) => ids.includes(id)) || ids[0];
396
- const smallModel = ids.find((id) => id.includes("mini") || id.includes("haiku")) || model;
404
+ const smallModel =
405
+ ids.find((id) => id.includes("-mini") || id.includes("haiku")) || model;
397
406
  return { model, smallModel };
398
407
  }
399
408
 
@@ -27,17 +27,60 @@ import {
27
27
  writePrivateJsonFile,
28
28
  } from "./run-zenocode.mjs";
29
29
 
30
- test("chooseDefaults prefers MiniMax M3 thinking for Zenocode", () => {
30
+ test("chooseDefaults prefers GPT-5.6 Luna as the Zenocode default", () => {
31
31
  const defaults = chooseDefaults({
32
- "glm-5-1": {},
33
- "kimi-k2-6": {},
32
+ "kimi-k2p7-code-non-thinking": {},
33
+ "gpt-5-6-luna": {},
34
+ "glm-5-2-non-thinking": {},
34
35
  "minimax-m3-thinking": {},
35
36
  "gpt-5-2": {},
36
37
  });
37
38
 
38
39
  assert.deepEqual(defaults, {
39
- model: "minimax-m3-thinking",
40
- smallModel: "minimax-m3-thinking",
40
+ model: "gpt-5-6-luna",
41
+ smallModel: "gpt-5-6-luna",
42
+ });
43
+ });
44
+
45
+ test("chooseDefaults falls back to Kimi K2.7 Code when GPT-5.6 Luna is unavailable", () => {
46
+ const defaults = chooseDefaults({
47
+ "kimi-k2p7-code-non-thinking": {},
48
+ "glm-5-2-non-thinking": {},
49
+ "glm-5-1-non-thinking": {},
50
+ "kimi-k2-6-non-thinking": {},
51
+ "minimax-m3-thinking": {},
52
+ "gpt-5-2": {},
53
+ });
54
+
55
+ assert.deepEqual(defaults, {
56
+ model: "kimi-k2p7-code-non-thinking",
57
+ smallModel: "kimi-k2p7-code-non-thinking",
58
+ });
59
+ });
60
+
61
+ test("chooseDefaults prefers GLM 5.2 over GLM 5.1 for Zenocode fallback", () => {
62
+ const defaults = chooseDefaults({
63
+ "glm-5-1-non-thinking": {},
64
+ "glm-5-2-non-thinking": {},
65
+ "gpt-5-mini": {},
66
+ });
67
+
68
+ assert.deepEqual(defaults, {
69
+ model: "glm-5-2-non-thinking",
70
+ smallModel: "gpt-5-mini",
71
+ });
72
+ });
73
+
74
+ test("chooseDefaults falls back to explicit thinking variants when base models are absent", () => {
75
+ const defaults = chooseDefaults({
76
+ "kimi-k2p7-code-thinking": {},
77
+ "glm-5-2-non-thinking": {},
78
+ "kimi-k2-6-thinking": {},
79
+ });
80
+
81
+ assert.deepEqual(defaults, {
82
+ model: "kimi-k2p7-code-thinking",
83
+ smallModel: "kimi-k2p7-code-thinking",
41
84
  });
42
85
  });
43
86
 
@@ -584,9 +627,14 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
584
627
  textcortex: {
585
628
  models: {
586
629
  "minimax-m3-thinking": {},
587
- "kimi-k2-6": {},
630
+ "kimi-k2-6-non-thinking": {},
631
+ "kimi-k2-6-thinking": {},
632
+ "kimi-k2-5": {},
588
633
  "kimi-k2-5-thinking": {},
589
- "glm-5-1": {},
634
+ "glm-5-2-non-thinking": {},
635
+ "glm-5-2-thinking": {},
636
+ "glm-5-1-non-thinking": {},
637
+ "glm-5-1-thinking": {},
590
638
  },
591
639
  },
592
640
  }),
@@ -646,6 +694,11 @@ test("prepare-only refreshes stored Zenocode credentials when the access token h
646
694
  await fs.readFile(path.join(zenocodeHome, "opencode.jsonc"), "utf-8"),
647
695
  );
648
696
  assert.equal(savedConfig.model, "textcortex/minimax-m3-thinking");
697
+
698
+ const savedModels = JSON.parse(
699
+ await fs.readFile(path.join(zenocodeHome, "models.json"), "utf-8"),
700
+ );
701
+ assert.ok(savedModels.textcortex.models["glm-5-2-non-thinking"]);
649
702
  });
650
703
 
651
704
  test("login launches the runtime immediately with system TUI theming", async (t) => {
@@ -733,9 +786,14 @@ main().catch((error) => {
733
786
  textcortex: {
734
787
  models: {
735
788
  "minimax-m3-thinking": {},
736
- "kimi-k2-6": {},
789
+ "kimi-k2-6-non-thinking": {},
790
+ "kimi-k2-6-thinking": {},
791
+ "kimi-k2-5": {},
737
792
  "kimi-k2-5-thinking": {},
738
- "glm-5-1": {},
793
+ "glm-5-2-non-thinking": {},
794
+ "glm-5-2-thinking": {},
795
+ "glm-5-1-non-thinking": {},
796
+ "glm-5-1-thinking": {},
739
797
  },
740
798
  },
741
799
  }),
@@ -806,6 +864,11 @@ main().catch((error) => {
806
864
  assert.equal(opencodeConfig.theme, "system");
807
865
  assert.equal(opencodeConfig.model, "textcortex/minimax-m3-thinking");
808
866
 
867
+ const modelsConfig = JSON.parse(
868
+ await fs.readFile(path.join(zenocodeHome, "models.json"), "utf-8"),
869
+ );
870
+ assert.ok(modelsConfig.textcortex.models["glm-5-2-non-thinking"]);
871
+
809
872
  const tuiConfig = JSON.parse(
810
873
  await fs.readFile(runtimeInvocation.env.OPENCODE_TUI_CONFIG, "utf-8"),
811
874
  );