jinzd-ai-cli 0.1.99 → 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.
Binary file
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.1.99";
11
+ var VERSION = "0.2.1";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-IV3LYGTI.js";
19
+ } from "./chunk-QXRBUMCB.js";
20
20
 
21
21
  // src/config/config-manager.ts
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -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 join13, dirname as dirname4, resolve as resolve5, relative as relative3 } from "path";
6
- import { existsSync as existsSync18, readFileSync as readFileSync13, readdirSync as readdirSync10, statSync as statSync8 } from "fs";
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.1.99";
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 = join13(config.getConfigDir(), SKILLS_DIR_NAME);
7502
- if (existsSync18(skillsDir)) {
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 = join13(moduleDir, "web", "client");
7523
- if (!existsSync18(clientDir)) {
7524
- clientDir = join13(moduleDir, "client");
7696
+ let clientDir = join14(moduleDir, "web", "client");
7697
+ if (!existsSync19(clientDir)) {
7698
+ clientDir = join14(moduleDir, "client");
7525
7699
  }
7526
- if (!existsSync18(clientDir)) {
7527
- clientDir = join13(moduleDir, "..", "..", "src", "web", "client");
7700
+ if (!existsSync19(clientDir)) {
7701
+ clientDir = join14(moduleDir, "..", "..", "src", "web", "client");
7528
7702
  }
7529
- if (!existsSync18(clientDir)) {
7530
- clientDir = join13(process.cwd(), "src", "web", "client");
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 = join13(cwd, prefix);
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 = readdirSync10(targetDir, { withFileTypes: true });
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, join13(targetDir, e.name)).replace(/\\/g, "/"),
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(join13(cwd, filePath));
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 = readFileSync13(fullPath, "utf-8");
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
- console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (total: ${handlers.size + 1})`);
7620
- const handler = new SessionHandler(ws, shared);
7621
- handlers.set(tabId, handler);
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 = join13(projectRoot, MCP_PROJECT_CONFIG_NAME);
7704
- if (!existsSync18(configPath)) return null;
7970
+ const configPath = join14(projectRoot, MCP_PROJECT_CONFIG_NAME);
7971
+ if (!existsSync19(configPath)) return null;
7705
7972
  try {
7706
- const raw = JSON.parse(readFileSync13(configPath, "utf-8"));
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-NSDG4SSQ.js";
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-IV3LYGTI.js";
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-3B3RXSVK.js");
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-QD5KCAGD.js");
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 () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-IV3LYGTI.js";
5
+ } from "./chunk-QXRBUMCB.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -23,7 +23,7 @@ import {
23
23
  setupProxy,
24
24
  spawnAgentContext,
25
25
  truncateOutput
26
- } from "./chunk-NSDG4SSQ.js";
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-IV3LYGTI.js";
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 join3, dirname, resolve as resolve2, relative } from "path";
45
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
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 = join3(config.getConfigDir(), SKILLS_DIR_NAME);
1412
- if (existsSync4(skillsDir)) {
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 = join3(moduleDir, "web", "client");
1433
- if (!existsSync4(clientDir)) {
1434
- clientDir = join3(moduleDir, "client");
1606
+ let clientDir = join4(moduleDir, "web", "client");
1607
+ if (!existsSync5(clientDir)) {
1608
+ clientDir = join4(moduleDir, "client");
1435
1609
  }
1436
- if (!existsSync4(clientDir)) {
1437
- clientDir = join3(moduleDir, "..", "..", "src", "web", "client");
1610
+ if (!existsSync5(clientDir)) {
1611
+ clientDir = join4(moduleDir, "..", "..", "src", "web", "client");
1438
1612
  }
1439
- if (!existsSync4(clientDir)) {
1440
- clientDir = join3(process.cwd(), "src", "web", "client");
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 = join3(cwd, prefix);
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 = readdirSync(targetDir, { withFileTypes: true });
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, join3(targetDir, e.name)).replace(/\\/g, "/"),
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(join3(cwd, filePath));
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 = readFileSync4(fullPath, "utf-8");
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
- console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (total: ${handlers.size + 1})`);
1530
- const handler = new SessionHandler(ws, shared);
1531
- handlers.set(tabId, handler);
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 = join3(projectRoot, MCP_PROJECT_CONFIG_NAME);
1614
- if (!existsSync4(configPath)) return null;
1880
+ const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
1881
+ if (!existsSync5(configPath)) return null;
1615
1882
  try {
1616
- const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
1883
+ const raw = JSON.parse(readFileSync5(configPath, "utf-8"));
1617
1884
  return raw.mcpServers ?? raw;
1618
1885
  } catch {
1619
1886
  return null;
@@ -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
- ws = new WebSocket(`${protocol}//${location.host}?tabId=${tabId}`);
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; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.99",
3
+ "version": "0.2.1",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -105,31 +105,51 @@
105
105
  "typescript": "^5.7.3",
106
106
  "vitest": "^2.1.8"
107
107
  },
108
+ "author": "jinzd",
108
109
  "build": {
109
110
  "appId": "com.jinzd.ai-cli",
110
111
  "productName": "ai-cli",
111
- "electronMain": "electron/main.cjs",
112
112
  "artifactName": "ai-cli-${os}-${arch}.${ext}",
113
+ "extraMetadata": {
114
+ "main": "electron/main.cjs"
115
+ },
113
116
  "files": [
114
117
  "dist/**/*",
115
- "dist-cjs/**/*",
116
- "electron/**/*.cjs",
117
- "!dist-cjs/index.cjs"
118
+ "electron/**/*.cjs"
118
119
  ],
119
120
  "extraResources": [
120
- { "from": "dist/web/client", "to": "web-client" }
121
+ {
122
+ "from": "dist/web/client",
123
+ "to": "web-client"
124
+ }
125
+ ],
126
+ "asar": true,
127
+ "asarUnpack": [
128
+ "dist/electron-server.js",
129
+ "dist/chunk-*.js"
121
130
  ],
131
+ "forceCodeSigning": false,
122
132
  "win": {
123
- "target": ["nsis", "portable"],
124
- "icon": "src/web/client/icon-512.png"
133
+ "target": [
134
+ "nsis",
135
+ "portable"
136
+ ],
137
+ "icon": "src/web/client/icon-512.png",
138
+ "signAndEditExecutable": false
125
139
  },
126
140
  "mac": {
127
- "target": ["dmg", "zip"],
141
+ "target": [
142
+ "dmg",
143
+ "zip"
144
+ ],
128
145
  "category": "public.app-category.developer-tools",
129
146
  "icon": "src/web/client/icon-512.png"
130
147
  },
131
148
  "linux": {
132
- "target": ["AppImage", "deb"],
149
+ "target": [
150
+ "AppImage",
151
+ "deb"
152
+ ],
133
153
  "icon": "src/web/client/icon-512.png",
134
154
  "category": "Development"
135
155
  },