jinzd-ai-cli 0.2.0 → 0.2.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.
- package/dist/auth-TWWF22YZ.js +7 -0
- package/dist/chunk-CPLT6CD3.js +202 -0
- package/dist/{chunk-PBJ4L7SK.js → chunk-NGGVPDNF.js} +1 -1
- package/dist/{chunk-46RYX62R.js → chunk-YSHQDGVZ.js} +1 -1
- package/dist/electron-server.js +335 -25
- package/dist/index.js +102 -4
- package/dist/{run-tests-6AZTCLL7.js → run-tests-3Y3P7QTJ.js} +1 -1
- package/dist/{server-PCSHZ5AI.js → server-KZ472I7D.js} +123 -7
- package/dist/web/client/app.js +174 -1
- package/dist/web/client/index.html +41 -0
- package/dist/web/client/style.css +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/web/auth.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
7
|
+
var USERS_FILE = "users.json";
|
|
8
|
+
var TOKEN_EXPIRY_HOURS = 24 * 7;
|
|
9
|
+
var USERS_DIR = "users";
|
|
10
|
+
var AuthManager = class {
|
|
11
|
+
usersFile;
|
|
12
|
+
baseDir;
|
|
13
|
+
db;
|
|
14
|
+
constructor(baseDir) {
|
|
15
|
+
this.baseDir = baseDir;
|
|
16
|
+
this.usersFile = join(baseDir, USERS_FILE);
|
|
17
|
+
this.db = this.loadOrCreate();
|
|
18
|
+
}
|
|
19
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
20
|
+
/** Check if any users exist (first-run detection) */
|
|
21
|
+
hasUsers() {
|
|
22
|
+
return this.db.users.length > 0;
|
|
23
|
+
}
|
|
24
|
+
/** Check if auth is enabled (has users.json with at least 1 user) */
|
|
25
|
+
isEnabled() {
|
|
26
|
+
return this.hasUsers();
|
|
27
|
+
}
|
|
28
|
+
/** Register a new user. Returns error string or null on success. */
|
|
29
|
+
register(username, password) {
|
|
30
|
+
username = username.trim().toLowerCase();
|
|
31
|
+
if (!username || username.length < 2 || username.length > 32) {
|
|
32
|
+
return "Username must be 2-32 characters";
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z0-9_-]+$/.test(username)) {
|
|
35
|
+
return "Username can only contain a-z, 0-9, _ and -";
|
|
36
|
+
}
|
|
37
|
+
if (!password || password.length < 4) {
|
|
38
|
+
return "Password must be at least 4 characters";
|
|
39
|
+
}
|
|
40
|
+
if (this.db.users.find((u) => u.username === username)) {
|
|
41
|
+
return "Username already exists";
|
|
42
|
+
}
|
|
43
|
+
const salt = randomBytes(16).toString("hex");
|
|
44
|
+
const passwordHash = this.hashPassword(password, salt);
|
|
45
|
+
const dataDir = join(USERS_DIR, username);
|
|
46
|
+
const fullDataDir = join(this.baseDir, dataDir);
|
|
47
|
+
mkdirSync(join(fullDataDir, "history"), { recursive: true });
|
|
48
|
+
const user = {
|
|
49
|
+
username,
|
|
50
|
+
passwordHash,
|
|
51
|
+
salt,
|
|
52
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
dataDir
|
|
54
|
+
};
|
|
55
|
+
this.db.users.push(user);
|
|
56
|
+
this.save();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Authenticate user. Returns JWT token or null. */
|
|
60
|
+
login(username, password) {
|
|
61
|
+
username = username.trim().toLowerCase();
|
|
62
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
63
|
+
if (!user) return null;
|
|
64
|
+
const hash = this.hashPassword(password, user.salt);
|
|
65
|
+
const a = Buffer.from(hash, "utf-8");
|
|
66
|
+
const b = Buffer.from(user.passwordHash, "utf-8");
|
|
67
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return this.createToken(username);
|
|
71
|
+
}
|
|
72
|
+
/** Verify a token. Returns username or null. */
|
|
73
|
+
verifyToken(token) {
|
|
74
|
+
try {
|
|
75
|
+
const parts = token.split(".");
|
|
76
|
+
if (parts.length !== 2) return null;
|
|
77
|
+
const [payloadB64, signature] = parts;
|
|
78
|
+
const expectedSig = this.sign(payloadB64);
|
|
79
|
+
if (signature !== expectedSig) return null;
|
|
80
|
+
const payload = JSON.parse(
|
|
81
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
82
|
+
);
|
|
83
|
+
if (Date.now() > payload.exp) return null;
|
|
84
|
+
if (!this.db.users.find((u) => u.username === payload.username)) return null;
|
|
85
|
+
return payload.username;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Get user's data directory (absolute path) */
|
|
91
|
+
getUserDataDir(username) {
|
|
92
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
93
|
+
if (!user) throw new Error(`User not found: ${username}`);
|
|
94
|
+
return join(this.baseDir, user.dataDir);
|
|
95
|
+
}
|
|
96
|
+
/** List all usernames */
|
|
97
|
+
listUsers() {
|
|
98
|
+
return this.db.users.map((u) => u.username);
|
|
99
|
+
}
|
|
100
|
+
/** Delete a user. Returns error string or null on success. */
|
|
101
|
+
deleteUser(username) {
|
|
102
|
+
username = username.trim().toLowerCase();
|
|
103
|
+
const idx = this.db.users.findIndex((u) => u.username === username);
|
|
104
|
+
if (idx === -1) return `User '${username}' not found`;
|
|
105
|
+
this.db.users.splice(idx, 1);
|
|
106
|
+
this.save();
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** Reset a user's password. Returns error string or null on success. */
|
|
110
|
+
resetPassword(username, newPassword) {
|
|
111
|
+
username = username.trim().toLowerCase();
|
|
112
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
113
|
+
if (!user) return `User '${username}' not found`;
|
|
114
|
+
if (!newPassword || newPassword.length < 4) {
|
|
115
|
+
return "Password must be at least 4 characters";
|
|
116
|
+
}
|
|
117
|
+
const salt = randomBytes(16).toString("hex");
|
|
118
|
+
user.passwordHash = this.hashPassword(newPassword, salt);
|
|
119
|
+
user.salt = salt;
|
|
120
|
+
this.save();
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Migrate existing single-user data to a named user account */
|
|
124
|
+
migrateExistingData(username, password) {
|
|
125
|
+
const err = this.register(username, password);
|
|
126
|
+
if (err) return err;
|
|
127
|
+
const userDir = this.getUserDataDir(username);
|
|
128
|
+
const globalConfig = join(this.baseDir, "config.json");
|
|
129
|
+
if (existsSync(globalConfig)) {
|
|
130
|
+
try {
|
|
131
|
+
const content = readFileSync(globalConfig, "utf-8");
|
|
132
|
+
writeFileSync(join(userDir, "config.json"), content, "utf-8");
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const globalMemory = join(this.baseDir, "memory.md");
|
|
137
|
+
if (existsSync(globalMemory)) {
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(globalMemory, "utf-8");
|
|
140
|
+
writeFileSync(join(userDir, "memory.md"), content, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const globalHistory = join(this.baseDir, "history");
|
|
145
|
+
if (existsSync(globalHistory)) {
|
|
146
|
+
try {
|
|
147
|
+
const files = readdirSync(globalHistory).filter((f) => f.endsWith(".json"));
|
|
148
|
+
const userHistory = join(userDir, "history");
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
try {
|
|
151
|
+
copyFileSync(join(globalHistory, f), join(userHistory, f));
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// ── Private methods ────────────────────────────────────────────
|
|
161
|
+
loadOrCreate() {
|
|
162
|
+
if (existsSync(this.usersFile)) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(this.usersFile, "utf-8"));
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const db = {
|
|
169
|
+
version: 1,
|
|
170
|
+
secret: randomBytes(32).toString("hex"),
|
|
171
|
+
users: []
|
|
172
|
+
};
|
|
173
|
+
this.saveDB(db);
|
|
174
|
+
return db;
|
|
175
|
+
}
|
|
176
|
+
save() {
|
|
177
|
+
this.saveDB(this.db);
|
|
178
|
+
}
|
|
179
|
+
saveDB(db) {
|
|
180
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
181
|
+
writeFileSync(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
|
|
182
|
+
}
|
|
183
|
+
hashPassword(password, salt) {
|
|
184
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
185
|
+
}
|
|
186
|
+
createToken(username) {
|
|
187
|
+
const payload = {
|
|
188
|
+
username,
|
|
189
|
+
exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
190
|
+
};
|
|
191
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
192
|
+
const signature = this.sign(payloadB64);
|
|
193
|
+
return `${payloadB64}.${signature}`;
|
|
194
|
+
}
|
|
195
|
+
sign(data) {
|
|
196
|
+
return createHmac("sha256", this.db.secret).update(data).digest("base64url");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
AuthManager
|
|
202
|
+
};
|
package/dist/electron-server.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { createServer } from "http";
|
|
4
4
|
import { WebSocketServer } from "ws";
|
|
5
|
-
import { join as
|
|
6
|
-
import { existsSync as
|
|
5
|
+
import { join as join14, dirname as dirname4, resolve as resolve5, relative as relative3 } from "path";
|
|
6
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
|
|
7
7
|
import { networkInterfaces } from "os";
|
|
8
8
|
|
|
9
9
|
// src/config/config-manager.ts
|
|
@@ -180,7 +180,7 @@ var EnvLoader = class {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
// src/core/constants.ts
|
|
183
|
-
var VERSION = "0.2.
|
|
183
|
+
var VERSION = "0.2.2";
|
|
184
184
|
var APP_NAME = "ai-cli";
|
|
185
185
|
var CONFIG_DIR_NAME = ".aicli";
|
|
186
186
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -7443,6 +7443,203 @@ async function setupProxy(configProxy) {
|
|
|
7443
7443
|
}
|
|
7444
7444
|
}
|
|
7445
7445
|
|
|
7446
|
+
// src/web/auth.ts
|
|
7447
|
+
import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, readdirSync as readdirSync10, copyFileSync } from "fs";
|
|
7448
|
+
import { join as join13 } from "path";
|
|
7449
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
7450
|
+
var USERS_FILE = "users.json";
|
|
7451
|
+
var TOKEN_EXPIRY_HOURS = 24 * 7;
|
|
7452
|
+
var USERS_DIR = "users";
|
|
7453
|
+
var AuthManager = class {
|
|
7454
|
+
usersFile;
|
|
7455
|
+
baseDir;
|
|
7456
|
+
db;
|
|
7457
|
+
constructor(baseDir) {
|
|
7458
|
+
this.baseDir = baseDir;
|
|
7459
|
+
this.usersFile = join13(baseDir, USERS_FILE);
|
|
7460
|
+
this.db = this.loadOrCreate();
|
|
7461
|
+
}
|
|
7462
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
7463
|
+
/** Check if any users exist (first-run detection) */
|
|
7464
|
+
hasUsers() {
|
|
7465
|
+
return this.db.users.length > 0;
|
|
7466
|
+
}
|
|
7467
|
+
/** Check if auth is enabled (has users.json with at least 1 user) */
|
|
7468
|
+
isEnabled() {
|
|
7469
|
+
return this.hasUsers();
|
|
7470
|
+
}
|
|
7471
|
+
/** Register a new user. Returns error string or null on success. */
|
|
7472
|
+
register(username, password) {
|
|
7473
|
+
username = username.trim().toLowerCase();
|
|
7474
|
+
if (!username || username.length < 2 || username.length > 32) {
|
|
7475
|
+
return "Username must be 2-32 characters";
|
|
7476
|
+
}
|
|
7477
|
+
if (!/^[a-z0-9_-]+$/.test(username)) {
|
|
7478
|
+
return "Username can only contain a-z, 0-9, _ and -";
|
|
7479
|
+
}
|
|
7480
|
+
if (!password || password.length < 4) {
|
|
7481
|
+
return "Password must be at least 4 characters";
|
|
7482
|
+
}
|
|
7483
|
+
if (this.db.users.find((u) => u.username === username)) {
|
|
7484
|
+
return "Username already exists";
|
|
7485
|
+
}
|
|
7486
|
+
const salt = randomBytes(16).toString("hex");
|
|
7487
|
+
const passwordHash = this.hashPassword(password, salt);
|
|
7488
|
+
const dataDir = join13(USERS_DIR, username);
|
|
7489
|
+
const fullDataDir = join13(this.baseDir, dataDir);
|
|
7490
|
+
mkdirSync10(join13(fullDataDir, "history"), { recursive: true });
|
|
7491
|
+
const user = {
|
|
7492
|
+
username,
|
|
7493
|
+
passwordHash,
|
|
7494
|
+
salt,
|
|
7495
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7496
|
+
dataDir
|
|
7497
|
+
};
|
|
7498
|
+
this.db.users.push(user);
|
|
7499
|
+
this.save();
|
|
7500
|
+
return null;
|
|
7501
|
+
}
|
|
7502
|
+
/** Authenticate user. Returns JWT token or null. */
|
|
7503
|
+
login(username, password) {
|
|
7504
|
+
username = username.trim().toLowerCase();
|
|
7505
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
7506
|
+
if (!user) return null;
|
|
7507
|
+
const hash = this.hashPassword(password, user.salt);
|
|
7508
|
+
const a = Buffer.from(hash, "utf-8");
|
|
7509
|
+
const b = Buffer.from(user.passwordHash, "utf-8");
|
|
7510
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
7511
|
+
return null;
|
|
7512
|
+
}
|
|
7513
|
+
return this.createToken(username);
|
|
7514
|
+
}
|
|
7515
|
+
/** Verify a token. Returns username or null. */
|
|
7516
|
+
verifyToken(token) {
|
|
7517
|
+
try {
|
|
7518
|
+
const parts = token.split(".");
|
|
7519
|
+
if (parts.length !== 2) return null;
|
|
7520
|
+
const [payloadB64, signature] = parts;
|
|
7521
|
+
const expectedSig = this.sign(payloadB64);
|
|
7522
|
+
if (signature !== expectedSig) return null;
|
|
7523
|
+
const payload = JSON.parse(
|
|
7524
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
7525
|
+
);
|
|
7526
|
+
if (Date.now() > payload.exp) return null;
|
|
7527
|
+
if (!this.db.users.find((u) => u.username === payload.username)) return null;
|
|
7528
|
+
return payload.username;
|
|
7529
|
+
} catch {
|
|
7530
|
+
return null;
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
/** Get user's data directory (absolute path) */
|
|
7534
|
+
getUserDataDir(username) {
|
|
7535
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
7536
|
+
if (!user) throw new Error(`User not found: ${username}`);
|
|
7537
|
+
return join13(this.baseDir, user.dataDir);
|
|
7538
|
+
}
|
|
7539
|
+
/** List all usernames */
|
|
7540
|
+
listUsers() {
|
|
7541
|
+
return this.db.users.map((u) => u.username);
|
|
7542
|
+
}
|
|
7543
|
+
/** Delete a user. Returns error string or null on success. */
|
|
7544
|
+
deleteUser(username) {
|
|
7545
|
+
username = username.trim().toLowerCase();
|
|
7546
|
+
const idx = this.db.users.findIndex((u) => u.username === username);
|
|
7547
|
+
if (idx === -1) return `User '${username}' not found`;
|
|
7548
|
+
this.db.users.splice(idx, 1);
|
|
7549
|
+
this.save();
|
|
7550
|
+
return null;
|
|
7551
|
+
}
|
|
7552
|
+
/** Reset a user's password. Returns error string or null on success. */
|
|
7553
|
+
resetPassword(username, newPassword) {
|
|
7554
|
+
username = username.trim().toLowerCase();
|
|
7555
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
7556
|
+
if (!user) return `User '${username}' not found`;
|
|
7557
|
+
if (!newPassword || newPassword.length < 4) {
|
|
7558
|
+
return "Password must be at least 4 characters";
|
|
7559
|
+
}
|
|
7560
|
+
const salt = randomBytes(16).toString("hex");
|
|
7561
|
+
user.passwordHash = this.hashPassword(newPassword, salt);
|
|
7562
|
+
user.salt = salt;
|
|
7563
|
+
this.save();
|
|
7564
|
+
return null;
|
|
7565
|
+
}
|
|
7566
|
+
/** Migrate existing single-user data to a named user account */
|
|
7567
|
+
migrateExistingData(username, password) {
|
|
7568
|
+
const err = this.register(username, password);
|
|
7569
|
+
if (err) return err;
|
|
7570
|
+
const userDir = this.getUserDataDir(username);
|
|
7571
|
+
const globalConfig = join13(this.baseDir, "config.json");
|
|
7572
|
+
if (existsSync18(globalConfig)) {
|
|
7573
|
+
try {
|
|
7574
|
+
const content = readFileSync13(globalConfig, "utf-8");
|
|
7575
|
+
writeFileSync9(join13(userDir, "config.json"), content, "utf-8");
|
|
7576
|
+
} catch {
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
const globalMemory = join13(this.baseDir, "memory.md");
|
|
7580
|
+
if (existsSync18(globalMemory)) {
|
|
7581
|
+
try {
|
|
7582
|
+
const content = readFileSync13(globalMemory, "utf-8");
|
|
7583
|
+
writeFileSync9(join13(userDir, "memory.md"), content, "utf-8");
|
|
7584
|
+
} catch {
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
const globalHistory = join13(this.baseDir, "history");
|
|
7588
|
+
if (existsSync18(globalHistory)) {
|
|
7589
|
+
try {
|
|
7590
|
+
const files = readdirSync10(globalHistory).filter((f) => f.endsWith(".json"));
|
|
7591
|
+
const userHistory = join13(userDir, "history");
|
|
7592
|
+
for (const f of files) {
|
|
7593
|
+
try {
|
|
7594
|
+
copyFileSync(join13(globalHistory, f), join13(userHistory, f));
|
|
7595
|
+
} catch {
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7598
|
+
} catch {
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
return null;
|
|
7602
|
+
}
|
|
7603
|
+
// ── Private methods ────────────────────────────────────────────
|
|
7604
|
+
loadOrCreate() {
|
|
7605
|
+
if (existsSync18(this.usersFile)) {
|
|
7606
|
+
try {
|
|
7607
|
+
return JSON.parse(readFileSync13(this.usersFile, "utf-8"));
|
|
7608
|
+
} catch {
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
const db = {
|
|
7612
|
+
version: 1,
|
|
7613
|
+
secret: randomBytes(32).toString("hex"),
|
|
7614
|
+
users: []
|
|
7615
|
+
};
|
|
7616
|
+
this.saveDB(db);
|
|
7617
|
+
return db;
|
|
7618
|
+
}
|
|
7619
|
+
save() {
|
|
7620
|
+
this.saveDB(this.db);
|
|
7621
|
+
}
|
|
7622
|
+
saveDB(db) {
|
|
7623
|
+
mkdirSync10(this.baseDir, { recursive: true });
|
|
7624
|
+
writeFileSync9(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
|
|
7625
|
+
}
|
|
7626
|
+
hashPassword(password, salt) {
|
|
7627
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
7628
|
+
}
|
|
7629
|
+
createToken(username) {
|
|
7630
|
+
const payload = {
|
|
7631
|
+
username,
|
|
7632
|
+
exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
7633
|
+
};
|
|
7634
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
7635
|
+
const signature = this.sign(payloadB64);
|
|
7636
|
+
return `${payloadB64}.${signature}`;
|
|
7637
|
+
}
|
|
7638
|
+
sign(data) {
|
|
7639
|
+
return createHmac("sha256", this.db.secret).update(data).digest("base64url");
|
|
7640
|
+
}
|
|
7641
|
+
};
|
|
7642
|
+
|
|
7446
7643
|
// src/web/server.ts
|
|
7447
7644
|
function getModuleDir() {
|
|
7448
7645
|
try {
|
|
@@ -7498,8 +7695,8 @@ async function startWebServer(options = {}) {
|
|
|
7498
7695
|
}
|
|
7499
7696
|
}
|
|
7500
7697
|
let skillManager = null;
|
|
7501
|
-
const skillsDir =
|
|
7502
|
-
if (
|
|
7698
|
+
const skillsDir = join14(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
7699
|
+
if (existsSync19(skillsDir)) {
|
|
7503
7700
|
skillManager = new SkillManager(skillsDir);
|
|
7504
7701
|
skillManager.loadSkills();
|
|
7505
7702
|
const count = skillManager.listSkills().length;
|
|
@@ -7519,15 +7716,15 @@ async function startWebServer(options = {}) {
|
|
|
7519
7716
|
const server = createServer(app);
|
|
7520
7717
|
const wss = new WebSocketServer({ server });
|
|
7521
7718
|
const moduleDir = getModuleDir();
|
|
7522
|
-
let clientDir =
|
|
7523
|
-
if (!
|
|
7524
|
-
clientDir =
|
|
7719
|
+
let clientDir = join14(moduleDir, "web", "client");
|
|
7720
|
+
if (!existsSync19(clientDir)) {
|
|
7721
|
+
clientDir = join14(moduleDir, "client");
|
|
7525
7722
|
}
|
|
7526
|
-
if (!
|
|
7527
|
-
clientDir =
|
|
7723
|
+
if (!existsSync19(clientDir)) {
|
|
7724
|
+
clientDir = join14(moduleDir, "..", "..", "src", "web", "client");
|
|
7528
7725
|
}
|
|
7529
|
-
if (!
|
|
7530
|
-
clientDir =
|
|
7726
|
+
if (!existsSync19(clientDir)) {
|
|
7727
|
+
clientDir = join14(process.cwd(), "src", "web", "client");
|
|
7531
7728
|
}
|
|
7532
7729
|
console.log(` Static files: ${clientDir}`);
|
|
7533
7730
|
app.use(express.static(clientDir));
|
|
@@ -7540,23 +7737,43 @@ async function startWebServer(options = {}) {
|
|
|
7540
7737
|
models: p.info.models.map((m) => ({ id: m.id, name: m.name ?? m.id }))
|
|
7541
7738
|
})),
|
|
7542
7739
|
tools: toolRegistry.getDefinitions().length,
|
|
7543
|
-
cwd: process.cwd()
|
|
7740
|
+
cwd: process.cwd(),
|
|
7741
|
+
auth: {
|
|
7742
|
+
enabled: authManager.isEnabled(),
|
|
7743
|
+
userCount: authManager.listUsers().length
|
|
7744
|
+
}
|
|
7544
7745
|
});
|
|
7545
7746
|
});
|
|
7747
|
+
app.use(express.json());
|
|
7748
|
+
app.post("/api/auth/register", (req, res) => {
|
|
7749
|
+
const { username, password } = req.body ?? {};
|
|
7750
|
+
if (!username || !password) {
|
|
7751
|
+
res.status(400).json({ error: "Username and password required" });
|
|
7752
|
+
return;
|
|
7753
|
+
}
|
|
7754
|
+
const err = authManager.register(username, password);
|
|
7755
|
+
if (err) {
|
|
7756
|
+
res.status(400).json({ error: err });
|
|
7757
|
+
return;
|
|
7758
|
+
}
|
|
7759
|
+
const token = authManager.login(username, password);
|
|
7760
|
+
console.log(` \u2713 User registered via API: ${username}`);
|
|
7761
|
+
res.json({ success: true, token, username });
|
|
7762
|
+
});
|
|
7546
7763
|
app.get("/api/files", (req, res) => {
|
|
7547
7764
|
const cwd = process.cwd();
|
|
7548
7765
|
const prefix = req.query.prefix || "";
|
|
7549
|
-
const targetDir =
|
|
7766
|
+
const targetDir = join14(cwd, prefix);
|
|
7550
7767
|
if (!resolve5(targetDir).startsWith(resolve5(cwd))) {
|
|
7551
7768
|
res.json({ files: [] });
|
|
7552
7769
|
return;
|
|
7553
7770
|
}
|
|
7554
7771
|
try {
|
|
7555
7772
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
7556
|
-
const entries =
|
|
7773
|
+
const entries = readdirSync11(targetDir, { withFileTypes: true });
|
|
7557
7774
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
7558
7775
|
name: e.name,
|
|
7559
|
-
path: relative3(cwd,
|
|
7776
|
+
path: relative3(cwd, join14(targetDir, e.name)).replace(/\\/g, "/"),
|
|
7560
7777
|
isDir: e.isDirectory()
|
|
7561
7778
|
}));
|
|
7562
7779
|
res.json({ files });
|
|
@@ -7588,7 +7805,7 @@ async function startWebServer(options = {}) {
|
|
|
7588
7805
|
return;
|
|
7589
7806
|
}
|
|
7590
7807
|
const cwd = process.cwd();
|
|
7591
|
-
const fullPath = resolve5(
|
|
7808
|
+
const fullPath = resolve5(join14(cwd, filePath));
|
|
7592
7809
|
if (!fullPath.startsWith(resolve5(cwd))) {
|
|
7593
7810
|
res.json({ error: "Access denied" });
|
|
7594
7811
|
return;
|
|
@@ -7599,26 +7816,76 @@ async function startWebServer(options = {}) {
|
|
|
7599
7816
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
7600
7817
|
return;
|
|
7601
7818
|
}
|
|
7602
|
-
const content =
|
|
7819
|
+
const content = readFileSync14(fullPath, "utf-8");
|
|
7603
7820
|
res.json({ content, size: stat.size });
|
|
7604
7821
|
} catch (err) {
|
|
7605
7822
|
res.json({ error: `Cannot read: ${err.message}` });
|
|
7606
7823
|
}
|
|
7607
7824
|
});
|
|
7825
|
+
const authManager = new AuthManager(config.getConfigDir());
|
|
7826
|
+
if (authManager.isEnabled()) {
|
|
7827
|
+
console.log(` Auth: ${authManager.listUsers().length} user(s) registered`);
|
|
7828
|
+
} else {
|
|
7829
|
+
console.log(` Auth: disabled (no users registered)`);
|
|
7830
|
+
}
|
|
7831
|
+
const userResources = /* @__PURE__ */ new Map();
|
|
7832
|
+
function getUserShared(username) {
|
|
7833
|
+
const cached = userResources.get(username);
|
|
7834
|
+
if (cached) return cached;
|
|
7835
|
+
const userDataDir = authManager.getUserDataDir(username);
|
|
7836
|
+
const userConfig = new ConfigManager(userDataDir);
|
|
7837
|
+
const userSessions = new SessionManager(userConfig);
|
|
7838
|
+
const userShared = {
|
|
7839
|
+
config: userConfig,
|
|
7840
|
+
providers,
|
|
7841
|
+
// Shared — providers re-read API keys per call
|
|
7842
|
+
sessions: userSessions,
|
|
7843
|
+
toolRegistry,
|
|
7844
|
+
// Shared
|
|
7845
|
+
mcpManager,
|
|
7846
|
+
// Shared
|
|
7847
|
+
skillManager
|
|
7848
|
+
// Shared
|
|
7849
|
+
};
|
|
7850
|
+
userResources.set(username, userShared);
|
|
7851
|
+
return userShared;
|
|
7852
|
+
}
|
|
7608
7853
|
const handlers = /* @__PURE__ */ new Map();
|
|
7609
7854
|
wss.on("connection", (ws, req) => {
|
|
7610
7855
|
const urlStr = req.url ?? "";
|
|
7611
7856
|
const qMark = urlStr.indexOf("?");
|
|
7612
7857
|
const params = new URLSearchParams(qMark >= 0 ? urlStr.slice(qMark + 1) : "");
|
|
7613
7858
|
const tabId = params.get("tabId") || `tab-${Date.now()}`;
|
|
7859
|
+
const token = params.get("token") || "";
|
|
7614
7860
|
const existing = handlers.get(tabId);
|
|
7615
7861
|
if (existing) {
|
|
7616
7862
|
existing.onDisconnect();
|
|
7617
7863
|
handlers.delete(tabId);
|
|
7618
7864
|
}
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7865
|
+
let authenticatedUser = null;
|
|
7866
|
+
let handler = null;
|
|
7867
|
+
if (token) {
|
|
7868
|
+
authenticatedUser = authManager.verifyToken(token);
|
|
7869
|
+
}
|
|
7870
|
+
const authRequired = authManager.isEnabled();
|
|
7871
|
+
if (!authRequired) {
|
|
7872
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
|
|
7873
|
+
handler = new SessionHandler(ws, shared);
|
|
7874
|
+
handlers.set(tabId, handler);
|
|
7875
|
+
} else if (authenticatedUser) {
|
|
7876
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
|
|
7877
|
+
const userShared = getUserShared(authenticatedUser);
|
|
7878
|
+
handler = new SessionHandler(ws, userShared);
|
|
7879
|
+
handlers.set(tabId, handler);
|
|
7880
|
+
} else {
|
|
7881
|
+
console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
|
|
7882
|
+
if (ws.readyState === ws.OPEN) {
|
|
7883
|
+
ws.send(JSON.stringify({
|
|
7884
|
+
type: "auth_required",
|
|
7885
|
+
hasUsers: authManager.hasUsers()
|
|
7886
|
+
}));
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7622
7889
|
ws.on("message", async (data) => {
|
|
7623
7890
|
try {
|
|
7624
7891
|
const raw = data.toString();
|
|
@@ -7629,6 +7896,49 @@ async function startWebServer(options = {}) {
|
|
|
7629
7896
|
}
|
|
7630
7897
|
return;
|
|
7631
7898
|
}
|
|
7899
|
+
if (parsed.type === "auth") {
|
|
7900
|
+
const { action, username, password } = parsed;
|
|
7901
|
+
if (action === "register") {
|
|
7902
|
+
const err = authManager.register(username, password);
|
|
7903
|
+
if (err) {
|
|
7904
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
|
|
7905
|
+
return;
|
|
7906
|
+
}
|
|
7907
|
+
const newToken = authManager.login(username, password);
|
|
7908
|
+
authenticatedUser = username;
|
|
7909
|
+
const userShared = getUserShared(username);
|
|
7910
|
+
handler = new SessionHandler(ws, userShared);
|
|
7911
|
+
handlers.set(tabId, handler);
|
|
7912
|
+
console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
7913
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
|
|
7914
|
+
return;
|
|
7915
|
+
}
|
|
7916
|
+
if (action === "login") {
|
|
7917
|
+
const loginToken = authManager.login(username, password);
|
|
7918
|
+
if (!loginToken) {
|
|
7919
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
|
|
7920
|
+
return;
|
|
7921
|
+
}
|
|
7922
|
+
authenticatedUser = username;
|
|
7923
|
+
const userShared = getUserShared(username);
|
|
7924
|
+
handler = new SessionHandler(ws, userShared);
|
|
7925
|
+
handlers.set(tabId, handler);
|
|
7926
|
+
console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
7927
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
|
|
7928
|
+
return;
|
|
7929
|
+
}
|
|
7930
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
|
|
7931
|
+
return;
|
|
7932
|
+
}
|
|
7933
|
+
if (!handler) {
|
|
7934
|
+
if (ws.readyState === ws.OPEN) {
|
|
7935
|
+
ws.send(JSON.stringify({
|
|
7936
|
+
type: "auth_required",
|
|
7937
|
+
hasUsers: authManager.hasUsers()
|
|
7938
|
+
}));
|
|
7939
|
+
}
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7632
7942
|
await handler.handleMessage(raw);
|
|
7633
7943
|
} catch (err) {
|
|
7634
7944
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7640,7 +7950,7 @@ async function startWebServer(options = {}) {
|
|
|
7640
7950
|
});
|
|
7641
7951
|
ws.on("close", () => {
|
|
7642
7952
|
console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
|
|
7643
|
-
handler.onDisconnect();
|
|
7953
|
+
if (handler) handler.onDisconnect();
|
|
7644
7954
|
handlers.delete(tabId);
|
|
7645
7955
|
});
|
|
7646
7956
|
ws.on("error", (err) => {
|
|
@@ -7700,10 +8010,10 @@ function loadProjectMcpConfig() {
|
|
|
7700
8010
|
const cwd = process.cwd();
|
|
7701
8011
|
const gitRoot = getGitRoot(cwd);
|
|
7702
8012
|
const projectRoot = gitRoot ?? cwd;
|
|
7703
|
-
const configPath =
|
|
7704
|
-
if (!
|
|
8013
|
+
const configPath = join14(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
8014
|
+
if (!existsSync19(configPath)) return null;
|
|
7705
8015
|
try {
|
|
7706
|
-
const raw = JSON.parse(
|
|
8016
|
+
const raw = JSON.parse(readFileSync14(configPath, "utf-8"));
|
|
7707
8017
|
return raw.mcpServers ?? raw;
|
|
7708
8018
|
} catch {
|
|
7709
8019
|
return null;
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
theme,
|
|
36
36
|
truncateOutput,
|
|
37
37
|
undoStack
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-NGGVPDNF.js";
|
|
39
39
|
import {
|
|
40
40
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
41
41
|
AUTHOR,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
REPO_URL,
|
|
56
56
|
SKILLS_DIR_NAME,
|
|
57
57
|
VERSION
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-YSHQDGVZ.js";
|
|
59
59
|
|
|
60
60
|
// src/index.ts
|
|
61
61
|
import { program } from "commander";
|
|
@@ -1904,7 +1904,7 @@ ${hint}` : "")
|
|
|
1904
1904
|
description: "Run project tests and show structured report",
|
|
1905
1905
|
usage: "/test [command|filter]",
|
|
1906
1906
|
async execute(args, _ctx) {
|
|
1907
|
-
const { executeTests } = await import("./run-tests-
|
|
1907
|
+
const { executeTests } = await import("./run-tests-3Y3P7QTJ.js");
|
|
1908
1908
|
const argStr = args.join(" ").trim();
|
|
1909
1909
|
let testArgs = {};
|
|
1910
1910
|
if (argStr) {
|
|
@@ -5292,9 +5292,107 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5292
5292
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5293
5293
|
process.exit(1);
|
|
5294
5294
|
}
|
|
5295
|
-
const { startWebServer } = await import("./server-
|
|
5295
|
+
const { startWebServer } = await import("./server-KZ472I7D.js");
|
|
5296
5296
|
await startWebServer({ port, host: options.host });
|
|
5297
5297
|
});
|
|
5298
|
+
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
5299
|
+
const { AuthManager } = await import("./auth-TWWF22YZ.js");
|
|
5300
|
+
const config = new ConfigManager();
|
|
5301
|
+
const auth = new AuthManager(config.getConfigDir());
|
|
5302
|
+
if (!action || action === "list") {
|
|
5303
|
+
const users = auth.listUsers();
|
|
5304
|
+
if (users.length === 0) {
|
|
5305
|
+
console.log("\nNo users registered. Auth is disabled (single-user mode).");
|
|
5306
|
+
console.log("Create a user to enable multi-user auth: aicli user create <username>\n");
|
|
5307
|
+
} else {
|
|
5308
|
+
console.log(`
|
|
5309
|
+
${users.length} user(s) registered (auth enabled):
|
|
5310
|
+
`);
|
|
5311
|
+
for (const u of users) {
|
|
5312
|
+
console.log(` \u2022 ${u}`);
|
|
5313
|
+
}
|
|
5314
|
+
console.log();
|
|
5315
|
+
}
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
5318
|
+
async function askPassword(msg) {
|
|
5319
|
+
const { password: pwFn } = await import("@inquirer/prompts");
|
|
5320
|
+
return pwFn({ message: msg, mask: "*" });
|
|
5321
|
+
}
|
|
5322
|
+
if (action === "create") {
|
|
5323
|
+
if (!username) {
|
|
5324
|
+
console.error("Usage: aicli user create <username>");
|
|
5325
|
+
process.exit(1);
|
|
5326
|
+
}
|
|
5327
|
+
const pw = await askPassword(`Password for ${username}:`);
|
|
5328
|
+
if (!pw || pw.length < 4) {
|
|
5329
|
+
console.error("Password must be at least 4 characters.");
|
|
5330
|
+
process.exit(1);
|
|
5331
|
+
}
|
|
5332
|
+
const err = auth.register(username, pw);
|
|
5333
|
+
if (err) {
|
|
5334
|
+
console.error(`Error: ${err}`);
|
|
5335
|
+
process.exit(1);
|
|
5336
|
+
}
|
|
5337
|
+
console.log(`\u2713 User '${username}' created. Auth is now enabled for Web UI.`);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
if (action === "delete") {
|
|
5341
|
+
if (!username) {
|
|
5342
|
+
console.error("Usage: aicli user delete <username>");
|
|
5343
|
+
process.exit(1);
|
|
5344
|
+
}
|
|
5345
|
+
const err = auth.deleteUser(username);
|
|
5346
|
+
if (err) {
|
|
5347
|
+
console.error(`Error: ${err}`);
|
|
5348
|
+
process.exit(1);
|
|
5349
|
+
}
|
|
5350
|
+
console.log(`\u2713 User '${username}' deleted.`);
|
|
5351
|
+
if (!auth.hasUsers()) {
|
|
5352
|
+
console.log(" No users remaining \u2014 auth is now disabled (single-user mode).");
|
|
5353
|
+
}
|
|
5354
|
+
return;
|
|
5355
|
+
}
|
|
5356
|
+
if (action === "reset-password") {
|
|
5357
|
+
if (!username) {
|
|
5358
|
+
console.error("Usage: aicli user reset-password <username>");
|
|
5359
|
+
process.exit(1);
|
|
5360
|
+
}
|
|
5361
|
+
const pw = await askPassword(`New password for ${username}:`);
|
|
5362
|
+
if (!pw || pw.length < 4) {
|
|
5363
|
+
console.error("Password must be at least 4 characters.");
|
|
5364
|
+
process.exit(1);
|
|
5365
|
+
}
|
|
5366
|
+
const err = auth.resetPassword(username, pw);
|
|
5367
|
+
if (err) {
|
|
5368
|
+
console.error(`Error: ${err}`);
|
|
5369
|
+
process.exit(1);
|
|
5370
|
+
}
|
|
5371
|
+
console.log(`\u2713 Password reset for '${username}'.`);
|
|
5372
|
+
return;
|
|
5373
|
+
}
|
|
5374
|
+
if (action === "migrate") {
|
|
5375
|
+
if (!username) {
|
|
5376
|
+
console.error("Usage: aicli user migrate <username>");
|
|
5377
|
+
process.exit(1);
|
|
5378
|
+
}
|
|
5379
|
+
const pw = await askPassword(`Password for new user ${username}:`);
|
|
5380
|
+
if (!pw || pw.length < 4) {
|
|
5381
|
+
console.error("Password must be at least 4 characters.");
|
|
5382
|
+
process.exit(1);
|
|
5383
|
+
}
|
|
5384
|
+
const err = auth.migrateExistingData(username, pw);
|
|
5385
|
+
if (err) {
|
|
5386
|
+
console.error(`Error: ${err}`);
|
|
5387
|
+
process.exit(1);
|
|
5388
|
+
}
|
|
5389
|
+
console.log(`\u2713 User '${username}' created with existing data migrated.`);
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5392
|
+
console.error(`Unknown action: ${action}`);
|
|
5393
|
+
console.error("Available: list, create, delete, reset-password, migrate");
|
|
5394
|
+
process.exit(1);
|
|
5395
|
+
});
|
|
5298
5396
|
program.command("sessions").description("List recent conversation sessions").action(async () => {
|
|
5299
5397
|
const config = new ConfigManager();
|
|
5300
5398
|
const sessions = new SessionManager(config);
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setupProxy,
|
|
24
24
|
spawnAgentContext,
|
|
25
25
|
truncateOutput
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-NGGVPDNF.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
29
29
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
PLAN_MODE_SYSTEM_ADDON,
|
|
36
36
|
SKILLS_DIR_NAME,
|
|
37
37
|
VERSION
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-YSHQDGVZ.js";
|
|
39
|
+
import {
|
|
40
|
+
AuthManager
|
|
41
|
+
} from "./chunk-CPLT6CD3.js";
|
|
39
42
|
|
|
40
43
|
// src/web/server.ts
|
|
41
44
|
import express from "express";
|
|
@@ -1450,9 +1453,29 @@ async function startWebServer(options = {}) {
|
|
|
1450
1453
|
models: p.info.models.map((m) => ({ id: m.id, name: m.name ?? m.id }))
|
|
1451
1454
|
})),
|
|
1452
1455
|
tools: toolRegistry.getDefinitions().length,
|
|
1453
|
-
cwd: process.cwd()
|
|
1456
|
+
cwd: process.cwd(),
|
|
1457
|
+
auth: {
|
|
1458
|
+
enabled: authManager.isEnabled(),
|
|
1459
|
+
userCount: authManager.listUsers().length
|
|
1460
|
+
}
|
|
1454
1461
|
});
|
|
1455
1462
|
});
|
|
1463
|
+
app.use(express.json());
|
|
1464
|
+
app.post("/api/auth/register", (req, res) => {
|
|
1465
|
+
const { username, password } = req.body ?? {};
|
|
1466
|
+
if (!username || !password) {
|
|
1467
|
+
res.status(400).json({ error: "Username and password required" });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const err = authManager.register(username, password);
|
|
1471
|
+
if (err) {
|
|
1472
|
+
res.status(400).json({ error: err });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const token = authManager.login(username, password);
|
|
1476
|
+
console.log(` \u2713 User registered via API: ${username}`);
|
|
1477
|
+
res.json({ success: true, token, username });
|
|
1478
|
+
});
|
|
1456
1479
|
app.get("/api/files", (req, res) => {
|
|
1457
1480
|
const cwd = process.cwd();
|
|
1458
1481
|
const prefix = req.query.prefix || "";
|
|
@@ -1515,20 +1538,70 @@ async function startWebServer(options = {}) {
|
|
|
1515
1538
|
res.json({ error: `Cannot read: ${err.message}` });
|
|
1516
1539
|
}
|
|
1517
1540
|
});
|
|
1541
|
+
const authManager = new AuthManager(config.getConfigDir());
|
|
1542
|
+
if (authManager.isEnabled()) {
|
|
1543
|
+
console.log(` Auth: ${authManager.listUsers().length} user(s) registered`);
|
|
1544
|
+
} else {
|
|
1545
|
+
console.log(` Auth: disabled (no users registered)`);
|
|
1546
|
+
}
|
|
1547
|
+
const userResources = /* @__PURE__ */ new Map();
|
|
1548
|
+
function getUserShared(username) {
|
|
1549
|
+
const cached = userResources.get(username);
|
|
1550
|
+
if (cached) return cached;
|
|
1551
|
+
const userDataDir = authManager.getUserDataDir(username);
|
|
1552
|
+
const userConfig = new ConfigManager(userDataDir);
|
|
1553
|
+
const userSessions = new SessionManager(userConfig);
|
|
1554
|
+
const userShared = {
|
|
1555
|
+
config: userConfig,
|
|
1556
|
+
providers,
|
|
1557
|
+
// Shared — providers re-read API keys per call
|
|
1558
|
+
sessions: userSessions,
|
|
1559
|
+
toolRegistry,
|
|
1560
|
+
// Shared
|
|
1561
|
+
mcpManager,
|
|
1562
|
+
// Shared
|
|
1563
|
+
skillManager
|
|
1564
|
+
// Shared
|
|
1565
|
+
};
|
|
1566
|
+
userResources.set(username, userShared);
|
|
1567
|
+
return userShared;
|
|
1568
|
+
}
|
|
1518
1569
|
const handlers = /* @__PURE__ */ new Map();
|
|
1519
1570
|
wss.on("connection", (ws, req) => {
|
|
1520
1571
|
const urlStr = req.url ?? "";
|
|
1521
1572
|
const qMark = urlStr.indexOf("?");
|
|
1522
1573
|
const params = new URLSearchParams(qMark >= 0 ? urlStr.slice(qMark + 1) : "");
|
|
1523
1574
|
const tabId = params.get("tabId") || `tab-${Date.now()}`;
|
|
1575
|
+
const token = params.get("token") || "";
|
|
1524
1576
|
const existing = handlers.get(tabId);
|
|
1525
1577
|
if (existing) {
|
|
1526
1578
|
existing.onDisconnect();
|
|
1527
1579
|
handlers.delete(tabId);
|
|
1528
1580
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1581
|
+
let authenticatedUser = null;
|
|
1582
|
+
let handler = null;
|
|
1583
|
+
if (token) {
|
|
1584
|
+
authenticatedUser = authManager.verifyToken(token);
|
|
1585
|
+
}
|
|
1586
|
+
const authRequired = authManager.isEnabled();
|
|
1587
|
+
if (!authRequired) {
|
|
1588
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
|
|
1589
|
+
handler = new SessionHandler(ws, shared);
|
|
1590
|
+
handlers.set(tabId, handler);
|
|
1591
|
+
} else if (authenticatedUser) {
|
|
1592
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
|
|
1593
|
+
const userShared = getUserShared(authenticatedUser);
|
|
1594
|
+
handler = new SessionHandler(ws, userShared);
|
|
1595
|
+
handlers.set(tabId, handler);
|
|
1596
|
+
} else {
|
|
1597
|
+
console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
|
|
1598
|
+
if (ws.readyState === ws.OPEN) {
|
|
1599
|
+
ws.send(JSON.stringify({
|
|
1600
|
+
type: "auth_required",
|
|
1601
|
+
hasUsers: authManager.hasUsers()
|
|
1602
|
+
}));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1532
1605
|
ws.on("message", async (data) => {
|
|
1533
1606
|
try {
|
|
1534
1607
|
const raw = data.toString();
|
|
@@ -1539,6 +1612,49 @@ async function startWebServer(options = {}) {
|
|
|
1539
1612
|
}
|
|
1540
1613
|
return;
|
|
1541
1614
|
}
|
|
1615
|
+
if (parsed.type === "auth") {
|
|
1616
|
+
const { action, username, password } = parsed;
|
|
1617
|
+
if (action === "register") {
|
|
1618
|
+
const err = authManager.register(username, password);
|
|
1619
|
+
if (err) {
|
|
1620
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const newToken = authManager.login(username, password);
|
|
1624
|
+
authenticatedUser = username;
|
|
1625
|
+
const userShared = getUserShared(username);
|
|
1626
|
+
handler = new SessionHandler(ws, userShared);
|
|
1627
|
+
handlers.set(tabId, handler);
|
|
1628
|
+
console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
1629
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (action === "login") {
|
|
1633
|
+
const loginToken = authManager.login(username, password);
|
|
1634
|
+
if (!loginToken) {
|
|
1635
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
authenticatedUser = username;
|
|
1639
|
+
const userShared = getUserShared(username);
|
|
1640
|
+
handler = new SessionHandler(ws, userShared);
|
|
1641
|
+
handlers.set(tabId, handler);
|
|
1642
|
+
console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
1643
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (!handler) {
|
|
1650
|
+
if (ws.readyState === ws.OPEN) {
|
|
1651
|
+
ws.send(JSON.stringify({
|
|
1652
|
+
type: "auth_required",
|
|
1653
|
+
hasUsers: authManager.hasUsers()
|
|
1654
|
+
}));
|
|
1655
|
+
}
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1542
1658
|
await handler.handleMessage(raw);
|
|
1543
1659
|
} catch (err) {
|
|
1544
1660
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1550,7 +1666,7 @@ async function startWebServer(options = {}) {
|
|
|
1550
1666
|
});
|
|
1551
1667
|
ws.on("close", () => {
|
|
1552
1668
|
console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
|
|
1553
|
-
handler.onDisconnect();
|
|
1669
|
+
if (handler) handler.onDisconnect();
|
|
1554
1670
|
handlers.delete(tabId);
|
|
1555
1671
|
});
|
|
1556
1672
|
ws.on("error", (err) => {
|
package/dist/web/client/app.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
let ws = null;
|
|
9
9
|
let connected = false;
|
|
10
10
|
let processing = false;
|
|
11
|
+
let authToken = localStorage.getItem('aicli-auth-token') || '';
|
|
12
|
+
let authUsername = localStorage.getItem('aicli-auth-user') || '';
|
|
13
|
+
let authMode = 'login'; // 'login' or 'register'
|
|
11
14
|
let currentAssistantEl = null;
|
|
12
15
|
let currentAssistantContent = '';
|
|
13
16
|
let currentThinkingEl = null;
|
|
@@ -73,7 +76,8 @@ let reconnectDelay = 1000; // Start at 1s, exponential backoff
|
|
|
73
76
|
|
|
74
77
|
function connect() {
|
|
75
78
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
76
|
-
|
|
79
|
+
const tokenParam = authToken ? `&token=${encodeURIComponent(authToken)}` : '';
|
|
80
|
+
ws = new WebSocket(`${protocol}//${location.host}?tabId=${tabId}${tokenParam}`);
|
|
77
81
|
|
|
78
82
|
ws.onopen = () => {
|
|
79
83
|
connected = true;
|
|
@@ -81,6 +85,7 @@ function connect() {
|
|
|
81
85
|
connectionStatus.textContent = '🟢 Connected';
|
|
82
86
|
connectionStatus.className = 'status-connected';
|
|
83
87
|
requestSessionList();
|
|
88
|
+
checkAuthStatus();
|
|
84
89
|
// Start heartbeat ping every 30s
|
|
85
90
|
clearInterval(heartbeatTimer);
|
|
86
91
|
heartbeatTimer = setInterval(() => {
|
|
@@ -153,6 +158,8 @@ function handleServerMessage(msg) {
|
|
|
153
158
|
case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
|
|
154
159
|
case 'export_data': handleExportData(msg); break;
|
|
155
160
|
case 'memory_content': handleMemoryContent(msg); break;
|
|
161
|
+
case 'auth_required': showAuthScreen(msg.hasUsers); break;
|
|
162
|
+
case 'auth_result': handleAuthResult(msg); break;
|
|
156
163
|
case 'info': addInfoMessage(msg.message); break;
|
|
157
164
|
case 'error': addErrorMessage(msg.message); hideRoundProgress(); clearAllToolTimers(); setProcessing(false); break;
|
|
158
165
|
case 'round_progress': handleRoundProgress(msg); break;
|
|
@@ -1791,3 +1798,169 @@ if (sessionListEl) {
|
|
|
1791
1798
|
}
|
|
1792
1799
|
});
|
|
1793
1800
|
}
|
|
1801
|
+
|
|
1802
|
+
// ── Auth: Login / Register / Logout ─────────────────────────────────
|
|
1803
|
+
|
|
1804
|
+
function showAuthScreen(hasUsers) {
|
|
1805
|
+
// Clear any stale token
|
|
1806
|
+
authToken = '';
|
|
1807
|
+
authUsername = '';
|
|
1808
|
+
localStorage.removeItem('aicli-auth-token');
|
|
1809
|
+
localStorage.removeItem('aicli-auth-user');
|
|
1810
|
+
|
|
1811
|
+
const screen = document.getElementById('auth-screen');
|
|
1812
|
+
const app = document.getElementById('app');
|
|
1813
|
+
screen.classList.remove('hidden');
|
|
1814
|
+
app.style.display = 'none';
|
|
1815
|
+
|
|
1816
|
+
// If no users registered, show register form by default
|
|
1817
|
+
if (!hasUsers) {
|
|
1818
|
+
authMode = 'register';
|
|
1819
|
+
updateAuthForm();
|
|
1820
|
+
} else {
|
|
1821
|
+
authMode = 'login';
|
|
1822
|
+
updateAuthForm();
|
|
1823
|
+
}
|
|
1824
|
+
// Focus username
|
|
1825
|
+
setTimeout(() => document.getElementById('auth-username')?.focus(), 100);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function hideAuthScreen() {
|
|
1829
|
+
const screen = document.getElementById('auth-screen');
|
|
1830
|
+
const app = document.getElementById('app');
|
|
1831
|
+
screen.classList.add('hidden');
|
|
1832
|
+
app.style.display = '';
|
|
1833
|
+
// Show user menu
|
|
1834
|
+
updateUserMenu();
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function updateAuthForm() {
|
|
1838
|
+
const subtitle = document.getElementById('auth-subtitle');
|
|
1839
|
+
const submit = document.getElementById('auth-submit');
|
|
1840
|
+
const toggle = document.getElementById('auth-toggle');
|
|
1841
|
+
const errorEl = document.getElementById('auth-error');
|
|
1842
|
+
|
|
1843
|
+
errorEl.classList.add('hidden');
|
|
1844
|
+
|
|
1845
|
+
if (authMode === 'register') {
|
|
1846
|
+
subtitle.textContent = 'Create your account to get started';
|
|
1847
|
+
submit.textContent = 'Create Account';
|
|
1848
|
+
toggle.innerHTML = 'Already have an account? <span class="text-primary">Sign In</span>';
|
|
1849
|
+
} else {
|
|
1850
|
+
subtitle.textContent = 'Sign in to continue';
|
|
1851
|
+
submit.textContent = 'Sign In';
|
|
1852
|
+
toggle.innerHTML = 'Don\'t have an account? <span class="text-primary">Register</span>';
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function toggleAuthMode() {
|
|
1857
|
+
authMode = authMode === 'login' ? 'register' : 'login';
|
|
1858
|
+
updateAuthForm();
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function handleAuth(event) {
|
|
1862
|
+
event.preventDefault();
|
|
1863
|
+
const username = document.getElementById('auth-username').value.trim();
|
|
1864
|
+
const password = document.getElementById('auth-password').value;
|
|
1865
|
+
const errorEl = document.getElementById('auth-error');
|
|
1866
|
+
|
|
1867
|
+
if (!username || !password) {
|
|
1868
|
+
errorEl.textContent = 'Please enter username and password';
|
|
1869
|
+
errorEl.classList.remove('hidden');
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
errorEl.classList.add('hidden');
|
|
1874
|
+
document.getElementById('auth-submit').disabled = true;
|
|
1875
|
+
|
|
1876
|
+
send({ type: 'auth', action: authMode, username, password });
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function handleAuthResult(msg) {
|
|
1880
|
+
const errorEl = document.getElementById('auth-error');
|
|
1881
|
+
const submitBtn = document.getElementById('auth-submit');
|
|
1882
|
+
submitBtn.disabled = false;
|
|
1883
|
+
|
|
1884
|
+
if (msg.success) {
|
|
1885
|
+
authToken = msg.token;
|
|
1886
|
+
authUsername = msg.username;
|
|
1887
|
+
localStorage.setItem('aicli-auth-token', msg.token);
|
|
1888
|
+
localStorage.setItem('aicli-auth-user', msg.username);
|
|
1889
|
+
hideAuthScreen();
|
|
1890
|
+
// Request session list now that we're authenticated
|
|
1891
|
+
requestSessionList();
|
|
1892
|
+
} else {
|
|
1893
|
+
errorEl.textContent = msg.error || 'Authentication failed';
|
|
1894
|
+
errorEl.classList.remove('hidden');
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function updateUserMenu() {
|
|
1899
|
+
const userMenu = document.getElementById('user-menu');
|
|
1900
|
+
const userDisplay = document.getElementById('user-display');
|
|
1901
|
+
const userLabel = document.getElementById('user-label');
|
|
1902
|
+
|
|
1903
|
+
if (authUsername) {
|
|
1904
|
+
userMenu.classList.remove('hidden');
|
|
1905
|
+
userDisplay.textContent = '👤 ' + authUsername;
|
|
1906
|
+
userLabel.textContent = authUsername;
|
|
1907
|
+
} else {
|
|
1908
|
+
userMenu.classList.add('hidden');
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function handleLogout() {
|
|
1913
|
+
authToken = '';
|
|
1914
|
+
authUsername = '';
|
|
1915
|
+
localStorage.removeItem('aicli-auth-token');
|
|
1916
|
+
localStorage.removeItem('aicli-auth-user');
|
|
1917
|
+
sessionStorage.removeItem('aicli-active-session');
|
|
1918
|
+
// Reconnect — server will send auth_required
|
|
1919
|
+
if (ws) ws.close();
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// ── Enable Auth (register first user from Web UI) ────────────────────
|
|
1923
|
+
|
|
1924
|
+
async function checkAuthStatus() {
|
|
1925
|
+
try {
|
|
1926
|
+
const res = await fetch('/api/status');
|
|
1927
|
+
const data = await res.json();
|
|
1928
|
+
const btn = document.getElementById('btn-enable-auth');
|
|
1929
|
+
if (data.auth && !data.auth.enabled && !authUsername) {
|
|
1930
|
+
btn.classList.remove('hidden');
|
|
1931
|
+
} else {
|
|
1932
|
+
btn.classList.add('hidden');
|
|
1933
|
+
}
|
|
1934
|
+
} catch { /* ignore */ }
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function showEnableAuthDialog() {
|
|
1938
|
+
const username = prompt('Create admin account — Username (2-32 chars, a-z/0-9/_/-):');
|
|
1939
|
+
if (!username) return;
|
|
1940
|
+
const password = prompt('Password (min 4 chars):');
|
|
1941
|
+
if (!password) return;
|
|
1942
|
+
|
|
1943
|
+
fetch('/api/auth/register', {
|
|
1944
|
+
method: 'POST',
|
|
1945
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1946
|
+
body: JSON.stringify({ username, password }),
|
|
1947
|
+
})
|
|
1948
|
+
.then(r => r.json())
|
|
1949
|
+
.then(data => {
|
|
1950
|
+
if (data.error) {
|
|
1951
|
+
alert('Error: ' + data.error);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
// Save token and reconnect with auth
|
|
1955
|
+
authToken = data.token;
|
|
1956
|
+
authUsername = data.username;
|
|
1957
|
+
localStorage.setItem('aicli-auth-token', data.token);
|
|
1958
|
+
localStorage.setItem('aicli-auth-user', data.username);
|
|
1959
|
+
document.getElementById('btn-enable-auth').classList.add('hidden');
|
|
1960
|
+
updateUserMenu();
|
|
1961
|
+
// Reconnect with new token
|
|
1962
|
+
if (ws) ws.close();
|
|
1963
|
+
alert('✓ Auth enabled! User "' + data.username + '" created. Other users can register from the login screen.');
|
|
1964
|
+
})
|
|
1965
|
+
.catch(err => alert('Failed: ' + err.message));
|
|
1966
|
+
}
|
|
@@ -22,6 +22,35 @@
|
|
|
22
22
|
<link rel="stylesheet" href="style.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-base-300 h-screen overflow-hidden">
|
|
25
|
+
|
|
26
|
+
<!-- ── Login / Register Screen (shown when auth required) ──── -->
|
|
27
|
+
<div id="auth-screen" class="auth-screen hidden">
|
|
28
|
+
<div class="auth-card">
|
|
29
|
+
<div class="text-center mb-6">
|
|
30
|
+
<div class="text-4xl mb-2">🤖</div>
|
|
31
|
+
<h1 class="text-2xl font-bold text-primary">ai-cli</h1>
|
|
32
|
+
<p id="auth-subtitle" class="text-sm text-base-content/60 mt-1">Sign in to continue</p>
|
|
33
|
+
</div>
|
|
34
|
+
<div id="auth-error" class="alert alert-error text-sm mb-4 hidden"></div>
|
|
35
|
+
<form id="auth-form" onsubmit="handleAuth(event)">
|
|
36
|
+
<div class="form-control mb-3">
|
|
37
|
+
<label class="label"><span class="label-text">Username</span></label>
|
|
38
|
+
<input id="auth-username" type="text" class="input input-bordered w-full" placeholder="username" autocomplete="username" required minlength="2" maxlength="32">
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-control mb-4">
|
|
41
|
+
<label class="label"><span class="label-text">Password</span></label>
|
|
42
|
+
<input id="auth-password" type="password" class="input input-bordered w-full" placeholder="password" autocomplete="current-password" required minlength="4">
|
|
43
|
+
</div>
|
|
44
|
+
<button id="auth-submit" type="submit" class="btn btn-primary w-full">Sign In</button>
|
|
45
|
+
</form>
|
|
46
|
+
<div class="text-center mt-4">
|
|
47
|
+
<button id="auth-toggle" class="btn btn-ghost btn-sm" onclick="toggleAuthMode()">
|
|
48
|
+
Don't have an account? <span class="text-primary">Register</span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
25
54
|
<div id="app" class="flex flex-col h-screen">
|
|
26
55
|
|
|
27
56
|
<!-- ── Navbar ─────────────────────────────────────── -->
|
|
@@ -53,6 +82,18 @@
|
|
|
53
82
|
<li><a onclick="setTheme('nord')">❄️ Nord</a></li>
|
|
54
83
|
</ul>
|
|
55
84
|
</div>
|
|
85
|
+
<!-- Enable Auth button (shown when no users registered) -->
|
|
86
|
+
<button id="btn-enable-auth" class="btn btn-sm btn-ghost hidden" title="Enable multi-user auth" onclick="showEnableAuthDialog()">👤 Enable Auth</button>
|
|
87
|
+
<!-- User menu (shown when authenticated) -->
|
|
88
|
+
<div id="user-menu" class="dropdown dropdown-end hidden">
|
|
89
|
+
<div tabindex="0" role="button" class="btn btn-sm btn-ghost gap-1">
|
|
90
|
+
<span id="user-display">👤</span>
|
|
91
|
+
</div>
|
|
92
|
+
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-40 p-2 shadow-lg border border-base-content/10">
|
|
93
|
+
<li><a id="user-label" class="text-sm font-semibold pointer-events-none"></a></li>
|
|
94
|
+
<li><a onclick="handleLogout()">🚪 Logout</a></li>
|
|
95
|
+
</ul>
|
|
96
|
+
</div>
|
|
56
97
|
</div>
|
|
57
98
|
</div>
|
|
58
99
|
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
/* ai-cli Web UI — Custom styles (DaisyUI handles base theme) */
|
|
2
2
|
|
|
3
|
+
/* ── Auth Screen ───────────────────────────────────── */
|
|
4
|
+
.auth-screen {
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset: 0;
|
|
7
|
+
z-index: 9999;
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
background: oklch(var(--b3));
|
|
12
|
+
backdrop-filter: blur(8px);
|
|
13
|
+
}
|
|
14
|
+
.auth-card {
|
|
15
|
+
background: oklch(var(--b1));
|
|
16
|
+
border: 1px solid oklch(var(--bc) / 0.1);
|
|
17
|
+
border-radius: var(--rounded-box, 1rem);
|
|
18
|
+
padding: 2rem;
|
|
19
|
+
width: 100%;
|
|
20
|
+
max-width: 380px;
|
|
21
|
+
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25);
|
|
22
|
+
}
|
|
23
|
+
|
|
3
24
|
/* ── Scrollbar ──────────────────────────────────────── */
|
|
4
25
|
#chat-area::-webkit-scrollbar { width: 6px; }
|
|
5
26
|
#chat-area::-webkit-scrollbar-track { background: transparent; }
|