@datalabrotterdam/nova-ai-cli 1.2.0-alpha.1 → 1.2.0-alpha.2

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.
@@ -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
- };
@@ -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
@@ -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
- };
@@ -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}