jinzd-ai-cli 0.2.1 → 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-QXRBUMCB.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.1";
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";
@@ -180,7 +180,7 @@ var EnvLoader = class {
180
180
  };
181
181
 
182
182
  // src/core/constants.ts
183
- var VERSION = "0.2.1";
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";
@@ -7540,6 +7540,29 @@ var AuthManager = class {
7540
7540
  listUsers() {
7541
7541
  return this.db.users.map((u) => u.username);
7542
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
+ }
7543
7566
  /** Migrate existing single-user data to a named user account */
7544
7567
  migrateExistingData(username, password) {
7545
7568
  const err = this.register(username, password);
@@ -7714,9 +7737,29 @@ async function startWebServer(options = {}) {
7714
7737
  models: p.info.models.map((m) => ({ id: m.id, name: m.name ?? m.id }))
7715
7738
  })),
7716
7739
  tools: toolRegistry.getDefinitions().length,
7717
- cwd: process.cwd()
7740
+ cwd: process.cwd(),
7741
+ auth: {
7742
+ enabled: authManager.isEnabled(),
7743
+ userCount: authManager.listUsers().length
7744
+ }
7718
7745
  });
7719
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
+ });
7720
7763
  app.get("/api/files", (req, res) => {
7721
7764
  const cwd = process.cwd();
7722
7765
  const prefix = req.query.prefix || "";
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  theme,
36
36
  truncateOutput,
37
37
  undoStack
38
- } from "./chunk-Z3JMFVT5.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-QXRBUMCB.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-DYALZXPS.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-SRILYYV3.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-QXRBUMCB.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-Z3JMFVT5.js";
26
+ } from "./chunk-NGGVPDNF.js";
27
27
  import {
28
28
  AGENTIC_BEHAVIOR_GUIDELINE,
29
29
  CONTEXT_FILE_CANDIDATES,
@@ -35,14 +35,17 @@ import {
35
35
  PLAN_MODE_SYSTEM_ADDON,
36
36
  SKILLS_DIR_NAME,
37
37
  VERSION
38
- } from "./chunk-QXRBUMCB.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";
42
45
  import { createServer } from "http";
43
46
  import { WebSocketServer } from "ws";
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";
47
+ import { join as join3, dirname, resolve as resolve2, relative } from "path";
48
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
46
49
  import { networkInterfaces } from "os";
47
50
 
48
51
  // src/web/tool-executor-web.ts
@@ -1353,180 +1356,6 @@ ${activated.meta.description || ""}` });
1353
1356
  }
1354
1357
  };
1355
1358
 
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
-
1530
1359
  // src/web/server.ts
1531
1360
  function getModuleDir() {
1532
1361
  try {
@@ -1582,8 +1411,8 @@ async function startWebServer(options = {}) {
1582
1411
  }
1583
1412
  }
1584
1413
  let skillManager = null;
1585
- const skillsDir = join4(config.getConfigDir(), SKILLS_DIR_NAME);
1586
- if (existsSync5(skillsDir)) {
1414
+ const skillsDir = join3(config.getConfigDir(), SKILLS_DIR_NAME);
1415
+ if (existsSync4(skillsDir)) {
1587
1416
  skillManager = new SkillManager(skillsDir);
1588
1417
  skillManager.loadSkills();
1589
1418
  const count = skillManager.listSkills().length;
@@ -1603,15 +1432,15 @@ async function startWebServer(options = {}) {
1603
1432
  const server = createServer(app);
1604
1433
  const wss = new WebSocketServer({ server });
1605
1434
  const moduleDir = getModuleDir();
1606
- let clientDir = join4(moduleDir, "web", "client");
1607
- if (!existsSync5(clientDir)) {
1608
- clientDir = join4(moduleDir, "client");
1435
+ let clientDir = join3(moduleDir, "web", "client");
1436
+ if (!existsSync4(clientDir)) {
1437
+ clientDir = join3(moduleDir, "client");
1609
1438
  }
1610
- if (!existsSync5(clientDir)) {
1611
- clientDir = join4(moduleDir, "..", "..", "src", "web", "client");
1439
+ if (!existsSync4(clientDir)) {
1440
+ clientDir = join3(moduleDir, "..", "..", "src", "web", "client");
1612
1441
  }
1613
- if (!existsSync5(clientDir)) {
1614
- clientDir = join4(process.cwd(), "src", "web", "client");
1442
+ if (!existsSync4(clientDir)) {
1443
+ clientDir = join3(process.cwd(), "src", "web", "client");
1615
1444
  }
1616
1445
  console.log(` Static files: ${clientDir}`);
1617
1446
  app.use(express.static(clientDir));
@@ -1624,23 +1453,43 @@ async function startWebServer(options = {}) {
1624
1453
  models: p.info.models.map((m) => ({ id: m.id, name: m.name ?? m.id }))
1625
1454
  })),
1626
1455
  tools: toolRegistry.getDefinitions().length,
1627
- cwd: process.cwd()
1456
+ cwd: process.cwd(),
1457
+ auth: {
1458
+ enabled: authManager.isEnabled(),
1459
+ userCount: authManager.listUsers().length
1460
+ }
1628
1461
  });
1629
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
+ });
1630
1479
  app.get("/api/files", (req, res) => {
1631
1480
  const cwd = process.cwd();
1632
1481
  const prefix = req.query.prefix || "";
1633
- const targetDir = join4(cwd, prefix);
1482
+ const targetDir = join3(cwd, prefix);
1634
1483
  if (!resolve2(targetDir).startsWith(resolve2(cwd))) {
1635
1484
  res.json({ files: [] });
1636
1485
  return;
1637
1486
  }
1638
1487
  try {
1639
1488
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
1640
- const entries = readdirSync2(targetDir, { withFileTypes: true });
1489
+ const entries = readdirSync(targetDir, { withFileTypes: true });
1641
1490
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
1642
1491
  name: e.name,
1643
- path: relative(cwd, join4(targetDir, e.name)).replace(/\\/g, "/"),
1492
+ path: relative(cwd, join3(targetDir, e.name)).replace(/\\/g, "/"),
1644
1493
  isDir: e.isDirectory()
1645
1494
  }));
1646
1495
  res.json({ files });
@@ -1672,7 +1521,7 @@ async function startWebServer(options = {}) {
1672
1521
  return;
1673
1522
  }
1674
1523
  const cwd = process.cwd();
1675
- const fullPath = resolve2(join4(cwd, filePath));
1524
+ const fullPath = resolve2(join3(cwd, filePath));
1676
1525
  if (!fullPath.startsWith(resolve2(cwd))) {
1677
1526
  res.json({ error: "Access denied" });
1678
1527
  return;
@@ -1683,7 +1532,7 @@ async function startWebServer(options = {}) {
1683
1532
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
1684
1533
  return;
1685
1534
  }
1686
- const content = readFileSync5(fullPath, "utf-8");
1535
+ const content = readFileSync4(fullPath, "utf-8");
1687
1536
  res.json({ content, size: stat.size });
1688
1537
  } catch (err) {
1689
1538
  res.json({ error: `Cannot read: ${err.message}` });
@@ -1877,10 +1726,10 @@ function loadProjectMcpConfig() {
1877
1726
  const cwd = process.cwd();
1878
1727
  const gitRoot = getGitRoot(cwd);
1879
1728
  const projectRoot = gitRoot ?? cwd;
1880
- const configPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
1881
- if (!existsSync5(configPath)) return null;
1729
+ const configPath = join3(projectRoot, MCP_PROJECT_CONFIG_NAME);
1730
+ if (!existsSync4(configPath)) return null;
1882
1731
  try {
1883
- const raw = JSON.parse(readFileSync5(configPath, "utf-8"));
1732
+ const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
1884
1733
  return raw.mcpServers ?? raw;
1885
1734
  } catch {
1886
1735
  return null;
@@ -85,6 +85,7 @@ function connect() {
85
85
  connectionStatus.textContent = '🟢 Connected';
86
86
  connectionStatus.className = 'status-connected';
87
87
  requestSessionList();
88
+ checkAuthStatus();
88
89
  // Start heartbeat ping every 30s
89
90
  clearInterval(heartbeatTimer);
90
91
  heartbeatTimer = setInterval(() => {
@@ -1917,3 +1918,49 @@ function handleLogout() {
1917
1918
  // Reconnect — server will send auth_required
1918
1919
  if (ws) ws.close();
1919
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
+ }
@@ -82,6 +82,8 @@
82
82
  <li><a onclick="setTheme('nord')">❄️ Nord</a></li>
83
83
  </ul>
84
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>
85
87
  <!-- User menu (shown when authenticated) -->
86
88
  <div id="user-menu" class="dropdown dropdown-end hidden">
87
89
  <div tabindex="0" role="button" class="btn btn-sm btn-ghost gap-1">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.2.1",
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",