jinzd-ai-cli 0.2.0 → 0.2.2

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