nvicode 0.1.13 → 0.1.15
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/dist/cli.js +170 -4
- package/dist/postinstall.js +74 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { constants, openSync } from "node:fs";
|
|
4
5
|
import { promises as fs } from "node:fs";
|
|
@@ -25,6 +26,7 @@ Commands:
|
|
|
25
26
|
nvicode activity Show recent request activity
|
|
26
27
|
nvicode dashboard Show usage summary and recent activity
|
|
27
28
|
nvicode launch claude [...] Launch Claude Code through nvicode
|
|
29
|
+
nvicode launch openclaw [...] Launch OpenClaw through nvicode
|
|
28
30
|
nvicode serve Run the local proxy in the foreground
|
|
29
31
|
`);
|
|
30
32
|
};
|
|
@@ -43,6 +45,9 @@ const getPathExts = () => {
|
|
|
43
45
|
const unique = (values) => [...new Set(values)];
|
|
44
46
|
const getProviderLabel = (provider) => provider === "openrouter" ? "OpenRouter" : "NVIDIA";
|
|
45
47
|
const getClaudeCommandNames = () => isWindows ? ["claude.exe", "claude.cmd", "claude.bat", "claude"] : ["claude"];
|
|
48
|
+
const getOpenClawCommandNames = () => isWindows
|
|
49
|
+
? ["openclaw.exe", "openclaw.cmd", "openclaw.bat", "openclaw"]
|
|
50
|
+
: ["openclaw"];
|
|
46
51
|
const getClaudeNativeNames = () => isWindows
|
|
47
52
|
? ["claude-native.exe", "claude-native.cmd", "claude-native.bat", "claude-native"]
|
|
48
53
|
: ["claude-native"];
|
|
@@ -320,6 +325,27 @@ const getUsageView = async () => {
|
|
|
320
325
|
return lines.join("\n");
|
|
321
326
|
};
|
|
322
327
|
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
328
|
+
const getNodeVersionParts = () => {
|
|
329
|
+
const parts = process.versions.node.split(".").map((part) => Number(part));
|
|
330
|
+
const major = parts[0] ?? 0;
|
|
331
|
+
const minor = parts[1] ?? 0;
|
|
332
|
+
const patch = parts[2] ?? 0;
|
|
333
|
+
return {
|
|
334
|
+
major: Number.isFinite(major) ? major : 0,
|
|
335
|
+
minor: Number.isFinite(minor) ? minor : 0,
|
|
336
|
+
patch: Number.isFinite(patch) ? patch : 0,
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const isNodeAtLeast = (major, minor = 0, patch = 0) => {
|
|
340
|
+
const current = getNodeVersionParts();
|
|
341
|
+
if (current.major !== major) {
|
|
342
|
+
return current.major > major;
|
|
343
|
+
}
|
|
344
|
+
if (current.minor !== minor) {
|
|
345
|
+
return current.minor > minor;
|
|
346
|
+
}
|
|
347
|
+
return current.patch >= patch;
|
|
348
|
+
};
|
|
323
349
|
const clearTerminal = () => {
|
|
324
350
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
325
351
|
};
|
|
@@ -587,6 +613,28 @@ const resolveClaudeBinary = async () => {
|
|
|
587
613
|
}
|
|
588
614
|
throw new Error("Unable to locate Claude Code binary.");
|
|
589
615
|
};
|
|
616
|
+
const resolveOpenClawBinary = async () => {
|
|
617
|
+
const homeBinCandidates = isWindows
|
|
618
|
+
? [
|
|
619
|
+
path.join(os.homedir(), ".local", "bin", "openclaw.exe"),
|
|
620
|
+
path.join(os.homedir(), ".local", "bin", "openclaw.cmd"),
|
|
621
|
+
path.join(os.homedir(), ".local", "bin", "openclaw.bat"),
|
|
622
|
+
path.join(os.homedir(), ".local", "bin", "openclaw"),
|
|
623
|
+
]
|
|
624
|
+
: [path.join(os.homedir(), ".local", "bin", "openclaw")];
|
|
625
|
+
for (const candidate of homeBinCandidates) {
|
|
626
|
+
if (await isExecutable(candidate)) {
|
|
627
|
+
return candidate;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
for (const name of getOpenClawCommandNames()) {
|
|
631
|
+
const openclawInPath = await findExecutableInPath(name);
|
|
632
|
+
if (openclawInPath) {
|
|
633
|
+
return openclawInPath;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
throw new Error("Unable to locate OpenClaw binary. Install OpenClaw first with `npm install -g openclaw@latest`.");
|
|
637
|
+
};
|
|
590
638
|
const findExecutableInPath = async (name) => {
|
|
591
639
|
const pathEntries = (process.env.PATH || "").split(path.delimiter);
|
|
592
640
|
for (const entry of pathEntries) {
|
|
@@ -601,6 +649,99 @@ const findExecutableInPath = async (name) => {
|
|
|
601
649
|
}
|
|
602
650
|
return null;
|
|
603
651
|
};
|
|
652
|
+
const buildOpenClawEnv = (baseEnv, activeApiKey) => ({
|
|
653
|
+
...baseEnv,
|
|
654
|
+
...(activeApiKey ? { CUSTOM_API_KEY: activeApiKey } : {}),
|
|
655
|
+
});
|
|
656
|
+
const getOpenClawPaths = () => {
|
|
657
|
+
const paths = getNvicodePaths();
|
|
658
|
+
return {
|
|
659
|
+
markerFile: path.join(paths.stateDir, "openclaw-profile.json"),
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
const getOpenClawProviderId = (config) => config.provider === "openrouter" ? "nvicode-openrouter" : "nvicode-nvidia";
|
|
663
|
+
const getOpenClawModelPath = (config) => `${getOpenClawProviderId(config)}/${getActiveModel(config)}`;
|
|
664
|
+
const getOpenClawProfileSignature = (config, activeApiKey) => createHash("sha256")
|
|
665
|
+
.update(JSON.stringify({
|
|
666
|
+
provider: config.provider,
|
|
667
|
+
model: getActiveModel(config),
|
|
668
|
+
apiKey: activeApiKey,
|
|
669
|
+
}))
|
|
670
|
+
.digest("hex");
|
|
671
|
+
const runOpenClawCommand = async (openclawBinary, args, env) => {
|
|
672
|
+
const child = spawnClaudeProcess(openclawBinary, args, env);
|
|
673
|
+
await new Promise((resolve, reject) => {
|
|
674
|
+
child.on("exit", (code, signal) => {
|
|
675
|
+
if (signal) {
|
|
676
|
+
reject(new Error(`OpenClaw exited with signal ${signal}`));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if ((code ?? 0) !== 0) {
|
|
680
|
+
reject(new Error(`OpenClaw command failed with exit code ${code ?? 0}`));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
resolve();
|
|
684
|
+
});
|
|
685
|
+
child.on("error", reject);
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
const ensureOpenClawConfigured = async (openclawBinary, config, activeApiKey) => {
|
|
689
|
+
if (!isNodeAtLeast(22, 14, 0)) {
|
|
690
|
+
throw new Error(`OpenClaw requires Node.js >=22.14.0. Current version is ${process.versions.node}.`);
|
|
691
|
+
}
|
|
692
|
+
const openclawPaths = getOpenClawPaths();
|
|
693
|
+
const baseEnv = buildOpenClawEnv(process.env, activeApiKey);
|
|
694
|
+
await fs.mkdir(path.dirname(openclawPaths.markerFile), { recursive: true });
|
|
695
|
+
const profileSignature = getOpenClawProfileSignature(config, activeApiKey);
|
|
696
|
+
const existingMarkerRaw = await readIfExists(openclawPaths.markerFile);
|
|
697
|
+
if (existingMarkerRaw) {
|
|
698
|
+
try {
|
|
699
|
+
const parsed = JSON.parse(existingMarkerRaw);
|
|
700
|
+
if (parsed.signature === profileSignature) {
|
|
701
|
+
return { env: baseEnv, updated: false };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// continue with reprovisioning
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const providerId = getOpenClawProviderId(config);
|
|
709
|
+
const onboardArgs = [
|
|
710
|
+
"onboard",
|
|
711
|
+
"--non-interactive",
|
|
712
|
+
"--accept-risk",
|
|
713
|
+
"--flow",
|
|
714
|
+
"advanced",
|
|
715
|
+
"--auth-choice",
|
|
716
|
+
"custom-api-key",
|
|
717
|
+
"--secret-input-mode",
|
|
718
|
+
"ref",
|
|
719
|
+
"--custom-provider-id",
|
|
720
|
+
providerId,
|
|
721
|
+
"--custom-compatibility",
|
|
722
|
+
"openai",
|
|
723
|
+
"--custom-base-url",
|
|
724
|
+
config.provider === "openrouter"
|
|
725
|
+
? "https://openrouter.ai/api/v1"
|
|
726
|
+
: "https://integrate.api.nvidia.com/v1",
|
|
727
|
+
"--custom-model-id",
|
|
728
|
+
getActiveModel(config),
|
|
729
|
+
"--no-install-daemon",
|
|
730
|
+
"--skip-channels",
|
|
731
|
+
"--skip-skills",
|
|
732
|
+
"--skip-health",
|
|
733
|
+
"--skip-ui",
|
|
734
|
+
];
|
|
735
|
+
await runOpenClawCommand(openclawBinary, onboardArgs, baseEnv);
|
|
736
|
+
await runOpenClawCommand(openclawBinary, ["config", "set", "agents.defaults.model.primary", JSON.stringify(getOpenClawModelPath(config))], baseEnv);
|
|
737
|
+
await fs.writeFile(openclawPaths.markerFile, `${JSON.stringify({
|
|
738
|
+
signature: profileSignature,
|
|
739
|
+
provider: config.provider,
|
|
740
|
+
model: getActiveModel(config),
|
|
741
|
+
configuredAt: new Date().toISOString(),
|
|
742
|
+
}, null, 2)}\n`);
|
|
743
|
+
return { env: baseEnv, updated: true };
|
|
744
|
+
};
|
|
604
745
|
const spawnClaudeProcess = (claudeBinary, args, env) => {
|
|
605
746
|
if (isWindows && /\.(cmd|bat)$/i.test(claudeBinary)) {
|
|
606
747
|
return spawn(claudeBinary, args, {
|
|
@@ -616,6 +757,27 @@ const spawnClaudeProcess = (claudeBinary, args, env) => {
|
|
|
616
757
|
windowsHide: true,
|
|
617
758
|
});
|
|
618
759
|
};
|
|
760
|
+
const runLaunchOpenClaw = async (args) => {
|
|
761
|
+
const config = await ensureConfigured();
|
|
762
|
+
const activeApiKey = getActiveApiKey(config);
|
|
763
|
+
const openclawBinary = await resolveOpenClawBinary();
|
|
764
|
+
const { env, updated } = await ensureOpenClawConfigured(openclawBinary, config, activeApiKey);
|
|
765
|
+
if (updated) {
|
|
766
|
+
console.error("nvicode updated the default OpenClaw config. Restart the OpenClaw gateway to apply it: `openclaw gateway restart`.");
|
|
767
|
+
}
|
|
768
|
+
const child = spawnClaudeProcess(openclawBinary, args, env);
|
|
769
|
+
await new Promise((resolve, reject) => {
|
|
770
|
+
child.on("exit", (code, signal) => {
|
|
771
|
+
if (signal) {
|
|
772
|
+
reject(new Error(`OpenClaw exited with signal ${signal}`));
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
process.exitCode = code ?? 0;
|
|
776
|
+
resolve();
|
|
777
|
+
});
|
|
778
|
+
child.on("error", reject);
|
|
779
|
+
});
|
|
780
|
+
};
|
|
619
781
|
const runLaunchClaude = async (args) => {
|
|
620
782
|
const config = await ensureConfigured();
|
|
621
783
|
const routingStatus = await ensurePersistentClaudeRouting().catch(() => "skipped");
|
|
@@ -727,11 +889,15 @@ const main = async () => {
|
|
|
727
889
|
return;
|
|
728
890
|
}
|
|
729
891
|
if (command === "launch") {
|
|
730
|
-
if (rest[0]
|
|
731
|
-
|
|
892
|
+
if (rest[0] === "claude") {
|
|
893
|
+
await runLaunchClaude(rest.slice(1));
|
|
894
|
+
return;
|
|
732
895
|
}
|
|
733
|
-
|
|
734
|
-
|
|
896
|
+
if (rest[0] === "openclaw") {
|
|
897
|
+
await runLaunchOpenClaw(rest.slice(1));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
throw new Error("Supported launch targets are `claude` and `openclaw`.");
|
|
735
901
|
}
|
|
736
902
|
throw new Error(`Unknown command: ${command}`);
|
|
737
903
|
};
|
package/dist/postinstall.js
CHANGED
|
@@ -5,19 +5,86 @@ import process from "node:process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { loadConfig } from "./config.js";
|
|
7
7
|
const isTruthy = (value) => value === "1" || value === "true" || value === "yes";
|
|
8
|
-
const isGlobalInstall = () => process.env.npm_config_global === "true" ||
|
|
9
|
-
process.env.npm_config_location === "global";
|
|
10
8
|
const isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
9
|
+
const getPackageRoot = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
10
|
const getCliPath = () => path.join(path.dirname(fileURLToPath(import.meta.url)), "cli.js");
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
11
|
+
const runNpmCommand = (args) => {
|
|
12
|
+
const result = process.env.npm_execpath
|
|
13
|
+
? spawnSync(process.execPath, [process.env.npm_execpath, ...args], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
16
|
+
})
|
|
17
|
+
: spawnSync("npm", args, {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
20
|
+
});
|
|
21
|
+
if (result.status !== 0) {
|
|
15
22
|
return null;
|
|
16
23
|
}
|
|
17
|
-
|
|
24
|
+
const output = result.stdout?.trim();
|
|
25
|
+
return output ? output : null;
|
|
26
|
+
};
|
|
27
|
+
const getContextFromPrefix = (prefix) => ({
|
|
28
|
+
binDir: process.platform === "win32" ? prefix : path.join(prefix, "bin"),
|
|
29
|
+
globalRoot: process.platform === "win32"
|
|
30
|
+
? path.join(prefix, "node_modules")
|
|
31
|
+
: path.join(prefix, "lib", "node_modules"),
|
|
32
|
+
});
|
|
33
|
+
const getContextFromGlobalRoot = (globalRoot) => {
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
return {
|
|
36
|
+
binDir: path.dirname(globalRoot),
|
|
37
|
+
globalRoot,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const suffix = path.join("lib", "node_modules");
|
|
41
|
+
if (!globalRoot.endsWith(suffix)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const prefix = path.dirname(path.dirname(globalRoot));
|
|
45
|
+
return {
|
|
46
|
+
binDir: path.join(prefix, "bin"),
|
|
47
|
+
globalRoot,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const isWithinPath = (targetPath, parentPath) => {
|
|
51
|
+
const relativePath = path.relative(parentPath, targetPath);
|
|
52
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
53
|
+
};
|
|
54
|
+
const getGlobalBinDir = async () => {
|
|
55
|
+
const packageRoot = await fs.realpath(getPackageRoot()).catch(() => getPackageRoot());
|
|
56
|
+
const contexts = new Map();
|
|
57
|
+
const envPrefix = process.env.npm_config_prefix || process.env.PREFIX;
|
|
58
|
+
if (envPrefix) {
|
|
59
|
+
const context = getContextFromPrefix(envPrefix);
|
|
60
|
+
contexts.set(`${context.globalRoot}|${context.binDir}`, context);
|
|
61
|
+
}
|
|
62
|
+
const npmPrefix = runNpmCommand(["prefix", "-g"]);
|
|
63
|
+
if (npmPrefix) {
|
|
64
|
+
const context = getContextFromPrefix(npmPrefix);
|
|
65
|
+
contexts.set(`${context.globalRoot}|${context.binDir}`, context);
|
|
66
|
+
}
|
|
67
|
+
const npmGlobalRoot = runNpmCommand(["root", "-g"]);
|
|
68
|
+
if (npmGlobalRoot) {
|
|
69
|
+
const context = getContextFromGlobalRoot(npmGlobalRoot);
|
|
70
|
+
if (context) {
|
|
71
|
+
contexts.set(`${context.globalRoot}|${context.binDir}`, context);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const context of contexts.values()) {
|
|
75
|
+
const globalRoot = await fs.realpath(context.globalRoot).catch(() => path.resolve(context.globalRoot));
|
|
76
|
+
if (isWithinPath(packageRoot, globalRoot)) {
|
|
77
|
+
return context.binDir;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (process.env.npm_config_global === "true" ||
|
|
81
|
+
process.env.npm_config_location === "global") {
|
|
82
|
+
return envPrefix ? getContextFromPrefix(envPrefix).binDir : null;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
18
85
|
};
|
|
19
86
|
const createGlobalLauncher = async () => {
|
|
20
|
-
const binDir = getGlobalBinDir();
|
|
87
|
+
const binDir = await getGlobalBinDir();
|
|
21
88
|
if (!binDir) {
|
|
22
89
|
return "skipped";
|
|
23
90
|
}
|
|
@@ -54,9 +121,6 @@ const main = async () => {
|
|
|
54
121
|
if (isTruthy(process.env.NVICODE_SKIP_POSTINSTALL) || isTruthy(process.env.CI)) {
|
|
55
122
|
return;
|
|
56
123
|
}
|
|
57
|
-
if (!isGlobalInstall()) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
124
|
try {
|
|
61
125
|
await createGlobalLauncher();
|
|
62
126
|
}
|