jinzd-ai-cli 0.2.0 → 0.2.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/dist/{chunk-46RYX62R.js → chunk-QXRBUMCB.js} +1 -1
- package/dist/{chunk-PBJ4L7SK.js → chunk-Z3JMFVT5.js} +1 -1
- package/dist/electron-server.js +291 -24
- package/dist/index.js +4 -4
- package/dist/{run-tests-6AZTCLL7.js → run-tests-DYALZXPS.js} +1 -1
- package/dist/{server-PCSHZ5AI.js → server-SRILYYV3.js} +292 -25
- package/dist/web/client/app.js +127 -1
- package/dist/web/client/index.html +39 -0
- package/dist/web/client/style.css +21 -0
- package/package.json +1 -1
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.1";
|
|
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,180 @@ 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
|
+
/** Migrate existing single-user data to a named user account */
|
|
7544
|
+
migrateExistingData(username, password) {
|
|
7545
|
+
const err = this.register(username, password);
|
|
7546
|
+
if (err) return err;
|
|
7547
|
+
const userDir = this.getUserDataDir(username);
|
|
7548
|
+
const globalConfig = join13(this.baseDir, "config.json");
|
|
7549
|
+
if (existsSync18(globalConfig)) {
|
|
7550
|
+
try {
|
|
7551
|
+
const content = readFileSync13(globalConfig, "utf-8");
|
|
7552
|
+
writeFileSync9(join13(userDir, "config.json"), content, "utf-8");
|
|
7553
|
+
} catch {
|
|
7554
|
+
}
|
|
7555
|
+
}
|
|
7556
|
+
const globalMemory = join13(this.baseDir, "memory.md");
|
|
7557
|
+
if (existsSync18(globalMemory)) {
|
|
7558
|
+
try {
|
|
7559
|
+
const content = readFileSync13(globalMemory, "utf-8");
|
|
7560
|
+
writeFileSync9(join13(userDir, "memory.md"), content, "utf-8");
|
|
7561
|
+
} catch {
|
|
7562
|
+
}
|
|
7563
|
+
}
|
|
7564
|
+
const globalHistory = join13(this.baseDir, "history");
|
|
7565
|
+
if (existsSync18(globalHistory)) {
|
|
7566
|
+
try {
|
|
7567
|
+
const files = readdirSync10(globalHistory).filter((f) => f.endsWith(".json"));
|
|
7568
|
+
const userHistory = join13(userDir, "history");
|
|
7569
|
+
for (const f of files) {
|
|
7570
|
+
try {
|
|
7571
|
+
copyFileSync(join13(globalHistory, f), join13(userHistory, f));
|
|
7572
|
+
} catch {
|
|
7573
|
+
}
|
|
7574
|
+
}
|
|
7575
|
+
} catch {
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
return null;
|
|
7579
|
+
}
|
|
7580
|
+
// ── Private methods ────────────────────────────────────────────
|
|
7581
|
+
loadOrCreate() {
|
|
7582
|
+
if (existsSync18(this.usersFile)) {
|
|
7583
|
+
try {
|
|
7584
|
+
return JSON.parse(readFileSync13(this.usersFile, "utf-8"));
|
|
7585
|
+
} catch {
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
const db = {
|
|
7589
|
+
version: 1,
|
|
7590
|
+
secret: randomBytes(32).toString("hex"),
|
|
7591
|
+
users: []
|
|
7592
|
+
};
|
|
7593
|
+
this.saveDB(db);
|
|
7594
|
+
return db;
|
|
7595
|
+
}
|
|
7596
|
+
save() {
|
|
7597
|
+
this.saveDB(this.db);
|
|
7598
|
+
}
|
|
7599
|
+
saveDB(db) {
|
|
7600
|
+
mkdirSync10(this.baseDir, { recursive: true });
|
|
7601
|
+
writeFileSync9(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
|
|
7602
|
+
}
|
|
7603
|
+
hashPassword(password, salt) {
|
|
7604
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
7605
|
+
}
|
|
7606
|
+
createToken(username) {
|
|
7607
|
+
const payload = {
|
|
7608
|
+
username,
|
|
7609
|
+
exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
7610
|
+
};
|
|
7611
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
7612
|
+
const signature = this.sign(payloadB64);
|
|
7613
|
+
return `${payloadB64}.${signature}`;
|
|
7614
|
+
}
|
|
7615
|
+
sign(data) {
|
|
7616
|
+
return createHmac("sha256", this.db.secret).update(data).digest("base64url");
|
|
7617
|
+
}
|
|
7618
|
+
};
|
|
7619
|
+
|
|
7446
7620
|
// src/web/server.ts
|
|
7447
7621
|
function getModuleDir() {
|
|
7448
7622
|
try {
|
|
@@ -7498,8 +7672,8 @@ async function startWebServer(options = {}) {
|
|
|
7498
7672
|
}
|
|
7499
7673
|
}
|
|
7500
7674
|
let skillManager = null;
|
|
7501
|
-
const skillsDir =
|
|
7502
|
-
if (
|
|
7675
|
+
const skillsDir = join14(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
7676
|
+
if (existsSync19(skillsDir)) {
|
|
7503
7677
|
skillManager = new SkillManager(skillsDir);
|
|
7504
7678
|
skillManager.loadSkills();
|
|
7505
7679
|
const count = skillManager.listSkills().length;
|
|
@@ -7519,15 +7693,15 @@ async function startWebServer(options = {}) {
|
|
|
7519
7693
|
const server = createServer(app);
|
|
7520
7694
|
const wss = new WebSocketServer({ server });
|
|
7521
7695
|
const moduleDir = getModuleDir();
|
|
7522
|
-
let clientDir =
|
|
7523
|
-
if (!
|
|
7524
|
-
clientDir =
|
|
7696
|
+
let clientDir = join14(moduleDir, "web", "client");
|
|
7697
|
+
if (!existsSync19(clientDir)) {
|
|
7698
|
+
clientDir = join14(moduleDir, "client");
|
|
7525
7699
|
}
|
|
7526
|
-
if (!
|
|
7527
|
-
clientDir =
|
|
7700
|
+
if (!existsSync19(clientDir)) {
|
|
7701
|
+
clientDir = join14(moduleDir, "..", "..", "src", "web", "client");
|
|
7528
7702
|
}
|
|
7529
|
-
if (!
|
|
7530
|
-
clientDir =
|
|
7703
|
+
if (!existsSync19(clientDir)) {
|
|
7704
|
+
clientDir = join14(process.cwd(), "src", "web", "client");
|
|
7531
7705
|
}
|
|
7532
7706
|
console.log(` Static files: ${clientDir}`);
|
|
7533
7707
|
app.use(express.static(clientDir));
|
|
@@ -7546,17 +7720,17 @@ async function startWebServer(options = {}) {
|
|
|
7546
7720
|
app.get("/api/files", (req, res) => {
|
|
7547
7721
|
const cwd = process.cwd();
|
|
7548
7722
|
const prefix = req.query.prefix || "";
|
|
7549
|
-
const targetDir =
|
|
7723
|
+
const targetDir = join14(cwd, prefix);
|
|
7550
7724
|
if (!resolve5(targetDir).startsWith(resolve5(cwd))) {
|
|
7551
7725
|
res.json({ files: [] });
|
|
7552
7726
|
return;
|
|
7553
7727
|
}
|
|
7554
7728
|
try {
|
|
7555
7729
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
7556
|
-
const entries =
|
|
7730
|
+
const entries = readdirSync11(targetDir, { withFileTypes: true });
|
|
7557
7731
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
7558
7732
|
name: e.name,
|
|
7559
|
-
path: relative3(cwd,
|
|
7733
|
+
path: relative3(cwd, join14(targetDir, e.name)).replace(/\\/g, "/"),
|
|
7560
7734
|
isDir: e.isDirectory()
|
|
7561
7735
|
}));
|
|
7562
7736
|
res.json({ files });
|
|
@@ -7588,7 +7762,7 @@ async function startWebServer(options = {}) {
|
|
|
7588
7762
|
return;
|
|
7589
7763
|
}
|
|
7590
7764
|
const cwd = process.cwd();
|
|
7591
|
-
const fullPath = resolve5(
|
|
7765
|
+
const fullPath = resolve5(join14(cwd, filePath));
|
|
7592
7766
|
if (!fullPath.startsWith(resolve5(cwd))) {
|
|
7593
7767
|
res.json({ error: "Access denied" });
|
|
7594
7768
|
return;
|
|
@@ -7599,26 +7773,76 @@ async function startWebServer(options = {}) {
|
|
|
7599
7773
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
7600
7774
|
return;
|
|
7601
7775
|
}
|
|
7602
|
-
const content =
|
|
7776
|
+
const content = readFileSync14(fullPath, "utf-8");
|
|
7603
7777
|
res.json({ content, size: stat.size });
|
|
7604
7778
|
} catch (err) {
|
|
7605
7779
|
res.json({ error: `Cannot read: ${err.message}` });
|
|
7606
7780
|
}
|
|
7607
7781
|
});
|
|
7782
|
+
const authManager = new AuthManager(config.getConfigDir());
|
|
7783
|
+
if (authManager.isEnabled()) {
|
|
7784
|
+
console.log(` Auth: ${authManager.listUsers().length} user(s) registered`);
|
|
7785
|
+
} else {
|
|
7786
|
+
console.log(` Auth: disabled (no users registered)`);
|
|
7787
|
+
}
|
|
7788
|
+
const userResources = /* @__PURE__ */ new Map();
|
|
7789
|
+
function getUserShared(username) {
|
|
7790
|
+
const cached = userResources.get(username);
|
|
7791
|
+
if (cached) return cached;
|
|
7792
|
+
const userDataDir = authManager.getUserDataDir(username);
|
|
7793
|
+
const userConfig = new ConfigManager(userDataDir);
|
|
7794
|
+
const userSessions = new SessionManager(userConfig);
|
|
7795
|
+
const userShared = {
|
|
7796
|
+
config: userConfig,
|
|
7797
|
+
providers,
|
|
7798
|
+
// Shared — providers re-read API keys per call
|
|
7799
|
+
sessions: userSessions,
|
|
7800
|
+
toolRegistry,
|
|
7801
|
+
// Shared
|
|
7802
|
+
mcpManager,
|
|
7803
|
+
// Shared
|
|
7804
|
+
skillManager
|
|
7805
|
+
// Shared
|
|
7806
|
+
};
|
|
7807
|
+
userResources.set(username, userShared);
|
|
7808
|
+
return userShared;
|
|
7809
|
+
}
|
|
7608
7810
|
const handlers = /* @__PURE__ */ new Map();
|
|
7609
7811
|
wss.on("connection", (ws, req) => {
|
|
7610
7812
|
const urlStr = req.url ?? "";
|
|
7611
7813
|
const qMark = urlStr.indexOf("?");
|
|
7612
7814
|
const params = new URLSearchParams(qMark >= 0 ? urlStr.slice(qMark + 1) : "");
|
|
7613
7815
|
const tabId = params.get("tabId") || `tab-${Date.now()}`;
|
|
7816
|
+
const token = params.get("token") || "";
|
|
7614
7817
|
const existing = handlers.get(tabId);
|
|
7615
7818
|
if (existing) {
|
|
7616
7819
|
existing.onDisconnect();
|
|
7617
7820
|
handlers.delete(tabId);
|
|
7618
7821
|
}
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7822
|
+
let authenticatedUser = null;
|
|
7823
|
+
let handler = null;
|
|
7824
|
+
if (token) {
|
|
7825
|
+
authenticatedUser = authManager.verifyToken(token);
|
|
7826
|
+
}
|
|
7827
|
+
const authRequired = authManager.isEnabled();
|
|
7828
|
+
if (!authRequired) {
|
|
7829
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
|
|
7830
|
+
handler = new SessionHandler(ws, shared);
|
|
7831
|
+
handlers.set(tabId, handler);
|
|
7832
|
+
} else if (authenticatedUser) {
|
|
7833
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
|
|
7834
|
+
const userShared = getUserShared(authenticatedUser);
|
|
7835
|
+
handler = new SessionHandler(ws, userShared);
|
|
7836
|
+
handlers.set(tabId, handler);
|
|
7837
|
+
} else {
|
|
7838
|
+
console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
|
|
7839
|
+
if (ws.readyState === ws.OPEN) {
|
|
7840
|
+
ws.send(JSON.stringify({
|
|
7841
|
+
type: "auth_required",
|
|
7842
|
+
hasUsers: authManager.hasUsers()
|
|
7843
|
+
}));
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7622
7846
|
ws.on("message", async (data) => {
|
|
7623
7847
|
try {
|
|
7624
7848
|
const raw = data.toString();
|
|
@@ -7629,6 +7853,49 @@ async function startWebServer(options = {}) {
|
|
|
7629
7853
|
}
|
|
7630
7854
|
return;
|
|
7631
7855
|
}
|
|
7856
|
+
if (parsed.type === "auth") {
|
|
7857
|
+
const { action, username, password } = parsed;
|
|
7858
|
+
if (action === "register") {
|
|
7859
|
+
const err = authManager.register(username, password);
|
|
7860
|
+
if (err) {
|
|
7861
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
7864
|
+
const newToken = authManager.login(username, password);
|
|
7865
|
+
authenticatedUser = username;
|
|
7866
|
+
const userShared = getUserShared(username);
|
|
7867
|
+
handler = new SessionHandler(ws, userShared);
|
|
7868
|
+
handlers.set(tabId, handler);
|
|
7869
|
+
console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
7870
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
|
|
7871
|
+
return;
|
|
7872
|
+
}
|
|
7873
|
+
if (action === "login") {
|
|
7874
|
+
const loginToken = authManager.login(username, password);
|
|
7875
|
+
if (!loginToken) {
|
|
7876
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
|
|
7877
|
+
return;
|
|
7878
|
+
}
|
|
7879
|
+
authenticatedUser = username;
|
|
7880
|
+
const userShared = getUserShared(username);
|
|
7881
|
+
handler = new SessionHandler(ws, userShared);
|
|
7882
|
+
handlers.set(tabId, handler);
|
|
7883
|
+
console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
7884
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
|
|
7885
|
+
return;
|
|
7886
|
+
}
|
|
7887
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
|
|
7888
|
+
return;
|
|
7889
|
+
}
|
|
7890
|
+
if (!handler) {
|
|
7891
|
+
if (ws.readyState === ws.OPEN) {
|
|
7892
|
+
ws.send(JSON.stringify({
|
|
7893
|
+
type: "auth_required",
|
|
7894
|
+
hasUsers: authManager.hasUsers()
|
|
7895
|
+
}));
|
|
7896
|
+
}
|
|
7897
|
+
return;
|
|
7898
|
+
}
|
|
7632
7899
|
await handler.handleMessage(raw);
|
|
7633
7900
|
} catch (err) {
|
|
7634
7901
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7640,7 +7907,7 @@ async function startWebServer(options = {}) {
|
|
|
7640
7907
|
});
|
|
7641
7908
|
ws.on("close", () => {
|
|
7642
7909
|
console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
|
|
7643
|
-
handler.onDisconnect();
|
|
7910
|
+
if (handler) handler.onDisconnect();
|
|
7644
7911
|
handlers.delete(tabId);
|
|
7645
7912
|
});
|
|
7646
7913
|
ws.on("error", (err) => {
|
|
@@ -7700,10 +7967,10 @@ function loadProjectMcpConfig() {
|
|
|
7700
7967
|
const cwd = process.cwd();
|
|
7701
7968
|
const gitRoot = getGitRoot(cwd);
|
|
7702
7969
|
const projectRoot = gitRoot ?? cwd;
|
|
7703
|
-
const configPath =
|
|
7704
|
-
if (!
|
|
7970
|
+
const configPath = join14(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
7971
|
+
if (!existsSync19(configPath)) return null;
|
|
7705
7972
|
try {
|
|
7706
|
-
const raw = JSON.parse(
|
|
7973
|
+
const raw = JSON.parse(readFileSync14(configPath, "utf-8"));
|
|
7707
7974
|
return raw.mcpServers ?? raw;
|
|
7708
7975
|
} catch {
|
|
7709
7976
|
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-Z3JMFVT5.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-QXRBUMCB.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-DYALZXPS.js");
|
|
1908
1908
|
const argStr = args.join(" ").trim();
|
|
1909
1909
|
let testArgs = {};
|
|
1910
1910
|
if (argStr) {
|
|
@@ -5292,7 +5292,7 @@ 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-SRILYYV3.js");
|
|
5296
5296
|
await startWebServer({ port, host: options.host });
|
|
5297
5297
|
});
|
|
5298
5298
|
program.command("sessions").description("List recent conversation sessions").action(async () => {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setupProxy,
|
|
24
24
|
spawnAgentContext,
|
|
25
25
|
truncateOutput
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-Z3JMFVT5.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
29
29
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -35,14 +35,14 @@ import {
|
|
|
35
35
|
PLAN_MODE_SYSTEM_ADDON,
|
|
36
36
|
SKILLS_DIR_NAME,
|
|
37
37
|
VERSION
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-QXRBUMCB.js";
|
|
39
39
|
|
|
40
40
|
// src/web/server.ts
|
|
41
41
|
import express from "express";
|
|
42
42
|
import { createServer } from "http";
|
|
43
43
|
import { WebSocketServer } from "ws";
|
|
44
|
-
import { join as
|
|
45
|
-
import { existsSync as
|
|
44
|
+
import { join as join4, dirname, resolve as resolve2, relative } from "path";
|
|
45
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
|
|
46
46
|
import { networkInterfaces } from "os";
|
|
47
47
|
|
|
48
48
|
// src/web/tool-executor-web.ts
|
|
@@ -1353,6 +1353,180 @@ ${activated.meta.description || ""}` });
|
|
|
1353
1353
|
}
|
|
1354
1354
|
};
|
|
1355
1355
|
|
|
1356
|
+
// src/web/auth.ts
|
|
1357
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync, copyFileSync } from "fs";
|
|
1358
|
+
import { join as join3 } from "path";
|
|
1359
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1360
|
+
var USERS_FILE = "users.json";
|
|
1361
|
+
var TOKEN_EXPIRY_HOURS = 24 * 7;
|
|
1362
|
+
var USERS_DIR = "users";
|
|
1363
|
+
var AuthManager = class {
|
|
1364
|
+
usersFile;
|
|
1365
|
+
baseDir;
|
|
1366
|
+
db;
|
|
1367
|
+
constructor(baseDir) {
|
|
1368
|
+
this.baseDir = baseDir;
|
|
1369
|
+
this.usersFile = join3(baseDir, USERS_FILE);
|
|
1370
|
+
this.db = this.loadOrCreate();
|
|
1371
|
+
}
|
|
1372
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
1373
|
+
/** Check if any users exist (first-run detection) */
|
|
1374
|
+
hasUsers() {
|
|
1375
|
+
return this.db.users.length > 0;
|
|
1376
|
+
}
|
|
1377
|
+
/** Check if auth is enabled (has users.json with at least 1 user) */
|
|
1378
|
+
isEnabled() {
|
|
1379
|
+
return this.hasUsers();
|
|
1380
|
+
}
|
|
1381
|
+
/** Register a new user. Returns error string or null on success. */
|
|
1382
|
+
register(username, password) {
|
|
1383
|
+
username = username.trim().toLowerCase();
|
|
1384
|
+
if (!username || username.length < 2 || username.length > 32) {
|
|
1385
|
+
return "Username must be 2-32 characters";
|
|
1386
|
+
}
|
|
1387
|
+
if (!/^[a-z0-9_-]+$/.test(username)) {
|
|
1388
|
+
return "Username can only contain a-z, 0-9, _ and -";
|
|
1389
|
+
}
|
|
1390
|
+
if (!password || password.length < 4) {
|
|
1391
|
+
return "Password must be at least 4 characters";
|
|
1392
|
+
}
|
|
1393
|
+
if (this.db.users.find((u) => u.username === username)) {
|
|
1394
|
+
return "Username already exists";
|
|
1395
|
+
}
|
|
1396
|
+
const salt = randomBytes(16).toString("hex");
|
|
1397
|
+
const passwordHash = this.hashPassword(password, salt);
|
|
1398
|
+
const dataDir = join3(USERS_DIR, username);
|
|
1399
|
+
const fullDataDir = join3(this.baseDir, dataDir);
|
|
1400
|
+
mkdirSync2(join3(fullDataDir, "history"), { recursive: true });
|
|
1401
|
+
const user = {
|
|
1402
|
+
username,
|
|
1403
|
+
passwordHash,
|
|
1404
|
+
salt,
|
|
1405
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1406
|
+
dataDir
|
|
1407
|
+
};
|
|
1408
|
+
this.db.users.push(user);
|
|
1409
|
+
this.save();
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
/** Authenticate user. Returns JWT token or null. */
|
|
1413
|
+
login(username, password) {
|
|
1414
|
+
username = username.trim().toLowerCase();
|
|
1415
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
1416
|
+
if (!user) return null;
|
|
1417
|
+
const hash = this.hashPassword(password, user.salt);
|
|
1418
|
+
const a = Buffer.from(hash, "utf-8");
|
|
1419
|
+
const b = Buffer.from(user.passwordHash, "utf-8");
|
|
1420
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
return this.createToken(username);
|
|
1424
|
+
}
|
|
1425
|
+
/** Verify a token. Returns username or null. */
|
|
1426
|
+
verifyToken(token) {
|
|
1427
|
+
try {
|
|
1428
|
+
const parts = token.split(".");
|
|
1429
|
+
if (parts.length !== 2) return null;
|
|
1430
|
+
const [payloadB64, signature] = parts;
|
|
1431
|
+
const expectedSig = this.sign(payloadB64);
|
|
1432
|
+
if (signature !== expectedSig) return null;
|
|
1433
|
+
const payload = JSON.parse(
|
|
1434
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
1435
|
+
);
|
|
1436
|
+
if (Date.now() > payload.exp) return null;
|
|
1437
|
+
if (!this.db.users.find((u) => u.username === payload.username)) return null;
|
|
1438
|
+
return payload.username;
|
|
1439
|
+
} catch {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/** Get user's data directory (absolute path) */
|
|
1444
|
+
getUserDataDir(username) {
|
|
1445
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
1446
|
+
if (!user) throw new Error(`User not found: ${username}`);
|
|
1447
|
+
return join3(this.baseDir, user.dataDir);
|
|
1448
|
+
}
|
|
1449
|
+
/** List all usernames */
|
|
1450
|
+
listUsers() {
|
|
1451
|
+
return this.db.users.map((u) => u.username);
|
|
1452
|
+
}
|
|
1453
|
+
/** Migrate existing single-user data to a named user account */
|
|
1454
|
+
migrateExistingData(username, password) {
|
|
1455
|
+
const err = this.register(username, password);
|
|
1456
|
+
if (err) return err;
|
|
1457
|
+
const userDir = this.getUserDataDir(username);
|
|
1458
|
+
const globalConfig = join3(this.baseDir, "config.json");
|
|
1459
|
+
if (existsSync4(globalConfig)) {
|
|
1460
|
+
try {
|
|
1461
|
+
const content = readFileSync4(globalConfig, "utf-8");
|
|
1462
|
+
writeFileSync2(join3(userDir, "config.json"), content, "utf-8");
|
|
1463
|
+
} catch {
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const globalMemory = join3(this.baseDir, "memory.md");
|
|
1467
|
+
if (existsSync4(globalMemory)) {
|
|
1468
|
+
try {
|
|
1469
|
+
const content = readFileSync4(globalMemory, "utf-8");
|
|
1470
|
+
writeFileSync2(join3(userDir, "memory.md"), content, "utf-8");
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const globalHistory = join3(this.baseDir, "history");
|
|
1475
|
+
if (existsSync4(globalHistory)) {
|
|
1476
|
+
try {
|
|
1477
|
+
const files = readdirSync(globalHistory).filter((f) => f.endsWith(".json"));
|
|
1478
|
+
const userHistory = join3(userDir, "history");
|
|
1479
|
+
for (const f of files) {
|
|
1480
|
+
try {
|
|
1481
|
+
copyFileSync(join3(globalHistory, f), join3(userHistory, f));
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return null;
|
|
1489
|
+
}
|
|
1490
|
+
// ── Private methods ────────────────────────────────────────────
|
|
1491
|
+
loadOrCreate() {
|
|
1492
|
+
if (existsSync4(this.usersFile)) {
|
|
1493
|
+
try {
|
|
1494
|
+
return JSON.parse(readFileSync4(this.usersFile, "utf-8"));
|
|
1495
|
+
} catch {
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const db = {
|
|
1499
|
+
version: 1,
|
|
1500
|
+
secret: randomBytes(32).toString("hex"),
|
|
1501
|
+
users: []
|
|
1502
|
+
};
|
|
1503
|
+
this.saveDB(db);
|
|
1504
|
+
return db;
|
|
1505
|
+
}
|
|
1506
|
+
save() {
|
|
1507
|
+
this.saveDB(this.db);
|
|
1508
|
+
}
|
|
1509
|
+
saveDB(db) {
|
|
1510
|
+
mkdirSync2(this.baseDir, { recursive: true });
|
|
1511
|
+
writeFileSync2(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
|
|
1512
|
+
}
|
|
1513
|
+
hashPassword(password, salt) {
|
|
1514
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
1515
|
+
}
|
|
1516
|
+
createToken(username) {
|
|
1517
|
+
const payload = {
|
|
1518
|
+
username,
|
|
1519
|
+
exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
1520
|
+
};
|
|
1521
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
1522
|
+
const signature = this.sign(payloadB64);
|
|
1523
|
+
return `${payloadB64}.${signature}`;
|
|
1524
|
+
}
|
|
1525
|
+
sign(data) {
|
|
1526
|
+
return createHmac("sha256", this.db.secret).update(data).digest("base64url");
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1356
1530
|
// src/web/server.ts
|
|
1357
1531
|
function getModuleDir() {
|
|
1358
1532
|
try {
|
|
@@ -1408,8 +1582,8 @@ async function startWebServer(options = {}) {
|
|
|
1408
1582
|
}
|
|
1409
1583
|
}
|
|
1410
1584
|
let skillManager = null;
|
|
1411
|
-
const skillsDir =
|
|
1412
|
-
if (
|
|
1585
|
+
const skillsDir = join4(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
1586
|
+
if (existsSync5(skillsDir)) {
|
|
1413
1587
|
skillManager = new SkillManager(skillsDir);
|
|
1414
1588
|
skillManager.loadSkills();
|
|
1415
1589
|
const count = skillManager.listSkills().length;
|
|
@@ -1429,15 +1603,15 @@ async function startWebServer(options = {}) {
|
|
|
1429
1603
|
const server = createServer(app);
|
|
1430
1604
|
const wss = new WebSocketServer({ server });
|
|
1431
1605
|
const moduleDir = getModuleDir();
|
|
1432
|
-
let clientDir =
|
|
1433
|
-
if (!
|
|
1434
|
-
clientDir =
|
|
1606
|
+
let clientDir = join4(moduleDir, "web", "client");
|
|
1607
|
+
if (!existsSync5(clientDir)) {
|
|
1608
|
+
clientDir = join4(moduleDir, "client");
|
|
1435
1609
|
}
|
|
1436
|
-
if (!
|
|
1437
|
-
clientDir =
|
|
1610
|
+
if (!existsSync5(clientDir)) {
|
|
1611
|
+
clientDir = join4(moduleDir, "..", "..", "src", "web", "client");
|
|
1438
1612
|
}
|
|
1439
|
-
if (!
|
|
1440
|
-
clientDir =
|
|
1613
|
+
if (!existsSync5(clientDir)) {
|
|
1614
|
+
clientDir = join4(process.cwd(), "src", "web", "client");
|
|
1441
1615
|
}
|
|
1442
1616
|
console.log(` Static files: ${clientDir}`);
|
|
1443
1617
|
app.use(express.static(clientDir));
|
|
@@ -1456,17 +1630,17 @@ async function startWebServer(options = {}) {
|
|
|
1456
1630
|
app.get("/api/files", (req, res) => {
|
|
1457
1631
|
const cwd = process.cwd();
|
|
1458
1632
|
const prefix = req.query.prefix || "";
|
|
1459
|
-
const targetDir =
|
|
1633
|
+
const targetDir = join4(cwd, prefix);
|
|
1460
1634
|
if (!resolve2(targetDir).startsWith(resolve2(cwd))) {
|
|
1461
1635
|
res.json({ files: [] });
|
|
1462
1636
|
return;
|
|
1463
1637
|
}
|
|
1464
1638
|
try {
|
|
1465
1639
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
1466
|
-
const entries =
|
|
1640
|
+
const entries = readdirSync2(targetDir, { withFileTypes: true });
|
|
1467
1641
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
1468
1642
|
name: e.name,
|
|
1469
|
-
path: relative(cwd,
|
|
1643
|
+
path: relative(cwd, join4(targetDir, e.name)).replace(/\\/g, "/"),
|
|
1470
1644
|
isDir: e.isDirectory()
|
|
1471
1645
|
}));
|
|
1472
1646
|
res.json({ files });
|
|
@@ -1498,7 +1672,7 @@ async function startWebServer(options = {}) {
|
|
|
1498
1672
|
return;
|
|
1499
1673
|
}
|
|
1500
1674
|
const cwd = process.cwd();
|
|
1501
|
-
const fullPath = resolve2(
|
|
1675
|
+
const fullPath = resolve2(join4(cwd, filePath));
|
|
1502
1676
|
if (!fullPath.startsWith(resolve2(cwd))) {
|
|
1503
1677
|
res.json({ error: "Access denied" });
|
|
1504
1678
|
return;
|
|
@@ -1509,26 +1683,76 @@ async function startWebServer(options = {}) {
|
|
|
1509
1683
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
1510
1684
|
return;
|
|
1511
1685
|
}
|
|
1512
|
-
const content =
|
|
1686
|
+
const content = readFileSync5(fullPath, "utf-8");
|
|
1513
1687
|
res.json({ content, size: stat.size });
|
|
1514
1688
|
} catch (err) {
|
|
1515
1689
|
res.json({ error: `Cannot read: ${err.message}` });
|
|
1516
1690
|
}
|
|
1517
1691
|
});
|
|
1692
|
+
const authManager = new AuthManager(config.getConfigDir());
|
|
1693
|
+
if (authManager.isEnabled()) {
|
|
1694
|
+
console.log(` Auth: ${authManager.listUsers().length} user(s) registered`);
|
|
1695
|
+
} else {
|
|
1696
|
+
console.log(` Auth: disabled (no users registered)`);
|
|
1697
|
+
}
|
|
1698
|
+
const userResources = /* @__PURE__ */ new Map();
|
|
1699
|
+
function getUserShared(username) {
|
|
1700
|
+
const cached = userResources.get(username);
|
|
1701
|
+
if (cached) return cached;
|
|
1702
|
+
const userDataDir = authManager.getUserDataDir(username);
|
|
1703
|
+
const userConfig = new ConfigManager(userDataDir);
|
|
1704
|
+
const userSessions = new SessionManager(userConfig);
|
|
1705
|
+
const userShared = {
|
|
1706
|
+
config: userConfig,
|
|
1707
|
+
providers,
|
|
1708
|
+
// Shared — providers re-read API keys per call
|
|
1709
|
+
sessions: userSessions,
|
|
1710
|
+
toolRegistry,
|
|
1711
|
+
// Shared
|
|
1712
|
+
mcpManager,
|
|
1713
|
+
// Shared
|
|
1714
|
+
skillManager
|
|
1715
|
+
// Shared
|
|
1716
|
+
};
|
|
1717
|
+
userResources.set(username, userShared);
|
|
1718
|
+
return userShared;
|
|
1719
|
+
}
|
|
1518
1720
|
const handlers = /* @__PURE__ */ new Map();
|
|
1519
1721
|
wss.on("connection", (ws, req) => {
|
|
1520
1722
|
const urlStr = req.url ?? "";
|
|
1521
1723
|
const qMark = urlStr.indexOf("?");
|
|
1522
1724
|
const params = new URLSearchParams(qMark >= 0 ? urlStr.slice(qMark + 1) : "");
|
|
1523
1725
|
const tabId = params.get("tabId") || `tab-${Date.now()}`;
|
|
1726
|
+
const token = params.get("token") || "";
|
|
1524
1727
|
const existing = handlers.get(tabId);
|
|
1525
1728
|
if (existing) {
|
|
1526
1729
|
existing.onDisconnect();
|
|
1527
1730
|
handlers.delete(tabId);
|
|
1528
1731
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1732
|
+
let authenticatedUser = null;
|
|
1733
|
+
let handler = null;
|
|
1734
|
+
if (token) {
|
|
1735
|
+
authenticatedUser = authManager.verifyToken(token);
|
|
1736
|
+
}
|
|
1737
|
+
const authRequired = authManager.isEnabled();
|
|
1738
|
+
if (!authRequired) {
|
|
1739
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (no auth) (total: ${handlers.size + 1})`);
|
|
1740
|
+
handler = new SessionHandler(ws, shared);
|
|
1741
|
+
handlers.set(tabId, handler);
|
|
1742
|
+
} else if (authenticatedUser) {
|
|
1743
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (user: ${authenticatedUser}) (total: ${handlers.size + 1})`);
|
|
1744
|
+
const userShared = getUserShared(authenticatedUser);
|
|
1745
|
+
handler = new SessionHandler(ws, userShared);
|
|
1746
|
+
handlers.set(tabId, handler);
|
|
1747
|
+
} else {
|
|
1748
|
+
console.log(` \u23F3 Tab waiting auth: ${tabId.slice(0, 12)}`);
|
|
1749
|
+
if (ws.readyState === ws.OPEN) {
|
|
1750
|
+
ws.send(JSON.stringify({
|
|
1751
|
+
type: "auth_required",
|
|
1752
|
+
hasUsers: authManager.hasUsers()
|
|
1753
|
+
}));
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1532
1756
|
ws.on("message", async (data) => {
|
|
1533
1757
|
try {
|
|
1534
1758
|
const raw = data.toString();
|
|
@@ -1539,6 +1763,49 @@ async function startWebServer(options = {}) {
|
|
|
1539
1763
|
}
|
|
1540
1764
|
return;
|
|
1541
1765
|
}
|
|
1766
|
+
if (parsed.type === "auth") {
|
|
1767
|
+
const { action, username, password } = parsed;
|
|
1768
|
+
if (action === "register") {
|
|
1769
|
+
const err = authManager.register(username, password);
|
|
1770
|
+
if (err) {
|
|
1771
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: err }));
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const newToken = authManager.login(username, password);
|
|
1775
|
+
authenticatedUser = username;
|
|
1776
|
+
const userShared = getUserShared(username);
|
|
1777
|
+
handler = new SessionHandler(ws, userShared);
|
|
1778
|
+
handlers.set(tabId, handler);
|
|
1779
|
+
console.log(` \u2713 User registered & connected: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
1780
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: newToken, username }));
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (action === "login") {
|
|
1784
|
+
const loginToken = authManager.login(username, password);
|
|
1785
|
+
if (!loginToken) {
|
|
1786
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Invalid username or password" }));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
authenticatedUser = username;
|
|
1790
|
+
const userShared = getUserShared(username);
|
|
1791
|
+
handler = new SessionHandler(ws, userShared);
|
|
1792
|
+
handlers.set(tabId, handler);
|
|
1793
|
+
console.log(` \u2713 User logged in: ${username} (tab: ${tabId.slice(0, 12)})`);
|
|
1794
|
+
ws.send(JSON.stringify({ type: "auth_result", success: true, token: loginToken, username }));
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
ws.send(JSON.stringify({ type: "auth_result", success: false, error: "Unknown auth action" }));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (!handler) {
|
|
1801
|
+
if (ws.readyState === ws.OPEN) {
|
|
1802
|
+
ws.send(JSON.stringify({
|
|
1803
|
+
type: "auth_required",
|
|
1804
|
+
hasUsers: authManager.hasUsers()
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1542
1809
|
await handler.handleMessage(raw);
|
|
1543
1810
|
} catch (err) {
|
|
1544
1811
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1550,7 +1817,7 @@ async function startWebServer(options = {}) {
|
|
|
1550
1817
|
});
|
|
1551
1818
|
ws.on("close", () => {
|
|
1552
1819
|
console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
|
|
1553
|
-
handler.onDisconnect();
|
|
1820
|
+
if (handler) handler.onDisconnect();
|
|
1554
1821
|
handlers.delete(tabId);
|
|
1555
1822
|
});
|
|
1556
1823
|
ws.on("error", (err) => {
|
|
@@ -1610,10 +1877,10 @@ function loadProjectMcpConfig() {
|
|
|
1610
1877
|
const cwd = process.cwd();
|
|
1611
1878
|
const gitRoot = getGitRoot(cwd);
|
|
1612
1879
|
const projectRoot = gitRoot ?? cwd;
|
|
1613
|
-
const configPath =
|
|
1614
|
-
if (!
|
|
1880
|
+
const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
1881
|
+
if (!existsSync5(configPath)) return null;
|
|
1615
1882
|
try {
|
|
1616
|
-
const raw = JSON.parse(
|
|
1883
|
+
const raw = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1617
1884
|
return raw.mcpServers ?? raw;
|
|
1618
1885
|
} catch {
|
|
1619
1886
|
return null;
|
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;
|
|
@@ -153,6 +157,8 @@ function handleServerMessage(msg) {
|
|
|
153
157
|
case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
|
|
154
158
|
case 'export_data': handleExportData(msg); break;
|
|
155
159
|
case 'memory_content': handleMemoryContent(msg); break;
|
|
160
|
+
case 'auth_required': showAuthScreen(msg.hasUsers); break;
|
|
161
|
+
case 'auth_result': handleAuthResult(msg); break;
|
|
156
162
|
case 'info': addInfoMessage(msg.message); break;
|
|
157
163
|
case 'error': addErrorMessage(msg.message); hideRoundProgress(); clearAllToolTimers(); setProcessing(false); break;
|
|
158
164
|
case 'round_progress': handleRoundProgress(msg); break;
|
|
@@ -1791,3 +1797,123 @@ if (sessionListEl) {
|
|
|
1791
1797
|
}
|
|
1792
1798
|
});
|
|
1793
1799
|
}
|
|
1800
|
+
|
|
1801
|
+
// ── Auth: Login / Register / Logout ─────────────────────────────────
|
|
1802
|
+
|
|
1803
|
+
function showAuthScreen(hasUsers) {
|
|
1804
|
+
// Clear any stale token
|
|
1805
|
+
authToken = '';
|
|
1806
|
+
authUsername = '';
|
|
1807
|
+
localStorage.removeItem('aicli-auth-token');
|
|
1808
|
+
localStorage.removeItem('aicli-auth-user');
|
|
1809
|
+
|
|
1810
|
+
const screen = document.getElementById('auth-screen');
|
|
1811
|
+
const app = document.getElementById('app');
|
|
1812
|
+
screen.classList.remove('hidden');
|
|
1813
|
+
app.style.display = 'none';
|
|
1814
|
+
|
|
1815
|
+
// If no users registered, show register form by default
|
|
1816
|
+
if (!hasUsers) {
|
|
1817
|
+
authMode = 'register';
|
|
1818
|
+
updateAuthForm();
|
|
1819
|
+
} else {
|
|
1820
|
+
authMode = 'login';
|
|
1821
|
+
updateAuthForm();
|
|
1822
|
+
}
|
|
1823
|
+
// Focus username
|
|
1824
|
+
setTimeout(() => document.getElementById('auth-username')?.focus(), 100);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function hideAuthScreen() {
|
|
1828
|
+
const screen = document.getElementById('auth-screen');
|
|
1829
|
+
const app = document.getElementById('app');
|
|
1830
|
+
screen.classList.add('hidden');
|
|
1831
|
+
app.style.display = '';
|
|
1832
|
+
// Show user menu
|
|
1833
|
+
updateUserMenu();
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function updateAuthForm() {
|
|
1837
|
+
const subtitle = document.getElementById('auth-subtitle');
|
|
1838
|
+
const submit = document.getElementById('auth-submit');
|
|
1839
|
+
const toggle = document.getElementById('auth-toggle');
|
|
1840
|
+
const errorEl = document.getElementById('auth-error');
|
|
1841
|
+
|
|
1842
|
+
errorEl.classList.add('hidden');
|
|
1843
|
+
|
|
1844
|
+
if (authMode === 'register') {
|
|
1845
|
+
subtitle.textContent = 'Create your account to get started';
|
|
1846
|
+
submit.textContent = 'Create Account';
|
|
1847
|
+
toggle.innerHTML = 'Already have an account? <span class="text-primary">Sign In</span>';
|
|
1848
|
+
} else {
|
|
1849
|
+
subtitle.textContent = 'Sign in to continue';
|
|
1850
|
+
submit.textContent = 'Sign In';
|
|
1851
|
+
toggle.innerHTML = 'Don\'t have an account? <span class="text-primary">Register</span>';
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function toggleAuthMode() {
|
|
1856
|
+
authMode = authMode === 'login' ? 'register' : 'login';
|
|
1857
|
+
updateAuthForm();
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function handleAuth(event) {
|
|
1861
|
+
event.preventDefault();
|
|
1862
|
+
const username = document.getElementById('auth-username').value.trim();
|
|
1863
|
+
const password = document.getElementById('auth-password').value;
|
|
1864
|
+
const errorEl = document.getElementById('auth-error');
|
|
1865
|
+
|
|
1866
|
+
if (!username || !password) {
|
|
1867
|
+
errorEl.textContent = 'Please enter username and password';
|
|
1868
|
+
errorEl.classList.remove('hidden');
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
errorEl.classList.add('hidden');
|
|
1873
|
+
document.getElementById('auth-submit').disabled = true;
|
|
1874
|
+
|
|
1875
|
+
send({ type: 'auth', action: authMode, username, password });
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function handleAuthResult(msg) {
|
|
1879
|
+
const errorEl = document.getElementById('auth-error');
|
|
1880
|
+
const submitBtn = document.getElementById('auth-submit');
|
|
1881
|
+
submitBtn.disabled = false;
|
|
1882
|
+
|
|
1883
|
+
if (msg.success) {
|
|
1884
|
+
authToken = msg.token;
|
|
1885
|
+
authUsername = msg.username;
|
|
1886
|
+
localStorage.setItem('aicli-auth-token', msg.token);
|
|
1887
|
+
localStorage.setItem('aicli-auth-user', msg.username);
|
|
1888
|
+
hideAuthScreen();
|
|
1889
|
+
// Request session list now that we're authenticated
|
|
1890
|
+
requestSessionList();
|
|
1891
|
+
} else {
|
|
1892
|
+
errorEl.textContent = msg.error || 'Authentication failed';
|
|
1893
|
+
errorEl.classList.remove('hidden');
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function updateUserMenu() {
|
|
1898
|
+
const userMenu = document.getElementById('user-menu');
|
|
1899
|
+
const userDisplay = document.getElementById('user-display');
|
|
1900
|
+
const userLabel = document.getElementById('user-label');
|
|
1901
|
+
|
|
1902
|
+
if (authUsername) {
|
|
1903
|
+
userMenu.classList.remove('hidden');
|
|
1904
|
+
userDisplay.textContent = '👤 ' + authUsername;
|
|
1905
|
+
userLabel.textContent = authUsername;
|
|
1906
|
+
} else {
|
|
1907
|
+
userMenu.classList.add('hidden');
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function handleLogout() {
|
|
1912
|
+
authToken = '';
|
|
1913
|
+
authUsername = '';
|
|
1914
|
+
localStorage.removeItem('aicli-auth-token');
|
|
1915
|
+
localStorage.removeItem('aicli-auth-user');
|
|
1916
|
+
sessionStorage.removeItem('aicli-active-session');
|
|
1917
|
+
// Reconnect — server will send auth_required
|
|
1918
|
+
if (ws) ws.close();
|
|
1919
|
+
}
|
|
@@ -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,16 @@
|
|
|
53
82
|
<li><a onclick="setTheme('nord')">❄️ Nord</a></li>
|
|
54
83
|
</ul>
|
|
55
84
|
</div>
|
|
85
|
+
<!-- User menu (shown when authenticated) -->
|
|
86
|
+
<div id="user-menu" class="dropdown dropdown-end hidden">
|
|
87
|
+
<div tabindex="0" role="button" class="btn btn-sm btn-ghost gap-1">
|
|
88
|
+
<span id="user-display">👤</span>
|
|
89
|
+
</div>
|
|
90
|
+
<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">
|
|
91
|
+
<li><a id="user-label" class="text-sm font-semibold pointer-events-none"></a></li>
|
|
92
|
+
<li><a onclick="handleLogout()">🚪 Logout</a></li>
|
|
93
|
+
</ul>
|
|
94
|
+
</div>
|
|
56
95
|
</div>
|
|
57
96
|
</div>
|
|
58
97
|
|
|
@@ -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; }
|