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 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] !== "claude") {
731
- throw new Error("Only `nvicode launch claude` is supported right now.");
892
+ if (rest[0] === "claude") {
893
+ await runLaunchClaude(rest.slice(1));
894
+ return;
732
895
  }
733
- await runLaunchClaude(rest.slice(1));
734
- return;
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
  };
@@ -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 getGlobalBinDir = () => {
13
- const prefix = process.env.npm_config_prefix || process.env.PREFIX;
14
- if (!prefix) {
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
- return process.platform === "win32" ? prefix : path.join(prefix, "bin");
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nvicode",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "NviCode - Introducing one click Nvidia/OpenRouter keys to Claude Code. Free Claude code.",
5
5
  "author": "Dinesh Potla",
6
6
  "keywords": [