@textcortex/zenocode 0.1.9 → 0.1.10

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.9",
3
+ "version": "0.1.10",
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",
@@ -237,6 +237,14 @@ async function packPackage(packageDir) {
237
237
  return path.join(packageDir, tarballs[tarballs.length - 1]);
238
238
  }
239
239
 
240
+ export function buildPublishCommandArgs(tarballPath, { tag } = {}) {
241
+ const args = ["publish", tarballPath, "--access", "public"];
242
+ if (tag) {
243
+ args.push("--tag", tag);
244
+ }
245
+ return args;
246
+ }
247
+
240
248
  async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
241
249
  if (!publishEnabled) return { published: false, skipped: false };
242
250
 
@@ -254,11 +262,7 @@ async function publishTarballIfNeeded(packageName, version, tarballPath, cwd) {
254
262
  }
255
263
 
256
264
  const npmCommand = _command("npm");
257
- await run(
258
- npmCommand,
259
- ["publish", tarballPath, "--access", "public", "--tag", publishTag, "--provenance"],
260
- { cwd },
261
- );
265
+ await run(npmCommand, buildPublishCommandArgs(tarballPath, { tag: publishTag }), { cwd });
262
266
  return { published: true, skipped: false };
263
267
  }
264
268
 
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
+ buildPublishCommandArgs,
4
5
  buildWrapperBinMap,
5
6
  mapBrandedBinaryPackageName,
6
7
  } from "./build-branded-opencode.mjs";
@@ -23,3 +24,23 @@ test("buildWrapperBinMap includes package, opencode, and zenocode entrypoints",
23
24
  zenocode: "./bin/opencode",
24
25
  });
25
26
  });
27
+
28
+ test("buildPublishCommandArgs omits npm provenance for runtime tarball publishing", () => {
29
+ assert.deepEqual(buildPublishCommandArgs("package.tgz", { tag: "latest" }), [
30
+ "publish",
31
+ "package.tgz",
32
+ "--access",
33
+ "public",
34
+ "--tag",
35
+ "latest",
36
+ ]);
37
+ });
38
+
39
+ test("buildPublishCommandArgs omits tag arguments when no tag is provided", () => {
40
+ assert.deepEqual(buildPublishCommandArgs("package.tgz"), [
41
+ "publish",
42
+ "package.tgz",
43
+ "--access",
44
+ "public",
45
+ ]);
46
+ });
@@ -31,6 +31,7 @@ const modelsPath = path.join(runtimeDir, "models.json");
31
31
  const configPath = path.join(runtimeDir, "opencode.jsonc");
32
32
  const localBaseUrlDefault = "http://127.0.0.1:8080";
33
33
  const cloudBaseUrlDefault = "https://api.textcortex.com";
34
+ const localBaseUrlFlags = new Set(["--local", "--localhost"]);
34
35
 
35
36
  const providerID = "textcortex";
36
37
  const configuredOpencodePackage =
@@ -492,6 +493,15 @@ function sleep(ms) {
492
493
  return new Promise((resolve) => setTimeout(resolve, ms));
493
494
  }
494
495
 
496
+ export function hasLocalBaseUrlFlag(args = []) {
497
+ const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
498
+ return normalizedArgs.some((arg) => localBaseUrlFlags.has(arg));
499
+ }
500
+
501
+ function stripLocalBaseUrlFlags(args = []) {
502
+ return args.filter((arg) => !localBaseUrlFlags.has(arg));
503
+ }
504
+
495
505
  async function parseLoginArgs(args) {
496
506
  const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
497
507
  let emailHint = process.env.TEXTCORTEX_LOGIN_EMAIL || null;
@@ -499,6 +509,9 @@ async function parseLoginArgs(args) {
499
509
 
500
510
  for (let idx = 0; idx < normalizedArgs.length; idx += 1) {
501
511
  const arg = normalizedArgs[idx];
512
+ if (localBaseUrlFlags.has(arg)) {
513
+ continue;
514
+ }
502
515
  if (arg === "--no-launch-browser") {
503
516
  launchBrowser = false;
504
517
  continue;
@@ -515,6 +528,7 @@ async function parseLoginArgs(args) {
515
528
  if (arg === "--help" || arg === "-h") {
516
529
  console.log("Zenocode login options:");
517
530
  console.log(" --email <address> Optional email hint for tenant/SSO routing");
531
+ console.log(` --local Use the local FastAPI backend at ${localBaseUrlDefault}`);
518
532
  console.log(" --no-launch-browser Do not open browser automatically");
519
533
  process.exit(0);
520
534
  }
@@ -543,10 +557,29 @@ function isLoginRouteNotFoundError(error) {
543
557
  return message.includes("Login initiate failed (404)");
544
558
  }
545
559
 
560
+ export function shouldFallbackLoginToCloud({ baseUrl, hasExplicitBaseUrl, error }) {
561
+ if (hasExplicitBaseUrl || baseUrl !== localBaseUrlDefault) {
562
+ return false;
563
+ }
564
+
565
+ return isFetchFailedError(error) || isLoginRouteNotFoundError(error);
566
+ }
567
+
568
+ export function resolveTextCortexBaseUrl({
569
+ envBaseUrl,
570
+ storedBaseUrl,
571
+ preferLocalhost = false,
572
+ } = {}) {
573
+ if (preferLocalhost) {
574
+ return localBaseUrlDefault;
575
+ }
576
+ return envBaseUrl || storedBaseUrl || cloudBaseUrlDefault;
577
+ }
578
+
546
579
  function _loginConnectivityHelp(baseUrl) {
547
580
  return [
548
581
  `Cannot reach Zenocode auth endpoint at ${baseUrl}.`,
549
- "Run local backend FastAPI (`cd backend && uv run dev_fastapi`) or set TEXTCORTEX_BASE_URL to a reachable backend API.",
582
+ `Set TEXTCORTEX_BASE_URL to ${cloudBaseUrlDefault} or, for local development, run local backend FastAPI (\`cd backend && uv run dev_fastapi\`).`,
550
583
  ].join(" ");
551
584
  }
552
585
 
@@ -655,18 +688,20 @@ async function saveRuntimeCredentials(baseUrl, tokenData) {
655
688
  await clearLogoutMarker();
656
689
  }
657
690
 
658
- async function runLoginCommand(baseUrl, args) {
691
+ async function runLoginCommand(baseUrl, args, options = {}) {
692
+ const preferLocalhost = options.preferLocalhost === true;
659
693
  const { emailHint, launchBrowser } = await parseLoginArgs(args);
660
- let resolvedBaseUrl = baseUrl;
694
+ let resolvedBaseUrl = preferLocalhost ? localBaseUrlDefault : baseUrl;
661
695
  let login;
662
696
  try {
663
697
  login = await initiateDeviceLogin(resolvedBaseUrl, emailHint);
664
698
  } catch (error) {
665
- if (
666
- !process.env.TEXTCORTEX_BASE_URL &&
667
- resolvedBaseUrl === localBaseUrlDefault &&
668
- isFetchFailedError(error)
669
- ) {
699
+ if (shouldFallbackLoginToCloud({
700
+ baseUrl: resolvedBaseUrl,
701
+ hasExplicitBaseUrl:
702
+ Boolean(process.env.TEXTCORTEX_BASE_URL) || preferLocalhost,
703
+ error,
704
+ })) {
670
705
  resolvedBaseUrl = cloudBaseUrlDefault;
671
706
  console.log(
672
707
  `Local backend not reachable at ${localBaseUrlDefault}. Falling back to ${cloudBaseUrlDefault}.`,
@@ -1225,6 +1260,7 @@ export async function runRuntimeWithSessionRecovery({
1225
1260
  token,
1226
1261
  childOptions,
1227
1262
  canAutoLoginRuntime,
1263
+ preferLocalhost = false,
1228
1264
  refreshTokenFn = refreshStoredCredentials,
1229
1265
  runLogin = runLoginCommand,
1230
1266
  resolveTokenFn = resolveToken,
@@ -1283,9 +1319,16 @@ export async function runRuntimeWithSessionRecovery({
1283
1319
  }
1284
1320
 
1285
1321
  console.log("Zenocode session expired. Starting login flow...\n");
1286
- await runLogin(activeBaseUrl, buildAutoLoginArgs());
1322
+ await runLogin(activeBaseUrl, buildAutoLoginArgs({ preferLocalhost }), {
1323
+ preferLocalhost,
1324
+ });
1287
1325
  activeToken = await resolveTokenFn();
1288
- activeBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrlFn()) || activeBaseUrl;
1326
+ activeBaseUrl =
1327
+ resolveTextCortexBaseUrl({
1328
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1329
+ storedBaseUrl: await resolveStoredBaseUrlFn(),
1330
+ preferLocalhost,
1331
+ }) || activeBaseUrl;
1289
1332
  await prepareRuntimeFn(activeBaseUrl, activeToken);
1290
1333
  const retryDelayMs = Math.min(
1291
1334
  Math.max(recoveryDelayMs, 0) * recoveryAttempts,
@@ -1366,11 +1409,18 @@ function maybeRenderBanner(args) {
1366
1409
  console.log(`${buildZenocodeBanner()}\n`);
1367
1410
  }
1368
1411
 
1369
- function buildAutoLoginArgs() {
1370
- return process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
1412
+ function buildAutoLoginArgs({ preferLocalhost = false } = {}) {
1413
+ const loginArgs = [];
1414
+ if (preferLocalhost) {
1415
+ loginArgs.push("--local");
1416
+ }
1417
+ if (
1418
+ process.env.ZENOCODE_AUTO_LOGIN_NO_BROWSER === "1" ||
1371
1419
  process.env.CODECORTEX_AUTO_LOGIN_NO_BROWSER === "1"
1372
- ? ["--no-launch-browser"]
1373
- : [];
1420
+ ) {
1421
+ loginArgs.push("--no-launch-browser");
1422
+ }
1423
+ return loginArgs;
1374
1424
  }
1375
1425
 
1376
1426
  function isMissingTokenError(error) {
@@ -1443,7 +1493,8 @@ function shouldAttemptAutoLogin(error, args) {
1443
1493
  return canAutoLogin(args);
1444
1494
  }
1445
1495
 
1446
- async function resolveTokenWithAutoLogin(baseUrl, args) {
1496
+ async function resolveTokenWithAutoLogin(baseUrl, args, options = {}) {
1497
+ const preferLocalhost = options.preferLocalhost === true;
1447
1498
  try {
1448
1499
  const token = await resolveToken();
1449
1500
  return { token, baseUrl };
@@ -1453,14 +1504,21 @@ async function resolveTokenWithAutoLogin(baseUrl, args) {
1453
1504
  }
1454
1505
 
1455
1506
  console.log("No local Zenocode credentials found. Starting login flow...\n");
1456
- await runLoginCommand(baseUrl, buildAutoLoginArgs());
1507
+ await runLoginCommand(baseUrl, buildAutoLoginArgs({ preferLocalhost }), {
1508
+ preferLocalhost,
1509
+ });
1457
1510
  const token = await resolveToken();
1458
- const persistedBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrl()) || baseUrl;
1511
+ const persistedBaseUrl = resolveTextCortexBaseUrl({
1512
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1513
+ storedBaseUrl: await resolveStoredBaseUrl(),
1514
+ preferLocalhost,
1515
+ });
1459
1516
  return { token, baseUrl: persistedBaseUrl };
1460
1517
  }
1461
1518
  }
1462
1519
 
1463
- async function prepareRuntimeWithAutoLogin(baseUrl, token, args) {
1520
+ async function prepareRuntimeWithAutoLogin(baseUrl, token, args, options = {}) {
1521
+ const preferLocalhost = options.preferLocalhost === true;
1464
1522
  try {
1465
1523
  const model = await prepareRuntime(baseUrl, token);
1466
1524
  return { model, token, baseUrl };
@@ -1484,9 +1542,15 @@ async function prepareRuntimeWithAutoLogin(baseUrl, token, args) {
1484
1542
  }
1485
1543
 
1486
1544
  console.log("Zenocode session expired. Starting login flow...\n");
1487
- await runLoginCommand(baseUrl, buildAutoLoginArgs());
1545
+ await runLoginCommand(baseUrl, buildAutoLoginArgs({ preferLocalhost }), {
1546
+ preferLocalhost,
1547
+ });
1488
1548
  const refreshedToken = await resolveToken();
1489
- const refreshedBaseUrl = process.env.TEXTCORTEX_BASE_URL || (await resolveStoredBaseUrl()) || baseUrl;
1549
+ const refreshedBaseUrl = resolveTextCortexBaseUrl({
1550
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1551
+ storedBaseUrl: await resolveStoredBaseUrl(),
1552
+ preferLocalhost,
1553
+ });
1490
1554
  const model = await prepareRuntime(refreshedBaseUrl, refreshedToken);
1491
1555
  return { model, token: refreshedToken, baseUrl: refreshedBaseUrl };
1492
1556
  }
@@ -1503,12 +1567,18 @@ async function main() {
1503
1567
  passthrough.shift();
1504
1568
  }
1505
1569
 
1570
+ const preferLocalhost = hasLocalBaseUrlFlag(passthrough);
1571
+ const runtimeArgs = stripLocalBaseUrlFlags(passthrough);
1506
1572
  const storedBaseUrl = await resolveStoredBaseUrl();
1507
- const baseUrl = process.env.TEXTCORTEX_BASE_URL || storedBaseUrl || localBaseUrlDefault;
1508
- const subcommand = passthrough[0];
1573
+ const baseUrl = resolveTextCortexBaseUrl({
1574
+ envBaseUrl: process.env.TEXTCORTEX_BASE_URL,
1575
+ storedBaseUrl,
1576
+ preferLocalhost,
1577
+ });
1578
+ const subcommand = runtimeArgs[0];
1509
1579
 
1510
1580
  if (subcommand === "login") {
1511
- await runLoginCommand(baseUrl, passthrough.slice(1));
1581
+ await runLoginCommand(baseUrl, runtimeArgs.slice(1), { preferLocalhost });
1512
1582
  return;
1513
1583
  }
1514
1584
 
@@ -1517,12 +1587,15 @@ async function main() {
1517
1587
  return;
1518
1588
  }
1519
1589
 
1520
- maybeRenderBanner(passthrough);
1521
- const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, passthrough);
1590
+ maybeRenderBanner(runtimeArgs);
1591
+ const tokenResolution = await resolveTokenWithAutoLogin(baseUrl, runtimeArgs, {
1592
+ preferLocalhost,
1593
+ });
1522
1594
  const runtime = await prepareRuntimeWithAutoLogin(
1523
1595
  tokenResolution.baseUrl,
1524
1596
  tokenResolution.token,
1525
- passthrough,
1597
+ runtimeArgs,
1598
+ { preferLocalhost },
1526
1599
  );
1527
1600
  const token = runtime.token;
1528
1601
  const model = runtime.model;
@@ -1539,14 +1612,15 @@ async function main() {
1539
1612
  TEXTCORTEX_API_KEY: token,
1540
1613
  },
1541
1614
  };
1542
- const monitorRuntimeSession = canAutoLogin(passthrough);
1615
+ const monitorRuntimeSession = canAutoLogin(runtimeArgs);
1543
1616
 
1544
1617
  if (opencodeBinaryPath) {
1545
1618
  const result = await runRuntimeWithSessionRecovery({
1546
- args: passthrough,
1619
+ args: runtimeArgs,
1547
1620
  baseUrl: runtime.baseUrl,
1548
1621
  token,
1549
1622
  childOptions,
1623
+ preferLocalhost,
1550
1624
  canAutoLoginRuntime: monitorRuntimeSession,
1551
1625
  launchRuntimeFn: ({ args, childOptions }) =>
1552
1626
  runRuntimeBinary(opencodeBinaryPath, args, childOptions, monitorRuntimeSession),
@@ -1559,10 +1633,11 @@ async function main() {
1559
1633
  const pinnedRuntimePath = await resolvePinnedRuntimeBinary(launchPackage, childOptions);
1560
1634
  if (pinnedRuntimePath) {
1561
1635
  const result = await runRuntimeWithSessionRecovery({
1562
- args: passthrough,
1636
+ args: runtimeArgs,
1563
1637
  baseUrl: runtime.baseUrl,
1564
1638
  token,
1565
1639
  childOptions,
1640
+ preferLocalhost,
1566
1641
  canAutoLoginRuntime: monitorRuntimeSession,
1567
1642
  launchRuntimeFn: ({ args, childOptions }) =>
1568
1643
  runRuntimeBinary(pinnedRuntimePath, args, childOptions, monitorRuntimeSession),
@@ -1570,7 +1645,7 @@ async function main() {
1570
1645
  exitWithChildResult(result);
1571
1646
  return;
1572
1647
  }
1573
- await runPackageLauncher(launchPackage, passthrough, childOptions);
1648
+ await runPackageLauncher(launchPackage, runtimeArgs, childOptions);
1574
1649
  }
1575
1650
 
1576
1651
  const resolveExecutablePath = (value) => {
@@ -15,8 +15,11 @@ import {
15
15
  canRecoverRuntimeSessionFromTranscript,
16
16
  buildZenocodeBanner,
17
17
  chooseDefaults,
18
+ hasLocalBaseUrlFlag,
18
19
  resolveLoginSuccessIdentifier,
20
+ resolveTextCortexBaseUrl,
19
21
  runRuntimeWithSessionRecovery,
22
+ shouldFallbackLoginToCloud,
20
23
  writePrivateJsonFile,
21
24
  } from "./run-zenocode.mjs";
22
25
 
@@ -33,6 +36,65 @@ test("chooseDefaults prefers kimi k2.5 thinking for Zenocode", () => {
33
36
  });
34
37
  });
35
38
 
39
+ test("resolveTextCortexBaseUrl defaults to the cloud API for packaged usage", () => {
40
+ assert.equal(
41
+ resolveTextCortexBaseUrl({
42
+ envBaseUrl: "",
43
+ storedBaseUrl: null,
44
+ }),
45
+ "https://api.textcortex.com",
46
+ );
47
+ });
48
+
49
+ test("resolveTextCortexBaseUrl prefers the explicit env var over stored credentials", () => {
50
+ assert.equal(
51
+ resolveTextCortexBaseUrl({
52
+ envBaseUrl: "https://staging.textcortex.com",
53
+ storedBaseUrl: "http://127.0.0.1:8080",
54
+ }),
55
+ "https://staging.textcortex.com",
56
+ );
57
+ });
58
+
59
+ test("resolveTextCortexBaseUrl prefers localhost when the local flag is enabled", () => {
60
+ assert.equal(
61
+ resolveTextCortexBaseUrl({
62
+ envBaseUrl: "https://staging.textcortex.com",
63
+ storedBaseUrl: "https://api.textcortex.com",
64
+ preferLocalhost: true,
65
+ }),
66
+ "http://127.0.0.1:8080",
67
+ );
68
+ });
69
+
70
+ test("hasLocalBaseUrlFlag detects localhost flags", () => {
71
+ assert.equal(hasLocalBaseUrlFlag(["login", "--local"]), true);
72
+ assert.equal(hasLocalBaseUrlFlag(["--localhost", "run"]), true);
73
+ assert.equal(hasLocalBaseUrlFlag(["run", "--help"]), false);
74
+ });
75
+
76
+ test("shouldFallbackLoginToCloud retries the cloud API when the local auth route returns 404", () => {
77
+ assert.equal(
78
+ shouldFallbackLoginToCloud({
79
+ baseUrl: "http://127.0.0.1:8080",
80
+ hasExplicitBaseUrl: false,
81
+ error: new Error("Login initiate failed (404): not found"),
82
+ }),
83
+ true,
84
+ );
85
+ });
86
+
87
+ test("shouldFallbackLoginToCloud does not override an explicit base URL", () => {
88
+ assert.equal(
89
+ shouldFallbackLoginToCloud({
90
+ baseUrl: "http://127.0.0.1:8080",
91
+ hasExplicitBaseUrl: true,
92
+ error: new Error("fetch failed"),
93
+ }),
94
+ false,
95
+ );
96
+ });
97
+
36
98
  test("buildOpenCodeConfig includes an openai stub for fallback runtime auth plugins", () => {
37
99
  const config = buildOpenCodeConfig({
38
100
  baseUrl: "http://127.0.0.1:8080",
@@ -253,6 +315,61 @@ test("runRuntimeWithSessionRecovery refreshes stored credentials before forcing
253
315
  assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
254
316
  });
255
317
 
318
+ test("runRuntimeWithSessionRecovery preserves explicit localhost preference during login recovery", async () => {
319
+ const events = [];
320
+ let launchCount = 0;
321
+
322
+ const result = await runRuntimeWithSessionRecovery({
323
+ args: ["run"],
324
+ baseUrl: "http://127.0.0.1:8080",
325
+ token: "token-1",
326
+ childOptions: {
327
+ cwd: process.cwd(),
328
+ env: {},
329
+ },
330
+ canAutoLoginRuntime: true,
331
+ preferLocalhost: true,
332
+ refreshTokenFn: async (baseUrl) => {
333
+ events.push(["refresh", baseUrl]);
334
+ throw new Error("refresh failed");
335
+ },
336
+ runLogin: async (baseUrl, loginArgs, options) => {
337
+ events.push(["login", baseUrl, loginArgs, options]);
338
+ },
339
+ resolveTokenFn: async () => {
340
+ events.push(["resolve-token"]);
341
+ return "token-2";
342
+ },
343
+ resolveStoredBaseUrlFn: async () => {
344
+ events.push(["resolve-base-url"]);
345
+ return "https://api.textcortex.com";
346
+ },
347
+ prepareRuntimeFn: async (baseUrl, token) => {
348
+ events.push(["prepare", baseUrl, token]);
349
+ return "kimi-k2-5-thinking";
350
+ },
351
+ launchRuntimeFn: async ({ childOptions }) => {
352
+ launchCount += 1;
353
+ events.push(["launch", launchCount, childOptions.env.TEXTCORTEX_API_KEY]);
354
+ if (launchCount === 1) {
355
+ return { expiredSession: true };
356
+ }
357
+ return { code: 0, signal: null, expiredSession: false };
358
+ },
359
+ });
360
+
361
+ assert.deepEqual(events, [
362
+ ["launch", 1, "token-1"],
363
+ ["refresh", "http://127.0.0.1:8080"],
364
+ ["login", "http://127.0.0.1:8080", ["--local"], { preferLocalhost: true }],
365
+ ["resolve-token"],
366
+ ["resolve-base-url"],
367
+ ["prepare", "http://127.0.0.1:8080", "token-2"],
368
+ ["launch", 2, "token-2"],
369
+ ]);
370
+ assert.deepEqual(result, { code: 0, signal: null, expiredSession: false });
371
+ });
372
+
256
373
  test("runRuntimeWithSessionRecovery stops after repeated recovery failures", async () => {
257
374
  const events = [];
258
375
  let launchCount = 0;