@datalabrotterdam/nova-ai-cli 1.1.0 → 1.2.0-alpha.1
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 +165 -96
- package/dist/acp-ZYOQ6PON.js +65 -0
- package/dist/chunk-GZZ7PY3S.js +8878 -0
- package/dist/chunk-JAEXUFAS.js +451 -0
- package/dist/chunk-Q7WL3DXP.js +19 -0
- package/dist/chunk-ZKRZIF3S.js +954 -0
- package/dist/headless-ZYUSTZDL.js +567 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +155 -17
- package/dist/login-6U7QS7EA.js +118 -0
- package/dist/tui-XOKXPXN2.js +4801 -0
- package/dist/webui-L4GGMXJ3.js +9 -0
- package/package.json +24 -8
- package/src/acp/web/dist/assets/{index-D2Z0sfLR.js → index-DlCz3fmq.js} +1 -1
- package/src/acp/web/dist/index.html +1 -1
- package/dist/acp/agent.d.ts +0 -13
- package/dist/acp/agent.js +0 -242
- package/dist/acp/auth-server.d.ts +0 -10
- package/dist/acp/auth-server.js +0 -67
- package/dist/acp/credentials.d.ts +0 -11
- package/dist/acp/credentials.js +0 -30
- package/dist/acp/index.d.ts +0 -2
- package/dist/acp/index.js +0 -21
- package/dist/acp/mcp.d.ts +0 -11
- package/dist/acp/mcp.js +0 -90
- package/dist/acp/sessions.d.ts +0 -20
- package/dist/acp/sessions.js +0 -91
- package/dist/acp/tools/index.d.ts +0 -6
- package/dist/acp/tools/index.js +0 -5
- package/dist/acp/tools/marker.d.ts +0 -17
- package/dist/acp/tools/marker.js +0 -43
- package/dist/acp/tools/permission.d.ts +0 -2
- package/dist/acp/tools/permission.js +0 -20
- package/dist/acp/tools/read-file.d.ts +0 -2
- package/dist/acp/tools/read-file.js +0 -20
- package/dist/acp/tools/registry.d.ts +0 -4
- package/dist/acp/tools/registry.js +0 -10
- package/dist/acp/tools/reporting.d.ts +0 -3
- package/dist/acp/tools/reporting.js +0 -26
- package/dist/acp/tools/run-command.d.ts +0 -2
- package/dist/acp/tools/run-command.js +0 -29
- package/dist/acp/tools/system-prompt.d.ts +0 -2
- package/dist/acp/tools/system-prompt.js +0 -23
- package/dist/acp/tools/types.d.ts +0 -18
- package/dist/acp/tools/types.js +0 -1
- package/dist/acp/tools/write-file.d.ts +0 -2
- package/dist/acp/tools/write-file.js +0 -20
- package/dist/tui/index.d.ts +0 -7
- package/dist/tui/index.js +0 -11
- package/dist/webui/index.d.ts +0 -6
- package/dist/webui/index.js +0 -9
|
@@ -0,0 +1,451 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
};
|