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/README.md +37 -19
- package/dist/cli.js +393 -55
- package/dist/config.js +64 -12
- package/dist/models.js +30 -5
- package/dist/proxy.js +255 -116
- package/dist/usage.js +146 -0
- package/package.json +2 -2
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 {
|
|
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
|
|
19
|
-
nvicode models Show recommended
|
|
20
|
-
nvicode auth Save or update
|
|
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
|
-
|
|
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(
|
|
114
|
+
throw new Error(`Missing ${providerLabel} API key. Run \`nvicode auth\` first.`);
|
|
44
115
|
}
|
|
45
|
-
const apiKey = await question(
|
|
116
|
+
const apiKey = await question(`${providerLabel} API key: `);
|
|
46
117
|
if (!apiKey) {
|
|
47
|
-
throw new Error(
|
|
118
|
+
throw new Error(`${providerLabel} API key is required.`);
|
|
48
119
|
}
|
|
49
120
|
config = {
|
|
50
121
|
...config,
|
|
51
|
-
|
|
122
|
+
...(config.provider === "openrouter"
|
|
123
|
+
? { openrouterApiKey: apiKey }
|
|
124
|
+
: { nvidiaApiKey: apiKey }),
|
|
52
125
|
};
|
|
53
126
|
changed = true;
|
|
54
127
|
}
|
|
55
|
-
if (!
|
|
56
|
-
const [first] = await getRecommendedModels(config.
|
|
128
|
+
if (!activeModel) {
|
|
129
|
+
const [first] = await getRecommendedModels(config.provider, getActiveApiKey(config));
|
|
57
130
|
config = {
|
|
58
131
|
...config,
|
|
59
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
155
|
+
throw new Error(`${providerLabel} API key is required.`);
|
|
77
156
|
}
|
|
78
157
|
await saveConfig({
|
|
79
158
|
...config,
|
|
80
|
-
|
|
159
|
+
...(config.provider === "openrouter"
|
|
160
|
+
? { openrouterApiKey: apiKey }
|
|
161
|
+
: { nvidiaApiKey: apiKey }),
|
|
81
162
|
});
|
|
82
|
-
console.log(
|
|
163
|
+
console.log(`Saved ${providerLabel} API key.`);
|
|
83
164
|
};
|
|
84
|
-
const printModels = async (apiKey) => {
|
|
85
|
-
const 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
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
await
|
|
97
|
-
|
|
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
|
-
...
|
|
108
|
-
|
|
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(`
|
|
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(`
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
213
|
-
ANTHROPIC_AUTH_TOKEN:
|
|
518
|
+
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
|
|
519
|
+
ANTHROPIC_AUTH_TOKEN: activeApiKey,
|
|
214
520
|
ANTHROPIC_API_KEY: "",
|
|
215
|
-
ANTHROPIC_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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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.
|
|
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();
|