nvicode 0.1.2 → 0.1.6

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
@@ -7,22 +7,40 @@ import path from "node:path";
7
7
  import process from "node:process";
8
8
  import { spawn } from "node:child_process";
9
9
  import { fileURLToPath } from "node:url";
10
- import { getNvicodePaths, loadConfig, saveConfig, } from "./config.js";
10
+ import { getActiveApiKey, getActiveModel, getNvicodePaths, loadConfig, saveConfig, } from "./config.js";
11
11
  import { createProxyServer } from "./proxy.js";
12
- import { CURATED_MODELS, getRecommendedModels } from "./models.js";
12
+ import { getRecommendedModels } from "./models.js";
13
+ import { filterRecordsSince, formatDuration, formatInteger, formatTimestamp, formatUsd, readUsageRecords, summarizeUsage, } from "./usage.js";
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const usage = () => {
15
16
  console.log(`nvicode
16
17
 
17
18
  Commands:
18
- nvicode select model Select and save a NVIDIA model
19
- nvicode models Show recommended coding models
20
- nvicode auth Save or update NVIDIA API key
19
+ nvicode select model Guided provider, key, and model selection
20
+ nvicode models Show recommended models for the active provider
21
+ nvicode auth Save or update the API key for the active provider
21
22
  nvicode config Show current nvicode config
23
+ nvicode usage Show token usage and cost comparison
24
+ nvicode activity Show recent request activity
25
+ nvicode dashboard Show usage summary and recent activity
22
26
  nvicode launch claude [...] Launch Claude Code through nvicode
23
27
  nvicode serve Run the local proxy in the foreground
24
28
  `);
25
29
  };
30
+ const isWindows = process.platform === "win32";
31
+ const getPathExts = () => {
32
+ if (!isWindows) {
33
+ return [""];
34
+ }
35
+ const raw = process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD";
36
+ return raw
37
+ .split(";")
38
+ .map((ext) => ext.trim())
39
+ .filter(Boolean)
40
+ .map((ext) => ext.toLowerCase());
41
+ };
42
+ const unique = (values) => [...new Set(values)];
43
+ const getProviderLabel = (provider) => provider === "openrouter" ? "OpenRouter" : "NVIDIA";
26
44
  const question = async (prompt) => {
27
45
  const rl = createInterface({
28
46
  input: process.stdin,
@@ -35,28 +53,85 @@ const question = async (prompt) => {
35
53
  rl.close();
36
54
  }
37
55
  };
56
+ const promptProviderSelection = async (initialProvider) => {
57
+ console.log("Choose a provider:");
58
+ console.log("1. NVIDIA");
59
+ console.log(" Uses the local nvicode proxy and usage dashboard.");
60
+ console.log("2. OpenRouter");
61
+ console.log(" Uses Claude Code direct Anthropic-compatible connection.");
62
+ const defaultChoice = initialProvider === "openrouter" ? "2" : "1";
63
+ const answer = (await question(`Provider selection [${defaultChoice}]: `)).toLowerCase();
64
+ const normalized = answer || defaultChoice;
65
+ if (normalized === "1" || normalized === "nvidia") {
66
+ return "nvidia";
67
+ }
68
+ if (normalized === "2" ||
69
+ normalized === "openrouter" ||
70
+ normalized === "open-router") {
71
+ return "openrouter";
72
+ }
73
+ throw new Error("Provider selection is required.");
74
+ };
75
+ const promptApiKeyUpdate = async (config, provider) => {
76
+ const providerLabel = getProviderLabel(provider);
77
+ const currentApiKey = provider === "openrouter" ? config.openrouterApiKey : config.nvidiaApiKey;
78
+ if (currentApiKey) {
79
+ const answer = (await question(`${providerLabel} API key already saved. Update it? [y/N]: `)).toLowerCase();
80
+ if (answer !== "y" && answer !== "yes") {
81
+ return provider === "openrouter"
82
+ ? { openrouterApiKey: currentApiKey, nvidiaApiKey: config.nvidiaApiKey }
83
+ : { nvidiaApiKey: currentApiKey, openrouterApiKey: config.openrouterApiKey };
84
+ }
85
+ const nextKey = await question(`${providerLabel} API key (press Enter or type "skip" to keep current): `);
86
+ if (!nextKey || nextKey.toLowerCase() === "skip") {
87
+ return provider === "openrouter"
88
+ ? { openrouterApiKey: currentApiKey, nvidiaApiKey: config.nvidiaApiKey }
89
+ : { nvidiaApiKey: currentApiKey, openrouterApiKey: config.openrouterApiKey };
90
+ }
91
+ return provider === "openrouter"
92
+ ? { openrouterApiKey: nextKey, nvidiaApiKey: config.nvidiaApiKey }
93
+ : { nvidiaApiKey: nextKey, openrouterApiKey: config.openrouterApiKey };
94
+ }
95
+ const nextKey = await question(`${providerLabel} API key (press Enter or type "skip" to skip): `);
96
+ if (!nextKey || nextKey.toLowerCase() === "skip") {
97
+ return {
98
+ nvidiaApiKey: config.nvidiaApiKey,
99
+ openrouterApiKey: config.openrouterApiKey,
100
+ };
101
+ }
102
+ return provider === "openrouter"
103
+ ? { openrouterApiKey: nextKey, nvidiaApiKey: config.nvidiaApiKey }
104
+ : { nvidiaApiKey: nextKey, openrouterApiKey: config.openrouterApiKey };
105
+ };
38
106
  const ensureConfigured = async () => {
39
107
  let config = await loadConfig();
40
108
  let changed = false;
41
- if (!config.apiKey) {
109
+ const providerLabel = getProviderLabel(config.provider);
110
+ const activeApiKey = getActiveApiKey(config);
111
+ const activeModel = getActiveModel(config);
112
+ if (!activeApiKey) {
42
113
  if (!process.stdin.isTTY) {
43
- throw new Error("Missing NVIDIA API key. Run `nvicode auth` first.");
114
+ throw new Error(`Missing ${providerLabel} API key. Run \`nvicode auth\` first.`);
44
115
  }
45
- const apiKey = await question("NVIDIA API key: ");
116
+ const apiKey = await question(`${providerLabel} API key: `);
46
117
  if (!apiKey) {
47
- throw new Error("NVIDIA API key is required.");
118
+ throw new Error(`${providerLabel} API key is required.`);
48
119
  }
49
120
  config = {
50
121
  ...config,
51
- apiKey,
122
+ ...(config.provider === "openrouter"
123
+ ? { openrouterApiKey: apiKey }
124
+ : { nvidiaApiKey: apiKey }),
52
125
  };
53
126
  changed = true;
54
127
  }
55
- if (!config.model) {
56
- const [first] = await getRecommendedModels(config.apiKey);
128
+ if (!activeModel) {
129
+ const [first] = await getRecommendedModels(config.provider, getActiveApiKey(config));
57
130
  config = {
58
131
  ...config,
59
- model: first?.id || CURATED_MODELS[0].id,
132
+ ...(config.provider === "openrouter"
133
+ ? { openrouterModel: first?.id || "anthropic/claude-sonnet-4.6" }
134
+ : { nvidiaModel: first?.id || "moonshotai/kimi-k2.5" }),
60
135
  };
61
136
  changed = true;
62
137
  }
@@ -67,22 +142,28 @@ const ensureConfigured = async () => {
67
142
  };
68
143
  const runAuth = async () => {
69
144
  const config = await loadConfig();
70
- const apiKey = await question(config.apiKey ? "NVIDIA API key (leave blank to keep current): " : "NVIDIA API key: ");
71
- if (!apiKey && config.apiKey) {
72
- console.log("Kept existing NVIDIA API key.");
145
+ const providerLabel = getProviderLabel(config.provider);
146
+ const currentApiKey = getActiveApiKey(config);
147
+ const apiKey = await question(currentApiKey
148
+ ? `${providerLabel} API key (leave blank to keep current): `
149
+ : `${providerLabel} API key: `);
150
+ if (!apiKey && currentApiKey) {
151
+ console.log(`Kept existing ${providerLabel} API key.`);
73
152
  return;
74
153
  }
75
154
  if (!apiKey) {
76
- throw new Error("NVIDIA API key is required.");
155
+ throw new Error(`${providerLabel} API key is required.`);
77
156
  }
78
157
  await saveConfig({
79
158
  ...config,
80
- apiKey,
159
+ ...(config.provider === "openrouter"
160
+ ? { openrouterApiKey: apiKey }
161
+ : { nvidiaApiKey: apiKey }),
81
162
  });
82
- console.log("Saved NVIDIA API key.");
163
+ console.log(`Saved ${providerLabel} API key.`);
83
164
  };
84
- const printModels = async (apiKey) => {
85
- const models = apiKey ? await getRecommendedModels(apiKey) : CURATED_MODELS;
165
+ const printModels = async (provider, apiKey) => {
166
+ const models = await getRecommendedModels(provider, apiKey || "");
86
167
  models.forEach((model, index) => {
87
168
  console.log(`${index + 1}. ${model.label}`);
88
169
  console.log(` ${model.id}`);
@@ -90,11 +171,20 @@ const printModels = async (apiKey) => {
90
171
  });
91
172
  };
92
173
  const runSelectModel = async () => {
93
- const config = await ensureConfigured();
94
- const models = await getRecommendedModels(config.apiKey);
95
- console.log("Recommended NVIDIA coding models:");
96
- await printModels(config.apiKey);
97
- console.log("Type a number from the list or enter a custom model id.");
174
+ const config = await loadConfig();
175
+ const provider = await promptProviderSelection(config.provider);
176
+ const providerLabel = getProviderLabel(provider);
177
+ const keyPatch = await promptApiKeyUpdate(config, provider);
178
+ const nextConfig = await saveConfig({
179
+ ...config,
180
+ ...keyPatch,
181
+ provider,
182
+ });
183
+ const models = await getRecommendedModels(provider, getActiveApiKey(nextConfig));
184
+ console.log(`Top popular ${providerLabel} models:`);
185
+ await printModels(provider, getActiveApiKey(nextConfig));
186
+ console.log("Or paste a full model id.");
187
+ console.log("Example: qwen/qwen3.6-plus-preview:free");
98
188
  const answer = await question("Model selection: ");
99
189
  const index = Number(answer);
100
190
  const chosenModel = Number.isInteger(index) && index >= 1 && index <= models.length
@@ -104,8 +194,10 @@ const runSelectModel = async () => {
104
194
  throw new Error("Model selection is required.");
105
195
  }
106
196
  await saveConfig({
107
- ...config,
108
- model: chosenModel,
197
+ ...nextConfig,
198
+ ...(provider === "openrouter"
199
+ ? { openrouterModel: chosenModel }
200
+ : { nvidiaModel: chosenModel }),
109
201
  });
110
202
  console.log(`Saved model: ${chosenModel}`);
111
203
  };
@@ -114,10 +206,153 @@ const runConfig = async () => {
114
206
  const paths = getNvicodePaths();
115
207
  console.log(`Config file: ${paths.configFile}`);
116
208
  console.log(`State dir: ${paths.stateDir}`);
117
- console.log(`Model: ${config.model}`);
209
+ console.log(`Usage log: ${paths.usageLogFile}`);
210
+ console.log(`Provider: ${getProviderLabel(config.provider)}`);
211
+ console.log(`Model: ${getActiveModel(config)}`);
118
212
  console.log(`Proxy port: ${config.proxyPort}`);
213
+ console.log(`Max RPM: ${config.maxRequestsPerMinute}`);
119
214
  console.log(`Thinking: ${config.thinking ? "on" : "off"}`);
120
- console.log(`API key: ${config.apiKey ? "saved" : "missing"}`);
215
+ console.log(`NVIDIA key: ${config.nvidiaApiKey ? "saved" : "missing"}`);
216
+ console.log(`OpenRouter key: ${config.openrouterApiKey ? "saved" : "missing"}`);
217
+ };
218
+ const printUsageBlock = (label, records) => {
219
+ const summary = summarizeUsage(records);
220
+ console.log(label);
221
+ console.log(`Requests: ${formatInteger(summary.requests)} (${formatInteger(summary.successes)} ok, ${formatInteger(summary.errors)} error)`);
222
+ console.log(`Turn input tokens: ${formatInteger(summary.turnInputTokens)}`);
223
+ console.log(`Billed input tokens: ${formatInteger(summary.inputTokens)}`);
224
+ console.log(`Turn output tokens: ${formatInteger(summary.turnOutputTokens)}`);
225
+ console.log(`Billed output tokens: ${formatInteger(summary.outputTokens)}`);
226
+ console.log(`NVIDIA cost: ${formatUsd(summary.providerCostUsd)}`);
227
+ console.log(`Estimated savings: ${formatUsd(summary.savingsUsd)}`);
228
+ };
229
+ const getUsageView = async () => {
230
+ const records = await readUsageRecords();
231
+ if (records.length === 0) {
232
+ return [
233
+ "nvicode usage",
234
+ "",
235
+ "No usage recorded yet.",
236
+ "Keep this open and new activity will appear automatically.",
237
+ ].join("\n");
238
+ }
239
+ const now = Date.now();
240
+ const latestPricing = records[0]?.pricing;
241
+ const lines = ["nvicode usage", ""];
242
+ if (latestPricing) {
243
+ lines.push("Pricing basis:");
244
+ lines.push(`- NVIDIA configured cost: ${formatUsd(latestPricing.providerInputUsdPerMTok)} / MTok input, ${formatUsd(latestPricing.providerOutputUsdPerMTok)} / MTok output`);
245
+ lines.push(`- ${latestPricing.compareModel}: ${formatUsd(latestPricing.compareInputUsdPerMTok)} / MTok input, ${formatUsd(latestPricing.compareOutputUsdPerMTok)} / MTok output`);
246
+ lines.push(`- Comparison source: ${latestPricing.comparePricingSource} (${latestPricing.comparePricingUpdatedAt})`);
247
+ lines.push("- In/Out columns show current-turn tokens.");
248
+ lines.push("- Billed In/Billed Out include the full Claude Code request context.");
249
+ lines.push("");
250
+ }
251
+ const windows = [
252
+ { label: "Last 1 hour", durationMs: 1 * 60 * 60 * 1000 },
253
+ { label: "Last 6 hours", durationMs: 6 * 60 * 60 * 1000 },
254
+ { label: "Last 12 hours", durationMs: 12 * 60 * 60 * 1000 },
255
+ { label: "Last 1 day", durationMs: 24 * 60 * 60 * 1000 },
256
+ { label: "Last 1 week", durationMs: 7 * 24 * 60 * 60 * 1000 },
257
+ { label: "Last 1 month", durationMs: 30 * 24 * 60 * 60 * 1000 },
258
+ ];
259
+ const rows = windows.map((window) => {
260
+ const summary = summarizeUsage(filterRecordsSince(records, now - window.durationMs));
261
+ return {
262
+ window: window.label,
263
+ requests: `${formatInteger(summary.requests)} (${formatInteger(summary.successes)} ok/${formatInteger(summary.errors)} err)`,
264
+ inputTokens: formatInteger(summary.turnInputTokens),
265
+ billedInputTokens: formatInteger(summary.inputTokens),
266
+ outputTokens: formatInteger(summary.turnOutputTokens),
267
+ billedOutputTokens: formatInteger(summary.outputTokens),
268
+ nvidiaCost: formatUsd(summary.providerCostUsd),
269
+ savings: formatUsd(summary.savingsUsd),
270
+ };
271
+ });
272
+ lines.push(`Snapshot: ${formatTimestamp(new Date(now).toISOString())}`);
273
+ lines.push("");
274
+ lines.push("Window Requests In Tok Billed In Out Tok Billed Out NVIDIA Saved");
275
+ rows.forEach((row) => {
276
+ lines.push(`${row.window.padEnd(13)} ${row.requests.padEnd(16)} ${row.inputTokens.padStart(8)} ${row.billedInputTokens.padStart(11)} ${row.outputTokens.padStart(8)} ${row.billedOutputTokens.padStart(11)} ${row.nvidiaCost.padStart(10)} ${row.savings.padStart(10)}`);
277
+ });
278
+ return lines.join("\n");
279
+ };
280
+ const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
281
+ const clearTerminal = () => {
282
+ process.stdout.write("\x1b[2J\x1b[H");
283
+ };
284
+ const runUsage = async () => {
285
+ const config = await loadConfig();
286
+ if (config.provider === "openrouter") {
287
+ console.log("OpenRouter uses a direct Claude Code connection.");
288
+ console.log("Local nvicode usage stats are only available for NVIDIA proxy sessions.");
289
+ console.log("Use the OpenRouter activity dashboard for OpenRouter usage.");
290
+ return;
291
+ }
292
+ const interactive = process.stdout.isTTY && process.stdin.isTTY;
293
+ if (!interactive) {
294
+ console.log(await getUsageView());
295
+ return;
296
+ }
297
+ let stopped = false;
298
+ const stop = () => {
299
+ stopped = true;
300
+ };
301
+ process.on("SIGINT", stop);
302
+ process.on("SIGTERM", stop);
303
+ try {
304
+ while (!stopped) {
305
+ clearTerminal();
306
+ process.stdout.write(await getUsageView());
307
+ process.stdout.write("\n\nRefreshing every 2s. Press Ctrl+C to exit.\n");
308
+ await sleep(2_000);
309
+ }
310
+ }
311
+ finally {
312
+ process.off("SIGINT", stop);
313
+ process.off("SIGTERM", stop);
314
+ }
315
+ };
316
+ const runActivity = async () => {
317
+ const config = await loadConfig();
318
+ if (config.provider === "openrouter") {
319
+ console.log("OpenRouter uses a direct Claude Code connection.");
320
+ console.log("Local nvicode activity logs are only available for NVIDIA proxy sessions.");
321
+ return;
322
+ }
323
+ const records = await readUsageRecords();
324
+ if (records.length === 0) {
325
+ console.log("No activity recorded yet.");
326
+ return;
327
+ }
328
+ console.log("Timestamp Status Model In Tok Bill In Out Tok Bill Out Latency NVIDIA Saved");
329
+ for (const record of records.slice(0, 15)) {
330
+ const model = record.model.length > 28 ? `${record.model.slice(0, 25)}...` : record.model;
331
+ const status = record.status === "success" ? "ok" : "error";
332
+ console.log(`${formatTimestamp(record.timestamp).padEnd(21)} ${status.padEnd(6)} ${model.padEnd(29)} ${formatInteger(record.turnInputTokens ?? record.visibleInputTokens ?? record.inputTokens).padStart(7)} ${formatInteger(record.inputTokens).padStart(8)} ${formatInteger(record.turnOutputTokens ?? record.visibleOutputTokens ?? record.outputTokens).padStart(8)} ${formatInteger(record.outputTokens).padStart(8)} ${formatDuration(record.latencyMs).padStart(8)} ${formatUsd(record.providerCostUsd).padStart(10)} ${formatUsd(record.savingsUsd).padStart(10)}`);
333
+ if (record.error) {
334
+ console.log(` error: ${record.error}`);
335
+ }
336
+ }
337
+ };
338
+ const runDashboard = async () => {
339
+ const config = await loadConfig();
340
+ if (config.provider === "openrouter") {
341
+ console.log("OpenRouter uses a direct Claude Code connection.");
342
+ console.log("Local nvicode dashboards are only available for NVIDIA proxy sessions.");
343
+ return;
344
+ }
345
+ const records = await readUsageRecords();
346
+ if (records.length === 0) {
347
+ console.log("No usage recorded yet.");
348
+ return;
349
+ }
350
+ const last7Days = filterRecordsSince(records, Date.now() - 7 * 24 * 60 * 60 * 1000);
351
+ printUsageBlock("Usage (7d)", last7Days);
352
+ console.log("");
353
+ console.log("Recent activity");
354
+ console.log("");
355
+ await runActivity();
121
356
  };
122
357
  const waitForHealthyProxy = async (port) => {
123
358
  for (let attempt = 0; attempt < 50; attempt += 1) {
@@ -147,6 +382,7 @@ const ensureProxyRunning = async (config) => {
147
382
  ...process.env,
148
383
  },
149
384
  stdio: ["ignore", logFd, logFd],
385
+ windowsHide: true,
150
386
  });
151
387
  child.unref();
152
388
  await fs.writeFile(paths.pidFile, `${child.pid}\n`);
@@ -156,17 +392,63 @@ const ensureProxyRunning = async (config) => {
156
392
  };
157
393
  const isExecutable = async (filePath) => {
158
394
  try {
159
- await fs.access(filePath, constants.X_OK);
395
+ await fs.access(filePath, isWindows ? constants.F_OK : constants.X_OK);
160
396
  return true;
161
397
  }
162
398
  catch {
163
399
  return false;
164
400
  }
165
401
  };
402
+ const buildExecutableCandidates = (entry, name) => {
403
+ const base = path.join(entry, name);
404
+ if (!isWindows) {
405
+ return [base];
406
+ }
407
+ if (path.extname(name)) {
408
+ return [base];
409
+ }
410
+ return unique([base, ...getPathExts().map((ext) => `${base}${ext}`)]);
411
+ };
412
+ const resolveClaudeVersionEntry = async (entryPath) => {
413
+ if (await isExecutable(entryPath)) {
414
+ return entryPath;
415
+ }
416
+ const nestedCandidates = isWindows
417
+ ? ["claude.exe", "claude.cmd", "claude.bat", "claude"]
418
+ : ["claude"];
419
+ for (const candidateName of nestedCandidates) {
420
+ const candidate = path.join(entryPath, candidateName);
421
+ if (await isExecutable(candidate)) {
422
+ return candidate;
423
+ }
424
+ }
425
+ return null;
426
+ };
166
427
  const resolveClaudeBinary = async () => {
167
- const nativeInPath = await findExecutableInPath("claude-native");
168
- if (nativeInPath) {
169
- return nativeInPath;
428
+ const nativeNames = isWindows
429
+ ? ["claude-native.exe", "claude-native.cmd", "claude-native.bat", "claude-native"]
430
+ : ["claude-native"];
431
+ for (const name of nativeNames) {
432
+ const nativeInPath = await findExecutableInPath(name);
433
+ if (nativeInPath) {
434
+ return nativeInPath;
435
+ }
436
+ }
437
+ const homeBinCandidates = isWindows
438
+ ? [
439
+ path.join(os.homedir(), ".local", "bin", "claude.exe"),
440
+ path.join(os.homedir(), ".local", "bin", "claude.cmd"),
441
+ path.join(os.homedir(), ".local", "bin", "claude.bat"),
442
+ path.join(os.homedir(), ".local", "bin", "claude"),
443
+ ]
444
+ : [
445
+ path.join(os.homedir(), ".local", "bin", "claude-native"),
446
+ path.join(os.homedir(), ".local", "bin", "claude"),
447
+ ];
448
+ for (const candidate of homeBinCandidates) {
449
+ if (await isExecutable(candidate)) {
450
+ return candidate;
451
+ }
170
452
  }
171
453
  const versionsDir = path.join(os.homedir(), ".local", "share", "claude", "versions");
172
454
  try {
@@ -176,15 +458,23 @@ const resolveClaudeBinary = async () => {
176
458
  sensitivity: "base",
177
459
  })).at(-1);
178
460
  if (latest) {
179
- return path.join(versionsDir, latest);
461
+ const resolved = await resolveClaudeVersionEntry(path.join(versionsDir, latest));
462
+ if (resolved) {
463
+ return resolved;
464
+ }
180
465
  }
181
466
  }
182
467
  catch {
183
468
  // continue
184
469
  }
185
- const claudeInPath = await findExecutableInPath("claude");
186
- if (claudeInPath) {
187
- return claudeInPath;
470
+ const cliNames = isWindows
471
+ ? ["claude.exe", "claude.cmd", "claude.bat", "claude"]
472
+ : ["claude"];
473
+ for (const name of cliNames) {
474
+ const claudeInPath = await findExecutableInPath(name);
475
+ if (claudeInPath) {
476
+ return claudeInPath;
477
+ }
188
478
  }
189
479
  throw new Error("Unable to locate Claude Code binary.");
190
480
  };
@@ -194,31 +484,64 @@ const findExecutableInPath = async (name) => {
194
484
  if (!entry) {
195
485
  continue;
196
486
  }
197
- const candidate = path.join(entry, name);
198
- if (await isExecutable(candidate)) {
199
- return candidate;
487
+ for (const candidate of buildExecutableCandidates(entry, name)) {
488
+ if (await isExecutable(candidate)) {
489
+ return candidate;
490
+ }
200
491
  }
201
492
  }
202
493
  return null;
203
494
  };
495
+ const spawnClaudeProcess = (claudeBinary, args, env) => {
496
+ if (isWindows && /\.(cmd|bat)$/i.test(claudeBinary)) {
497
+ return spawn(claudeBinary, args, {
498
+ stdio: "inherit",
499
+ env,
500
+ shell: true,
501
+ windowsHide: true,
502
+ });
503
+ }
504
+ return spawn(claudeBinary, args, {
505
+ stdio: "inherit",
506
+ env,
507
+ windowsHide: true,
508
+ });
509
+ };
204
510
  const runLaunchClaude = async (args) => {
205
511
  const config = await ensureConfigured();
206
- await ensureProxyRunning(config);
207
512
  const claudeBinary = await resolveClaudeBinary();
208
- const child = spawn(claudeBinary, args, {
209
- stdio: "inherit",
210
- env: {
513
+ const activeModel = getActiveModel(config);
514
+ const activeApiKey = getActiveApiKey(config);
515
+ const env = config.provider === "openrouter"
516
+ ? {
211
517
  ...process.env,
212
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxyPort}`,
213
- ANTHROPIC_AUTH_TOKEN: config.proxyToken,
518
+ ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
519
+ ANTHROPIC_AUTH_TOKEN: activeApiKey,
214
520
  ANTHROPIC_API_KEY: "",
215
- ANTHROPIC_MODEL: config.model,
521
+ ANTHROPIC_MODEL: activeModel,
522
+ ANTHROPIC_DEFAULT_SONNET_MODEL: activeModel,
523
+ ANTHROPIC_DEFAULT_OPUS_MODEL: activeModel,
524
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: activeModel,
525
+ CLAUDE_CODE_SUBAGENT_MODEL: activeModel,
216
526
  CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
217
- ANTHROPIC_CUSTOM_MODEL_OPTION: config.model,
218
- ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: "nvicode custom model",
219
- ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: "Claude Code via local NVIDIA gateway",
220
- },
221
- });
527
+ }
528
+ : (() => {
529
+ return {
530
+ ...process.env,
531
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.proxyPort}`,
532
+ ANTHROPIC_AUTH_TOKEN: config.proxyToken,
533
+ ANTHROPIC_API_KEY: "",
534
+ ANTHROPIC_MODEL: activeModel,
535
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
536
+ ANTHROPIC_CUSTOM_MODEL_OPTION: activeModel,
537
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: "nvicode custom model",
538
+ ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: "Claude Code via local NVIDIA gateway",
539
+ };
540
+ })();
541
+ if (config.provider === "nvidia") {
542
+ await ensureProxyRunning(config);
543
+ }
544
+ const child = spawnClaudeProcess(claudeBinary, args, env);
222
545
  await new Promise((resolve, reject) => {
223
546
  child.on("exit", (code, signal) => {
224
547
  if (signal) {
@@ -233,12 +556,15 @@ const runLaunchClaude = async (args) => {
233
556
  };
234
557
  const runServe = async () => {
235
558
  const config = await ensureConfigured();
559
+ if (config.provider !== "nvidia") {
560
+ throw new Error("`nvicode serve` is only available for the NVIDIA provider.");
561
+ }
236
562
  const server = createProxyServer(config);
237
563
  await new Promise((resolve, reject) => {
238
564
  server.once("error", reject);
239
565
  server.listen(config.proxyPort, "127.0.0.1", () => resolve());
240
566
  });
241
- console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${config.model}`);
567
+ console.error(`nvicode proxy listening on http://127.0.0.1:${config.proxyPort} using ${config.nvidiaModel}`);
242
568
  const shutdown = () => {
243
569
  server.close(() => process.exit(0));
244
570
  };
@@ -258,7 +584,7 @@ const main = async () => {
258
584
  }
259
585
  if (command === "models") {
260
586
  const config = await loadConfig();
261
- await printModels(config.apiKey || undefined);
587
+ await printModels(config.provider, getActiveApiKey(config) || undefined);
262
588
  return;
263
589
  }
264
590
  if (command === "auth") {
@@ -269,6 +595,18 @@ const main = async () => {
269
595
  await runConfig();
270
596
  return;
271
597
  }
598
+ if (command === "usage") {
599
+ await runUsage();
600
+ return;
601
+ }
602
+ if (command === "activity") {
603
+ await runActivity();
604
+ return;
605
+ }
606
+ if (command === "dashboard") {
607
+ await runDashboard();
608
+ return;
609
+ }
272
610
  if ((command === "select" && rest[0] === "model") ||
273
611
  command === "select-model") {
274
612
  await runSelectModel();