@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 +2 -1
- package/package.json +1 -1
- package/scripts/run-zenocode.mjs +15 -2
- package/scripts/run-zenocode.test.mjs +162 -0
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
package/scripts/run-zenocode.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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");
|