@datalabrotterdam/nova-ai-cli 1.2.0-alpha.1 → 1.2.0-alpha.10
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/CHANGELOG.md +165 -0
- package/README.md +74 -32
- package/dist/browser-setup-7SWHUQEL.js +57 -0
- package/dist/chunk-FMHWW3E6.js +11 -0
- package/dist/{chunk-ZKRZIF3S.js → chunk-KDAUUQBE.js} +49 -360
- package/dist/chunk-L77KQRGZ.js +170 -0
- package/dist/{headless-ZYUSTZDL.js → headless-BQXK7TNC.js} +29 -46
- package/dist/index.js +57 -19
- package/dist/{tui-XOKXPXN2.js → tui-TJFV6NC6.js} +719 -335
- package/dist/update-check-G3PYCRWM.js +23 -0
- package/package.json +25 -39
- package/dist/acp-ZYOQ6PON.js +0 -65
- package/dist/chunk-GZZ7PY3S.js +0 -8878
- package/dist/chunk-JAEXUFAS.js +0 -451
- package/dist/chunk-Q7WL3DXP.js +0 -19
- package/dist/index.d.ts +0 -1
- package/dist/login-6U7QS7EA.js +0 -118
- package/dist/webui-L4GGMXJ3.js +0 -9
- package/src/acp/web/dist/assets/index-CSccI7lJ.css +0 -1
- package/src/acp/web/dist/assets/index-DlCz3fmq.js +0 -1
- package/src/acp/web/dist/favicon.svg +0 -46
- package/src/acp/web/dist/index.html +0 -32
package/dist/chunk-JAEXUFAS.js
DELETED
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
// src/core/nova-home.ts
|
|
2
|
-
import { createHash, randomUUID } from "crypto";
|
|
3
|
-
import {
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
realpathSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
rmSync,
|
|
9
|
-
writeFileSync
|
|
10
|
-
} from "fs";
|
|
11
|
-
import { homedir } from "os";
|
|
12
|
-
import { basename, dirname, join, posix, resolve, win32 } from "path";
|
|
13
|
-
var DIR_MODE = 448;
|
|
14
|
-
var FILE_MODE = 384;
|
|
15
|
-
var SLUG_MAX_LENGTH = 40;
|
|
16
|
-
function novaHomeRoot() {
|
|
17
|
-
return process.env.NOVA_AI_HOME?.trim() || join(homedir(), ".nova-ai");
|
|
18
|
-
}
|
|
19
|
-
function projectsDir() {
|
|
20
|
-
return join(novaHomeRoot(), "projects");
|
|
21
|
-
}
|
|
22
|
-
function projectPaths(cwd) {
|
|
23
|
-
const key = workspaceKey(cwd);
|
|
24
|
-
const dir2 = join(projectsDir(), key);
|
|
25
|
-
return {
|
|
26
|
-
key,
|
|
27
|
-
dir: dir2,
|
|
28
|
-
info: join(dir2, "project.json"),
|
|
29
|
-
settings: join(dir2, "settings.json"),
|
|
30
|
-
cliSessions: join(dir2, "cli-sessions")
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function ensureProject(cwd) {
|
|
34
|
-
const paths = projectPaths(cwd);
|
|
35
|
-
mkdirSync(paths.dir, { recursive: true, mode: DIR_MODE });
|
|
36
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
37
|
-
const workspacePath = resolve(cwd);
|
|
38
|
-
let info = {
|
|
39
|
-
path: workspacePath,
|
|
40
|
-
name: basename(workspacePath),
|
|
41
|
-
createdAt: now,
|
|
42
|
-
lastOpenedAt: now
|
|
43
|
-
};
|
|
44
|
-
try {
|
|
45
|
-
const existing = JSON.parse(readFileSync(paths.info, "utf8"));
|
|
46
|
-
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
47
|
-
info = { ...existing, path: workspacePath, lastOpenedAt: now };
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
writeFileSync(paths.info, `${JSON.stringify(info, null, 2)}
|
|
53
|
-
`, {
|
|
54
|
-
mode: FILE_MODE
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
58
|
-
return paths;
|
|
59
|
-
}
|
|
60
|
-
function workspaceKey(cwd, platform = process.platform) {
|
|
61
|
-
const slug = slugify(basename(resolve(cwd))) || "workspace";
|
|
62
|
-
const hash = createHash("sha256").update(normalizeWorkspacePath(cwd, platform)).digest("hex").slice(0, 8);
|
|
63
|
-
return `${slug}-${hash}`;
|
|
64
|
-
}
|
|
65
|
-
function normalizeWorkspacePath(cwd, platform = process.platform) {
|
|
66
|
-
let resolved = resolve(cwd);
|
|
67
|
-
try {
|
|
68
|
-
resolved = realpathSync(resolved);
|
|
69
|
-
} catch {
|
|
70
|
-
}
|
|
71
|
-
const pathApi = platform === "win32" ? win32 : posix;
|
|
72
|
-
let normalized = pathApi.normalize(resolved);
|
|
73
|
-
while (normalized.length > 1 && /[\\/]$/.test(normalized) && !/^[a-zA-Z]:\\$/.test(normalized)) {
|
|
74
|
-
normalized = normalized.slice(0, -1);
|
|
75
|
-
}
|
|
76
|
-
return platform === "win32" || platform === "darwin" ? normalized.toLowerCase() : normalized;
|
|
77
|
-
}
|
|
78
|
-
function slugify(name) {
|
|
79
|
-
return name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, SLUG_MAX_LENGTH).replace(/[-.]+$/g, "");
|
|
80
|
-
}
|
|
81
|
-
function isWorkspaceTrusted(cwd) {
|
|
82
|
-
try {
|
|
83
|
-
const info = JSON.parse(readFileSync(projectPaths(cwd).info, "utf8"));
|
|
84
|
-
return info?.trusted === true;
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function setWorkspaceTrusted(cwd, trusted) {
|
|
90
|
-
const paths = ensureProject(cwd);
|
|
91
|
-
const info = JSON.parse(readFileSync(paths.info, "utf8"));
|
|
92
|
-
writePrivateFile(paths.info, `${JSON.stringify({ ...info, trusted }, null, 2)}
|
|
93
|
-
`);
|
|
94
|
-
}
|
|
95
|
-
function writePrivateFile(path, content) {
|
|
96
|
-
const dir2 = dirname(path);
|
|
97
|
-
mkdirSync(dir2, { recursive: true, mode: DIR_MODE });
|
|
98
|
-
const temp = join(dir2, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
99
|
-
try {
|
|
100
|
-
writeFileSync(temp, content, { encoding: "utf8", flag: "wx", mode: FILE_MODE });
|
|
101
|
-
renameSync(temp, path);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
rmSync(temp, { force: true });
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/core/credentials.ts
|
|
109
|
-
import {
|
|
110
|
-
chmodSync,
|
|
111
|
-
mkdirSync as mkdirSync2,
|
|
112
|
-
readFileSync as readFileSync2,
|
|
113
|
-
renameSync as renameSync2,
|
|
114
|
-
rmSync as rmSync2,
|
|
115
|
-
writeFileSync as writeFileSync2
|
|
116
|
-
} from "fs";
|
|
117
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
118
|
-
var DIR_MODE2 = 448;
|
|
119
|
-
var FILE_MODE2 = 384;
|
|
120
|
-
function credentialsPath() {
|
|
121
|
-
return join2(novaHomeRoot(), "credentials.json");
|
|
122
|
-
}
|
|
123
|
-
function readCredentials() {
|
|
124
|
-
const envApiKey = process.env.NOVA_API_KEY;
|
|
125
|
-
if (envApiKey) {
|
|
126
|
-
return { apiKey: envApiKey, defaultModel: process.env.NOVA_MODEL };
|
|
127
|
-
}
|
|
128
|
-
return readStoredCredentials();
|
|
129
|
-
}
|
|
130
|
-
function readStoredCredentials() {
|
|
131
|
-
let parsed;
|
|
132
|
-
try {
|
|
133
|
-
parsed = JSON.parse(readFileSync2(credentialsPath(), "utf8"));
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
const { apiKey, defaultModel } = parsed;
|
|
141
|
-
if (typeof apiKey !== "string" || !apiKey.trim()) return null;
|
|
142
|
-
return {
|
|
143
|
-
apiKey,
|
|
144
|
-
defaultModel: typeof defaultModel === "string" && defaultModel ? defaultModel : void 0
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
function writeCredentials(credentials) {
|
|
148
|
-
const path = credentialsPath();
|
|
149
|
-
const dir2 = dirname2(path);
|
|
150
|
-
mkdirSync2(dir2, { recursive: true, mode: DIR_MODE2 });
|
|
151
|
-
chmodSync(dir2, DIR_MODE2);
|
|
152
|
-
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
153
|
-
try {
|
|
154
|
-
writeFileSync2(temp, `${JSON.stringify(credentials, null, 2)}
|
|
155
|
-
`, {
|
|
156
|
-
mode: FILE_MODE2
|
|
157
|
-
});
|
|
158
|
-
chmodSync(temp, FILE_MODE2);
|
|
159
|
-
renameSync2(temp, path);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
rmSync2(temp, { force: true });
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
chmodSync(path, FILE_MODE2);
|
|
165
|
-
}
|
|
166
|
-
function saveDefaultModel(model) {
|
|
167
|
-
const stored = readStoredCredentials();
|
|
168
|
-
if (!stored) return false;
|
|
169
|
-
writeCredentials({ ...stored, defaultModel: model });
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
function deleteCredentials() {
|
|
173
|
-
try {
|
|
174
|
-
rmSync2(credentialsPath());
|
|
175
|
-
return true;
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (error.code === "ENOENT") return false;
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/acp/auth-server.ts
|
|
183
|
-
import { spawn } from "child_process";
|
|
184
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
185
|
-
import { existsSync } from "fs";
|
|
186
|
-
import { dirname as dirname3, join as join3 } from "path";
|
|
187
|
-
import { fileURLToPath } from "url";
|
|
188
|
-
import { NovaAIError } from "@datalabrotterdam/nova-sdk";
|
|
189
|
-
|
|
190
|
-
// src/core/model-capabilities.ts
|
|
191
|
-
var IMAGE_INPUT_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
192
|
-
"image",
|
|
193
|
-
"images",
|
|
194
|
-
"image_input",
|
|
195
|
-
"input_image",
|
|
196
|
-
"vision",
|
|
197
|
-
"multimodal",
|
|
198
|
-
"multimodal_input"
|
|
199
|
-
]);
|
|
200
|
-
var TOOL_CALL_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
201
|
-
"function_calling",
|
|
202
|
-
"functions",
|
|
203
|
-
"tools",
|
|
204
|
-
"tool_calling",
|
|
205
|
-
"tool_calls",
|
|
206
|
-
"tool_use"
|
|
207
|
-
]);
|
|
208
|
-
var TEXT_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
209
|
-
"text",
|
|
210
|
-
"chat",
|
|
211
|
-
"completion",
|
|
212
|
-
"completions",
|
|
213
|
-
"chat_completion",
|
|
214
|
-
"chat_completions",
|
|
215
|
-
...TOOL_CALL_CAPABILITIES
|
|
216
|
-
]);
|
|
217
|
-
function modelSupportsToolCalls(model) {
|
|
218
|
-
return (model?.capabilities ?? []).some(
|
|
219
|
-
(capability) => TOOL_CALL_CAPABILITIES.has(normalizeCapability(capability))
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
function isChatModel(model) {
|
|
223
|
-
const capabilities = model?.capabilities;
|
|
224
|
-
if (!Array.isArray(capabilities) || capabilities.length === 0) return true;
|
|
225
|
-
return capabilities.some(
|
|
226
|
-
(capability) => TEXT_CAPABILITIES.has(normalizeCapability(capability))
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
function chatModels(models) {
|
|
230
|
-
return models.filter((model) => model.enabled !== false && isChatModel(model));
|
|
231
|
-
}
|
|
232
|
-
function chooseDefaultModel(models) {
|
|
233
|
-
const usable = chatModels(models);
|
|
234
|
-
return (usable.find(modelSupportsToolCalls) ?? usable[0])?.id;
|
|
235
|
-
}
|
|
236
|
-
function resolveDefaultModel(models, preferred) {
|
|
237
|
-
if (preferred) {
|
|
238
|
-
const known = findModelMetadata(models, preferred);
|
|
239
|
-
if (!known || known.enabled !== false && isChatModel(known)) return preferred;
|
|
240
|
-
}
|
|
241
|
-
return chooseDefaultModel(models) ?? preferred ?? void 0;
|
|
242
|
-
}
|
|
243
|
-
function modelSupportsImageInput(model) {
|
|
244
|
-
return (model?.capabilities ?? []).some(
|
|
245
|
-
(capability) => IMAGE_INPUT_CAPABILITIES.has(normalizeCapability(capability))
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
function findModelMetadata(models, modelId) {
|
|
249
|
-
return models.find(
|
|
250
|
-
(model) => model.id === modelId || model.aliases?.includes(modelId)
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
async function resolveModelMetadata(client, modelId) {
|
|
254
|
-
let after;
|
|
255
|
-
do {
|
|
256
|
-
const response = await client.models.list({
|
|
257
|
-
limit: 100,
|
|
258
|
-
...after ? { after } : {}
|
|
259
|
-
});
|
|
260
|
-
const model = findModelMetadata(response.data, modelId);
|
|
261
|
-
if (model) return model;
|
|
262
|
-
if (!response.has_more || !response.last_id || response.last_id === after) {
|
|
263
|
-
return void 0;
|
|
264
|
-
}
|
|
265
|
-
after = response.last_id;
|
|
266
|
-
} while (true);
|
|
267
|
-
}
|
|
268
|
-
async function resolveModelSupportsImageInput(client, modelId) {
|
|
269
|
-
return modelSupportsImageInput(await resolveModelMetadata(client, modelId));
|
|
270
|
-
}
|
|
271
|
-
function normalizeCapability(value) {
|
|
272
|
-
return value.trim().toLowerCase().replace(/[\s:-]+/g, "_");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// src/core/nova-client.ts
|
|
276
|
-
import { NovaAI } from "@datalabrotterdam/nova-sdk";
|
|
277
|
-
function createNovaClient(apiKey) {
|
|
278
|
-
const baseUrl = process.env.NOVA_BASE_URL?.trim();
|
|
279
|
-
return new NovaAI({ apiKey, ...baseUrl ? { baseUrl } : {} });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// src/acp/auth-server.ts
|
|
283
|
-
import { dir, json, WebServer } from "@sourceregistry/node-webserver";
|
|
284
|
-
var AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
285
|
-
var HOST = "127.0.0.1";
|
|
286
|
-
var SETUP_TOKEN_HEADER = "x-nova-setup-token";
|
|
287
|
-
function resolveUiDir() {
|
|
288
|
-
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
289
|
-
const candidates = [
|
|
290
|
-
// Running from src/acp/auth-server.ts via tsx.
|
|
291
|
-
join3(moduleDir, "web", "dist"),
|
|
292
|
-
// Running from the bundled package entry in dist/index.js.
|
|
293
|
-
join3(moduleDir, "..", "src", "acp", "web", "dist")
|
|
294
|
-
];
|
|
295
|
-
const uiDir = candidates.find((candidate) => existsSync(candidate));
|
|
296
|
-
if (!uiDir) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`Nova AI setup UI is missing. Expected it at one of: ${candidates.join(", ")}. Run npm run build before using --setup from a source checkout.`
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
return uiDir;
|
|
302
|
-
}
|
|
303
|
-
function openBrowser(url) {
|
|
304
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
305
|
-
try {
|
|
306
|
-
spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => {
|
|
307
|
-
}).unref();
|
|
308
|
-
} catch {
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
async function validateApiKey(apiKey) {
|
|
312
|
-
const client = createNovaClient(apiKey);
|
|
313
|
-
const { data: models } = await client.models.list({ limit: 100 });
|
|
314
|
-
return { apiKey, defaultModel: chooseDefaultModel(models) };
|
|
315
|
-
}
|
|
316
|
-
function startAuthServer(options = {}) {
|
|
317
|
-
const uiDir = options.uiDir ?? resolveUiDir();
|
|
318
|
-
const validate = options.validateApiKey ?? validateApiKey;
|
|
319
|
-
const store = options.writeCredentials ?? writeCredentials;
|
|
320
|
-
const token = randomBytes(32).toString("base64url");
|
|
321
|
-
const expectedToken = Buffer.from(token);
|
|
322
|
-
let resolveDone;
|
|
323
|
-
let rejectDone;
|
|
324
|
-
const done = new Promise((resolve2, reject) => {
|
|
325
|
-
resolveDone = resolve2;
|
|
326
|
-
rejectDone = reject;
|
|
327
|
-
});
|
|
328
|
-
done.catch(() => {
|
|
329
|
-
});
|
|
330
|
-
const app = new WebServer();
|
|
331
|
-
let origin = "";
|
|
332
|
-
let settled = false;
|
|
333
|
-
let timeout;
|
|
334
|
-
const settle = (error) => {
|
|
335
|
-
if (settled) return;
|
|
336
|
-
settled = true;
|
|
337
|
-
clearTimeout(timeout);
|
|
338
|
-
const shutdown = app.shutdown().catch(() => {
|
|
339
|
-
});
|
|
340
|
-
if (error) void shutdown.then(() => rejectDone(error));
|
|
341
|
-
else void shutdown.then(resolveDone);
|
|
342
|
-
};
|
|
343
|
-
app.POST("/api/authenticate", async (event) => {
|
|
344
|
-
const rejection = rejectUntrustedRequest(
|
|
345
|
-
event.request,
|
|
346
|
-
origin,
|
|
347
|
-
expectedToken
|
|
348
|
-
);
|
|
349
|
-
if (rejection) return rejection;
|
|
350
|
-
if (settled) {
|
|
351
|
-
return json({ message: "Setup already finished." }, { status: 409 });
|
|
352
|
-
}
|
|
353
|
-
const body = await event.request.json().catch(() => null);
|
|
354
|
-
const apiKey = typeof body?.apiKey === "string" ? body.apiKey.trim() : "";
|
|
355
|
-
if (!apiKey) {
|
|
356
|
-
return json({ message: "API key is required." }, { status: 400 });
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
store(await validate(apiKey));
|
|
360
|
-
} catch (err) {
|
|
361
|
-
const message = err instanceof NovaAIError ? `Nova AI rejected this key (status ${err.status}): ${err.message}` : "Unexpected error validating the key.";
|
|
362
|
-
return json({ message }, { status: 401 });
|
|
363
|
-
}
|
|
364
|
-
setImmediate(() => settle());
|
|
365
|
-
return json({ ok: true });
|
|
366
|
-
});
|
|
367
|
-
app.GET("/", dir(uiDir, { spa: true }));
|
|
368
|
-
app.GET("/[...path]", dir(uiDir, { spa: true }));
|
|
369
|
-
return new Promise((resolve2, reject) => {
|
|
370
|
-
app.listen(0, HOST, () => {
|
|
371
|
-
const address = app.address();
|
|
372
|
-
if (typeof address !== "object" || !address) {
|
|
373
|
-
void app.shutdown().catch(() => {
|
|
374
|
-
});
|
|
375
|
-
reject(new Error("Nova AI setup server failed to bind."));
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
origin = `http://${HOST}:${address.port}`;
|
|
379
|
-
timeout = setTimeout(
|
|
380
|
-
() => settle(
|
|
381
|
-
new Error("Nova AI authentication timed out after 5 minutes.")
|
|
382
|
-
),
|
|
383
|
-
options.timeoutMs ?? AUTH_TIMEOUT_MS
|
|
384
|
-
);
|
|
385
|
-
resolve2({
|
|
386
|
-
url: `${origin}/#token=${token}`,
|
|
387
|
-
origin,
|
|
388
|
-
token,
|
|
389
|
-
done,
|
|
390
|
-
close: async () => {
|
|
391
|
-
settle(new Error("Nova AI authentication was cancelled."));
|
|
392
|
-
await done.catch(() => {
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
function rejectUntrustedRequest(request, origin, expectedToken) {
|
|
400
|
-
const forbidden = () => json(
|
|
401
|
-
{
|
|
402
|
-
message: "Request not allowed. Open the setup link printed by nova-ai-cli."
|
|
403
|
-
},
|
|
404
|
-
{ status: 403 }
|
|
405
|
-
);
|
|
406
|
-
if (!origin || request.headers.get("host") !== new URL(origin).host) {
|
|
407
|
-
return forbidden();
|
|
408
|
-
}
|
|
409
|
-
const requestOrigin = request.headers.get("origin");
|
|
410
|
-
if (requestOrigin !== null && requestOrigin !== origin) return forbidden();
|
|
411
|
-
const contentType = request.headers.get("content-type") ?? "";
|
|
412
|
-
if (contentType.split(";")[0].trim().toLowerCase() !== "application/json") {
|
|
413
|
-
return json(
|
|
414
|
-
{ message: "Content-Type must be application/json." },
|
|
415
|
-
{ status: 415 }
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
const provided = Buffer.from(request.headers.get(SETUP_TOKEN_HEADER) ?? "");
|
|
419
|
-
if (provided.length !== expectedToken.length || !timingSafeEqual(provided, expectedToken)) {
|
|
420
|
-
return forbidden();
|
|
421
|
-
}
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
async function runBrowserAuth() {
|
|
425
|
-
const server = await startAuthServer();
|
|
426
|
-
console.error(`Opening browser to authenticate with Nova AI: ${server.url}`);
|
|
427
|
-
openBrowser(server.url);
|
|
428
|
-
await server.done;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export {
|
|
432
|
-
createNovaClient,
|
|
433
|
-
novaHomeRoot,
|
|
434
|
-
projectsDir,
|
|
435
|
-
projectPaths,
|
|
436
|
-
ensureProject,
|
|
437
|
-
isWorkspaceTrusted,
|
|
438
|
-
setWorkspaceTrusted,
|
|
439
|
-
writePrivateFile,
|
|
440
|
-
chatModels,
|
|
441
|
-
resolveDefaultModel,
|
|
442
|
-
resolveModelMetadata,
|
|
443
|
-
resolveModelSupportsImageInput,
|
|
444
|
-
credentialsPath,
|
|
445
|
-
readCredentials,
|
|
446
|
-
writeCredentials,
|
|
447
|
-
saveDefaultModel,
|
|
448
|
-
deleteCredentials,
|
|
449
|
-
validateApiKey,
|
|
450
|
-
runBrowserAuth
|
|
451
|
-
};
|
package/dist/chunk-Q7WL3DXP.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/core/version.ts
|
|
2
|
-
import { createRequire } from "module";
|
|
3
|
-
function readPackageVersion() {
|
|
4
|
-
const require2 = createRequire(import.meta.url);
|
|
5
|
-
for (const path of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
6
|
-
try {
|
|
7
|
-
const metadata = require2(path);
|
|
8
|
-
if (metadata && typeof metadata === "object" && metadata.name === "@datalabrotterdam/nova-ai-cli" && typeof metadata.version === "string") {
|
|
9
|
-
return metadata.version;
|
|
10
|
-
}
|
|
11
|
-
} catch {
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return process.env.npm_package_version ?? null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
readPackageVersion
|
|
19
|
-
};
|
package/dist/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
package/dist/login-6U7QS7EA.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
credentialsPath,
|
|
3
|
-
deleteCredentials,
|
|
4
|
-
runBrowserAuth,
|
|
5
|
-
validateApiKey,
|
|
6
|
-
writeCredentials
|
|
7
|
-
} from "./chunk-JAEXUFAS.js";
|
|
8
|
-
|
|
9
|
-
// src/commands/login.ts
|
|
10
|
-
import { NovaAIError } from "@datalabrotterdam/nova-sdk";
|
|
11
|
-
var defaultIo = () => ({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr });
|
|
12
|
-
var LOGIN_HELP = `Usage: nova-ai login [--no-browser]
|
|
13
|
-
|
|
14
|
-
Connects nova-ai to your Nova AI account and stores the API key in
|
|
15
|
-
${"~"}/.nova-ai/credentials.json (readable only by you).
|
|
16
|
-
|
|
17
|
-
(default) Opens a local page in your browser to enter the key
|
|
18
|
-
--no-browser Asks for the key in this terminal (input is hidden)
|
|
19
|
-
piped stdin Reads the key from stdin, e.g. echo "$KEY" | nova-ai login
|
|
20
|
-
|
|
21
|
-
Get a key at https://platform.nova.datalabrotterdam.nl/dashboard/api-keys`;
|
|
22
|
-
async function runLogin(args, io = defaultIo()) {
|
|
23
|
-
const unknown = args.filter((arg) => arg !== "--no-browser" && arg !== "--help" && arg !== "-h");
|
|
24
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
25
|
-
io.stdout.write(`${LOGIN_HELP}
|
|
26
|
-
`);
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
if (unknown.length) {
|
|
30
|
-
io.stderr.write(`Unknown option: ${unknown[0]}
|
|
31
|
-
|
|
32
|
-
${LOGIN_HELP}
|
|
33
|
-
`);
|
|
34
|
-
return 2;
|
|
35
|
-
}
|
|
36
|
-
if (!io.stdin.isTTY) {
|
|
37
|
-
return storeKey((await readAll(io.stdin)).trim(), io);
|
|
38
|
-
}
|
|
39
|
-
if (args.includes("--no-browser")) {
|
|
40
|
-
return storeKey((await readHidden(io, "Nova API key: ")).trim(), io);
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
await runBrowserAuth();
|
|
44
|
-
io.stdout.write(`Logged in. The key is stored in ${credentialsPath()}.
|
|
45
|
-
`);
|
|
46
|
-
return 0;
|
|
47
|
-
} catch (error) {
|
|
48
|
-
io.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
49
|
-
`);
|
|
50
|
-
io.stderr.write("Tip: run `nova-ai login --no-browser` to enter the key in this terminal.\n");
|
|
51
|
-
return 1;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function runLogout(io = defaultIo()) {
|
|
55
|
-
const removed = deleteCredentials();
|
|
56
|
-
io.stdout.write(removed ? "Logged out: the stored API key was removed.\n" : "No stored API key to remove.\n");
|
|
57
|
-
if (process.env.NOVA_API_KEY) {
|
|
58
|
-
io.stdout.write("Note: NOVA_API_KEY is set in your environment and still logs you in.\n");
|
|
59
|
-
}
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
async function storeKey(apiKey, io) {
|
|
63
|
-
if (!apiKey) {
|
|
64
|
-
io.stderr.write("No API key given.\n");
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
writeCredentials(await validateApiKey(apiKey));
|
|
69
|
-
} catch (error) {
|
|
70
|
-
io.stderr.write(
|
|
71
|
-
error instanceof NovaAIError ? `Nova AI rejected this key (status ${error.status}): ${error.message}
|
|
72
|
-
` : `Could not validate the key: ${error instanceof Error ? error.message : String(error)}
|
|
73
|
-
`
|
|
74
|
-
);
|
|
75
|
-
return 1;
|
|
76
|
-
}
|
|
77
|
-
io.stdout.write(`Logged in. The key is stored in ${credentialsPath()}.
|
|
78
|
-
`);
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
async function readAll(stream) {
|
|
82
|
-
const chunks = [];
|
|
83
|
-
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
|
84
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
85
|
-
}
|
|
86
|
-
function readHidden(io, prompt) {
|
|
87
|
-
return new Promise((resolve, reject) => {
|
|
88
|
-
const { stdin } = io;
|
|
89
|
-
io.stdout.write(prompt);
|
|
90
|
-
let value = "";
|
|
91
|
-
const wasRaw = stdin.isRaw;
|
|
92
|
-
stdin.setRawMode(true);
|
|
93
|
-
stdin.resume();
|
|
94
|
-
stdin.setEncoding("utf8");
|
|
95
|
-
const finish = (error) => {
|
|
96
|
-
stdin.setRawMode(wasRaw);
|
|
97
|
-
stdin.pause();
|
|
98
|
-
stdin.removeListener("data", onData);
|
|
99
|
-
io.stdout.write("\n");
|
|
100
|
-
if (error) reject(error);
|
|
101
|
-
else resolve(value);
|
|
102
|
-
};
|
|
103
|
-
const onData = (chunk) => {
|
|
104
|
-
for (const char of chunk) {
|
|
105
|
-
if (char === "\r" || char === "\n") return finish();
|
|
106
|
-
if (char === "") return finish(new Error("Login cancelled."));
|
|
107
|
-
if (char === "\x7F" || char === "\b") value = value.slice(0, -1);
|
|
108
|
-
else if (char >= " ") value += char;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
stdin.on("data", onData);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
export {
|
|
115
|
-
LOGIN_HELP,
|
|
116
|
-
runLogin,
|
|
117
|
-
runLogout
|
|
118
|
-
};
|
package/dist/webui-L4GGMXJ3.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// src/webui/index.ts
|
|
2
|
-
async function runWebUi() {
|
|
3
|
-
console.log("nova-ai --web is a work in progress \u2014 the browser UI isn't built yet.");
|
|
4
|
-
console.log("Use `nova-ai` for the terminal UI, `nova-ai -p` headless, or `nova-ai --acp` for editors.");
|
|
5
|
-
}
|
|
6
|
-
var webui_default = runWebUi;
|
|
7
|
-
export {
|
|
8
|
-
webui_default as default
|
|
9
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[data-theme=dark]{--color-primary:#63e;--color-success:#4ade80;--color-danger:#ef4444;--color-text-primary:#e4e6eb;--color-text-secondary:#b0b3b8;--color-border-primary:#424242;--color-background-page:linear-gradient(150deg, #000 0%, #0b0618 100%);--color-card-bg:#121212d1;--color-card-border:#ffffff0f;--color-input-bg:#1a1a1e}[data-theme=light]{--color-primary:#63e;--color-success:#16a34a;--color-danger:#dc2626;--color-text-primary:#212529;--color-text-secondary:#5b5b5b;--color-border-primary:#e0e0df;--color-background-page:linear-gradient(150deg, #f0f0f0 0%, #ede8ff 100%);--color-card-bg:#ffffffc7;--color-card-border:#fff9;--color-input-bg:#fff}html,body{min-height:100dvh;margin:0}body{color:var(--color-text-primary);background:var(--color-background-page);font-family:Poppins,system-ui,-apple-system,sans-serif;transition:background .2s,color .2s}main.svelte-1n46o8q{justify-content:center;align-items:center;min-height:100dvh;padding:1.5rem;display:flex;position:relative}.theme-toggle.svelte-1n46o8q{border:1px solid var(--color-border-primary);background:var(--color-card-bg);width:36px;height:36px;color:var(--color-text-primary);cursor:pointer;border-radius:50%;justify-content:center;align-items:center;transition:all .15s;display:flex;position:absolute;top:1.25rem;right:1.25rem}.theme-toggle.svelte-1n46o8q:hover{opacity:.8}.theme-toggle.svelte-1n46o8q svg:where(.svelte-1n46o8q){width:16px;height:16px}.card.svelte-1n46o8q{background:var(--color-card-bg);border:1px solid var(--color-card-border);text-align:center;border-radius:1rem;width:100%;max-width:380px;padding:2.5rem 2rem;box-shadow:0 10px 20px #0003}.logo.svelte-1n46o8q{width:48px;height:48px;color:var(--color-text-primary);margin:0 auto 1.25rem}.logo.svelte-1n46o8q svg:where(.svelte-1n46o8q){width:100%;height:100%}h1.svelte-1n46o8q{margin-bottom:.5rem;font-size:1.25rem;font-weight:600}.subtitle.svelte-1n46o8q{color:var(--color-text-secondary);margin-bottom:1.5rem;font-size:.875rem}form.svelte-1n46o8q{flex-direction:column;gap:.75rem;display:flex}input.svelte-1n46o8q{box-sizing:border-box;border:1px solid var(--color-border-primary);background:var(--color-input-bg);width:100%;color:inherit;border-radius:.75rem;padding:.65rem .85rem;font-family:inherit;font-size:.925rem;transition:all .15s}input.svelte-1n46o8q:focus{border-color:var(--color-primary);outline:none}button[type=submit].svelte-1n46o8q{background:var(--color-primary);color:#fff;cursor:pointer;border:none;border-radius:.75rem;padding:.65rem 1rem;font-family:inherit;font-size:.925rem;font-weight:500;transition:all .15s}button[type=submit].svelte-1n46o8q:hover:not(:disabled){opacity:.85}button[type=submit].svelte-1n46o8q:disabled{opacity:.4;cursor:not-allowed}.success.svelte-1n46o8q{color:var(--color-success);font-size:.925rem}.error.svelte-1n46o8q{color:var(--color-danger);margin-top:.75rem;font-size:.875rem}.help-links.svelte-1n46o8q{border-top:1px solid var(--color-border-primary);flex-direction:column;gap:.5rem;margin-top:1.5rem;padding-top:1.25rem;display:flex}.help-button.svelte-1n46o8q{border:1px solid var(--color-primary);color:var(--color-primary);border-radius:.75rem;justify-content:center;align-items:center;padding:.5rem .85rem;font-size:.85rem;font-weight:500;text-decoration:none;transition:all .15s;display:inline-flex}.help-button.svelte-1n46o8q:hover{background:var(--color-primary);color:#fff}.docs-link.svelte-1n46o8q{color:var(--color-text-secondary);font-size:.8rem;text-decoration:underline}
|