nvicode 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/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,105 @@ const findExecutableInPath = async (name) => {
601
649
  }
602
650
  return null;
603
651
  };
652
+ const buildOpenClawEnv = (baseEnv, openclawHome, activeApiKey) => ({
653
+ ...baseEnv,
654
+ OPENCLAW_HOME: openclawHome,
655
+ ...(activeApiKey ? { CUSTOM_API_KEY: activeApiKey } : {}),
656
+ });
657
+ const getOpenClawPaths = () => {
658
+ const paths = getNvicodePaths();
659
+ const openclawHome = path.join(paths.stateDir, "openclaw");
660
+ return {
661
+ homeDir: openclawHome,
662
+ workspaceDir: path.join(openclawHome, "workspace"),
663
+ markerFile: path.join(openclawHome, "nvicode-profile.json"),
664
+ };
665
+ };
666
+ const getOpenClawProviderId = (config) => config.provider === "openrouter" ? "nvicode-openrouter" : "nvicode-nvidia";
667
+ const getOpenClawModelPath = (config) => `${getOpenClawProviderId(config)}/${getActiveModel(config)}`;
668
+ const getOpenClawProfileSignature = (config, activeApiKey) => createHash("sha256")
669
+ .update(JSON.stringify({
670
+ provider: config.provider,
671
+ model: getActiveModel(config),
672
+ apiKey: activeApiKey,
673
+ }))
674
+ .digest("hex");
675
+ const runOpenClawCommand = async (openclawBinary, args, env) => {
676
+ const child = spawnClaudeProcess(openclawBinary, args, env);
677
+ await new Promise((resolve, reject) => {
678
+ child.on("exit", (code, signal) => {
679
+ if (signal) {
680
+ reject(new Error(`OpenClaw exited with signal ${signal}`));
681
+ return;
682
+ }
683
+ if ((code ?? 0) !== 0) {
684
+ reject(new Error(`OpenClaw command failed with exit code ${code ?? 0}`));
685
+ return;
686
+ }
687
+ resolve();
688
+ });
689
+ child.on("error", reject);
690
+ });
691
+ };
692
+ const ensureOpenClawConfigured = async (openclawBinary, config, activeApiKey) => {
693
+ if (!isNodeAtLeast(22, 14, 0)) {
694
+ throw new Error(`OpenClaw requires Node.js >=22.14.0. Current version is ${process.versions.node}.`);
695
+ }
696
+ const openclawPaths = getOpenClawPaths();
697
+ const baseEnv = buildOpenClawEnv(process.env, openclawPaths.homeDir, activeApiKey);
698
+ await fs.mkdir(openclawPaths.workspaceDir, { recursive: true });
699
+ const profileSignature = getOpenClawProfileSignature(config, activeApiKey);
700
+ const existingMarkerRaw = await readIfExists(openclawPaths.markerFile);
701
+ if (existingMarkerRaw) {
702
+ try {
703
+ const parsed = JSON.parse(existingMarkerRaw);
704
+ if (parsed.signature === profileSignature) {
705
+ return baseEnv;
706
+ }
707
+ }
708
+ catch {
709
+ // continue with reprovisioning
710
+ }
711
+ }
712
+ const providerId = getOpenClawProviderId(config);
713
+ const onboardArgs = [
714
+ "onboard",
715
+ "--non-interactive",
716
+ "--accept-risk",
717
+ "--flow",
718
+ "advanced",
719
+ "--workspace",
720
+ openclawPaths.workspaceDir,
721
+ "--auth-choice",
722
+ "custom-api-key",
723
+ "--secret-input-mode",
724
+ "ref",
725
+ "--custom-provider-id",
726
+ providerId,
727
+ "--custom-compatibility",
728
+ "openai",
729
+ "--custom-base-url",
730
+ config.provider === "openrouter"
731
+ ? "https://openrouter.ai/api/v1"
732
+ : "https://integrate.api.nvidia.com/v1",
733
+ "--custom-model-id",
734
+ getActiveModel(config),
735
+ "--no-install-daemon",
736
+ "--skip-channels",
737
+ "--skip-skills",
738
+ "--skip-health",
739
+ "--skip-ui",
740
+ ];
741
+ await runOpenClawCommand(openclawBinary, onboardArgs, baseEnv);
742
+ await runOpenClawCommand(openclawBinary, ["config", "set", "agents.defaults.model.primary", JSON.stringify(getOpenClawModelPath(config))], baseEnv);
743
+ await fs.writeFile(openclawPaths.markerFile, `${JSON.stringify({
744
+ signature: profileSignature,
745
+ provider: config.provider,
746
+ model: getActiveModel(config),
747
+ configuredAt: new Date().toISOString(),
748
+ }, null, 2)}\n`);
749
+ return baseEnv;
750
+ };
604
751
  const spawnClaudeProcess = (claudeBinary, args, env) => {
605
752
  if (isWindows && /\.(cmd|bat)$/i.test(claudeBinary)) {
606
753
  return spawn(claudeBinary, args, {
@@ -616,6 +763,24 @@ const spawnClaudeProcess = (claudeBinary, args, env) => {
616
763
  windowsHide: true,
617
764
  });
618
765
  };
766
+ const runLaunchOpenClaw = async (args) => {
767
+ const config = await ensureConfigured();
768
+ const activeApiKey = getActiveApiKey(config);
769
+ const openclawBinary = await resolveOpenClawBinary();
770
+ const env = await ensureOpenClawConfigured(openclawBinary, config, activeApiKey);
771
+ const child = spawnClaudeProcess(openclawBinary, args, env);
772
+ await new Promise((resolve, reject) => {
773
+ child.on("exit", (code, signal) => {
774
+ if (signal) {
775
+ reject(new Error(`OpenClaw exited with signal ${signal}`));
776
+ return;
777
+ }
778
+ process.exitCode = code ?? 0;
779
+ resolve();
780
+ });
781
+ child.on("error", reject);
782
+ });
783
+ };
619
784
  const runLaunchClaude = async (args) => {
620
785
  const config = await ensureConfigured();
621
786
  const routingStatus = await ensurePersistentClaudeRouting().catch(() => "skipped");
@@ -727,11 +892,15 @@ const main = async () => {
727
892
  return;
728
893
  }
729
894
  if (command === "launch") {
730
- if (rest[0] !== "claude") {
731
- throw new Error("Only `nvicode launch claude` is supported right now.");
895
+ if (rest[0] === "claude") {
896
+ await runLaunchClaude(rest.slice(1));
897
+ return;
732
898
  }
733
- await runLaunchClaude(rest.slice(1));
734
- return;
899
+ if (rest[0] === "openclaw") {
900
+ await runLaunchOpenClaw(rest.slice(1));
901
+ return;
902
+ }
903
+ throw new Error("Supported launch targets are `claude` and `openclaw`.");
735
904
  }
736
905
  throw new Error(`Unknown command: ${command}`);
737
906
  };
@@ -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.14",
4
4
  "description": "NviCode - Introducing one click Nvidia/OpenRouter keys to Claude Code. Free Claude code.",
5
5
  "author": "Dinesh Potla",
6
6
  "keywords": [