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.
- package/dist/auth-TWWF22YZ.js +7 -0
- package/dist/chunk-CPLT6CD3.js +202 -0
- package/dist/{chunk-Z3JMFVT5.js → chunk-NGGVPDNF.js} +1 -1
- package/dist/{chunk-QXRBUMCB.js → chunk-YSHQDGVZ.js} +1 -1
- package/dist/electron-server.js +45 -2
- package/dist/index.js +102 -4
- package/dist/{run-tests-DYALZXPS.js → run-tests-3Y3P7QTJ.js} +1 -1
- package/dist/{server-SRILYYV3.js → server-KZ472I7D.js} +45 -196
- package/dist/web/client/app.js +47 -0
- package/dist/web/client/index.html +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/web/auth.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
7
|
+
var USERS_FILE = "users.json";
|
|
8
|
+
var TOKEN_EXPIRY_HOURS = 24 * 7;
|
|
9
|
+
var USERS_DIR = "users";
|
|
10
|
+
var AuthManager = class {
|
|
11
|
+
usersFile;
|
|
12
|
+
baseDir;
|
|
13
|
+
db;
|
|
14
|
+
constructor(baseDir) {
|
|
15
|
+
this.baseDir = baseDir;
|
|
16
|
+
this.usersFile = join(baseDir, USERS_FILE);
|
|
17
|
+
this.db = this.loadOrCreate();
|
|
18
|
+
}
|
|
19
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
20
|
+
/** Check if any users exist (first-run detection) */
|
|
21
|
+
hasUsers() {
|
|
22
|
+
return this.db.users.length > 0;
|
|
23
|
+
}
|
|
24
|
+
/** Check if auth is enabled (has users.json with at least 1 user) */
|
|
25
|
+
isEnabled() {
|
|
26
|
+
return this.hasUsers();
|
|
27
|
+
}
|
|
28
|
+
/** Register a new user. Returns error string or null on success. */
|
|
29
|
+
register(username, password) {
|
|
30
|
+
username = username.trim().toLowerCase();
|
|
31
|
+
if (!username || username.length < 2 || username.length > 32) {
|
|
32
|
+
return "Username must be 2-32 characters";
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z0-9_-]+$/.test(username)) {
|
|
35
|
+
return "Username can only contain a-z, 0-9, _ and -";
|
|
36
|
+
}
|
|
37
|
+
if (!password || password.length < 4) {
|
|
38
|
+
return "Password must be at least 4 characters";
|
|
39
|
+
}
|
|
40
|
+
if (this.db.users.find((u) => u.username === username)) {
|
|
41
|
+
return "Username already exists";
|
|
42
|
+
}
|
|
43
|
+
const salt = randomBytes(16).toString("hex");
|
|
44
|
+
const passwordHash = this.hashPassword(password, salt);
|
|
45
|
+
const dataDir = join(USERS_DIR, username);
|
|
46
|
+
const fullDataDir = join(this.baseDir, dataDir);
|
|
47
|
+
mkdirSync(join(fullDataDir, "history"), { recursive: true });
|
|
48
|
+
const user = {
|
|
49
|
+
username,
|
|
50
|
+
passwordHash,
|
|
51
|
+
salt,
|
|
52
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
dataDir
|
|
54
|
+
};
|
|
55
|
+
this.db.users.push(user);
|
|
56
|
+
this.save();
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** Authenticate user. Returns JWT token or null. */
|
|
60
|
+
login(username, password) {
|
|
61
|
+
username = username.trim().toLowerCase();
|
|
62
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
63
|
+
if (!user) return null;
|
|
64
|
+
const hash = this.hashPassword(password, user.salt);
|
|
65
|
+
const a = Buffer.from(hash, "utf-8");
|
|
66
|
+
const b = Buffer.from(user.passwordHash, "utf-8");
|
|
67
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return this.createToken(username);
|
|
71
|
+
}
|
|
72
|
+
/** Verify a token. Returns username or null. */
|
|
73
|
+
verifyToken(token) {
|
|
74
|
+
try {
|
|
75
|
+
const parts = token.split(".");
|
|
76
|
+
if (parts.length !== 2) return null;
|
|
77
|
+
const [payloadB64, signature] = parts;
|
|
78
|
+
const expectedSig = this.sign(payloadB64);
|
|
79
|
+
if (signature !== expectedSig) return null;
|
|
80
|
+
const payload = JSON.parse(
|
|
81
|
+
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
82
|
+
);
|
|
83
|
+
if (Date.now() > payload.exp) return null;
|
|
84
|
+
if (!this.db.users.find((u) => u.username === payload.username)) return null;
|
|
85
|
+
return payload.username;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Get user's data directory (absolute path) */
|
|
91
|
+
getUserDataDir(username) {
|
|
92
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
93
|
+
if (!user) throw new Error(`User not found: ${username}`);
|
|
94
|
+
return join(this.baseDir, user.dataDir);
|
|
95
|
+
}
|
|
96
|
+
/** List all usernames */
|
|
97
|
+
listUsers() {
|
|
98
|
+
return this.db.users.map((u) => u.username);
|
|
99
|
+
}
|
|
100
|
+
/** Delete a user. Returns error string or null on success. */
|
|
101
|
+
deleteUser(username) {
|
|
102
|
+
username = username.trim().toLowerCase();
|
|
103
|
+
const idx = this.db.users.findIndex((u) => u.username === username);
|
|
104
|
+
if (idx === -1) return `User '${username}' not found`;
|
|
105
|
+
this.db.users.splice(idx, 1);
|
|
106
|
+
this.save();
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/** Reset a user's password. Returns error string or null on success. */
|
|
110
|
+
resetPassword(username, newPassword) {
|
|
111
|
+
username = username.trim().toLowerCase();
|
|
112
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
113
|
+
if (!user) return `User '${username}' not found`;
|
|
114
|
+
if (!newPassword || newPassword.length < 4) {
|
|
115
|
+
return "Password must be at least 4 characters";
|
|
116
|
+
}
|
|
117
|
+
const salt = randomBytes(16).toString("hex");
|
|
118
|
+
user.passwordHash = this.hashPassword(newPassword, salt);
|
|
119
|
+
user.salt = salt;
|
|
120
|
+
this.save();
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Migrate existing single-user data to a named user account */
|
|
124
|
+
migrateExistingData(username, password) {
|
|
125
|
+
const err = this.register(username, password);
|
|
126
|
+
if (err) return err;
|
|
127
|
+
const userDir = this.getUserDataDir(username);
|
|
128
|
+
const globalConfig = join(this.baseDir, "config.json");
|
|
129
|
+
if (existsSync(globalConfig)) {
|
|
130
|
+
try {
|
|
131
|
+
const content = readFileSync(globalConfig, "utf-8");
|
|
132
|
+
writeFileSync(join(userDir, "config.json"), content, "utf-8");
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const globalMemory = join(this.baseDir, "memory.md");
|
|
137
|
+
if (existsSync(globalMemory)) {
|
|
138
|
+
try {
|
|
139
|
+
const content = readFileSync(globalMemory, "utf-8");
|
|
140
|
+
writeFileSync(join(userDir, "memory.md"), content, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const globalHistory = join(this.baseDir, "history");
|
|
145
|
+
if (existsSync(globalHistory)) {
|
|
146
|
+
try {
|
|
147
|
+
const files = readdirSync(globalHistory).filter((f) => f.endsWith(".json"));
|
|
148
|
+
const userHistory = join(userDir, "history");
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
try {
|
|
151
|
+
copyFileSync(join(globalHistory, f), join(userHistory, f));
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// ── Private methods ────────────────────────────────────────────
|
|
161
|
+
loadOrCreate() {
|
|
162
|
+
if (existsSync(this.usersFile)) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(this.usersFile, "utf-8"));
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const db = {
|
|
169
|
+
version: 1,
|
|
170
|
+
secret: randomBytes(32).toString("hex"),
|
|
171
|
+
users: []
|
|
172
|
+
};
|
|
173
|
+
this.saveDB(db);
|
|
174
|
+
return db;
|
|
175
|
+
}
|
|
176
|
+
save() {
|
|
177
|
+
this.saveDB(this.db);
|
|
178
|
+
}
|
|
179
|
+
saveDB(db) {
|
|
180
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
181
|
+
writeFileSync(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
|
|
182
|
+
}
|
|
183
|
+
hashPassword(password, salt) {
|
|
184
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
185
|
+
}
|
|
186
|
+
createToken(username) {
|
|
187
|
+
const payload = {
|
|
188
|
+
username,
|
|
189
|
+
exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
190
|
+
};
|
|
191
|
+
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
192
|
+
const signature = this.sign(payloadB64);
|
|
193
|
+
return `${payloadB64}.${signature}`;
|
|
194
|
+
}
|
|
195
|
+
sign(data) {
|
|
196
|
+
return createHmac("sha256", this.db.secret).update(data).digest("base64url");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
AuthManager
|
|
202
|
+
};
|
package/dist/electron-server.js
CHANGED
|
@@ -180,7 +180,7 @@ var EnvLoader = class {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
// src/core/constants.ts
|
|
183
|
-
var VERSION = "0.2.
|
|
183
|
+
var VERSION = "0.2.2";
|
|
184
184
|
var APP_NAME = "ai-cli";
|
|
185
185
|
var CONFIG_DIR_NAME = ".aicli";
|
|
186
186
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -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-
|
|
38
|
+
} from "./chunk-NGGVPDNF.js";
|
|
39
39
|
import {
|
|
40
40
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
41
41
|
AUTHOR,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
REPO_URL,
|
|
56
56
|
SKILLS_DIR_NAME,
|
|
57
57
|
VERSION
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-YSHQDGVZ.js";
|
|
59
59
|
|
|
60
60
|
// src/index.ts
|
|
61
61
|
import { program } from "commander";
|
|
@@ -1904,7 +1904,7 @@ ${hint}` : "")
|
|
|
1904
1904
|
description: "Run project tests and show structured report",
|
|
1905
1905
|
usage: "/test [command|filter]",
|
|
1906
1906
|
async execute(args, _ctx) {
|
|
1907
|
-
const { executeTests } = await import("./run-tests-
|
|
1907
|
+
const { executeTests } = await import("./run-tests-3Y3P7QTJ.js");
|
|
1908
1908
|
const argStr = args.join(" ").trim();
|
|
1909
1909
|
let testArgs = {};
|
|
1910
1910
|
if (argStr) {
|
|
@@ -5292,9 +5292,107 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5292
5292
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5293
5293
|
process.exit(1);
|
|
5294
5294
|
}
|
|
5295
|
-
const { startWebServer } = await import("./server-
|
|
5295
|
+
const { startWebServer } = await import("./server-KZ472I7D.js");
|
|
5296
5296
|
await startWebServer({ port, host: options.host });
|
|
5297
5297
|
});
|
|
5298
|
+
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
5299
|
+
const { AuthManager } = await import("./auth-TWWF22YZ.js");
|
|
5300
|
+
const config = new ConfigManager();
|
|
5301
|
+
const auth = new AuthManager(config.getConfigDir());
|
|
5302
|
+
if (!action || action === "list") {
|
|
5303
|
+
const users = auth.listUsers();
|
|
5304
|
+
if (users.length === 0) {
|
|
5305
|
+
console.log("\nNo users registered. Auth is disabled (single-user mode).");
|
|
5306
|
+
console.log("Create a user to enable multi-user auth: aicli user create <username>\n");
|
|
5307
|
+
} else {
|
|
5308
|
+
console.log(`
|
|
5309
|
+
${users.length} user(s) registered (auth enabled):
|
|
5310
|
+
`);
|
|
5311
|
+
for (const u of users) {
|
|
5312
|
+
console.log(` \u2022 ${u}`);
|
|
5313
|
+
}
|
|
5314
|
+
console.log();
|
|
5315
|
+
}
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
5318
|
+
async function askPassword(msg) {
|
|
5319
|
+
const { password: pwFn } = await import("@inquirer/prompts");
|
|
5320
|
+
return pwFn({ message: msg, mask: "*" });
|
|
5321
|
+
}
|
|
5322
|
+
if (action === "create") {
|
|
5323
|
+
if (!username) {
|
|
5324
|
+
console.error("Usage: aicli user create <username>");
|
|
5325
|
+
process.exit(1);
|
|
5326
|
+
}
|
|
5327
|
+
const pw = await askPassword(`Password for ${username}:`);
|
|
5328
|
+
if (!pw || pw.length < 4) {
|
|
5329
|
+
console.error("Password must be at least 4 characters.");
|
|
5330
|
+
process.exit(1);
|
|
5331
|
+
}
|
|
5332
|
+
const err = auth.register(username, pw);
|
|
5333
|
+
if (err) {
|
|
5334
|
+
console.error(`Error: ${err}`);
|
|
5335
|
+
process.exit(1);
|
|
5336
|
+
}
|
|
5337
|
+
console.log(`\u2713 User '${username}' created. Auth is now enabled for Web UI.`);
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
if (action === "delete") {
|
|
5341
|
+
if (!username) {
|
|
5342
|
+
console.error("Usage: aicli user delete <username>");
|
|
5343
|
+
process.exit(1);
|
|
5344
|
+
}
|
|
5345
|
+
const err = auth.deleteUser(username);
|
|
5346
|
+
if (err) {
|
|
5347
|
+
console.error(`Error: ${err}`);
|
|
5348
|
+
process.exit(1);
|
|
5349
|
+
}
|
|
5350
|
+
console.log(`\u2713 User '${username}' deleted.`);
|
|
5351
|
+
if (!auth.hasUsers()) {
|
|
5352
|
+
console.log(" No users remaining \u2014 auth is now disabled (single-user mode).");
|
|
5353
|
+
}
|
|
5354
|
+
return;
|
|
5355
|
+
}
|
|
5356
|
+
if (action === "reset-password") {
|
|
5357
|
+
if (!username) {
|
|
5358
|
+
console.error("Usage: aicli user reset-password <username>");
|
|
5359
|
+
process.exit(1);
|
|
5360
|
+
}
|
|
5361
|
+
const pw = await askPassword(`New password for ${username}:`);
|
|
5362
|
+
if (!pw || pw.length < 4) {
|
|
5363
|
+
console.error("Password must be at least 4 characters.");
|
|
5364
|
+
process.exit(1);
|
|
5365
|
+
}
|
|
5366
|
+
const err = auth.resetPassword(username, pw);
|
|
5367
|
+
if (err) {
|
|
5368
|
+
console.error(`Error: ${err}`);
|
|
5369
|
+
process.exit(1);
|
|
5370
|
+
}
|
|
5371
|
+
console.log(`\u2713 Password reset for '${username}'.`);
|
|
5372
|
+
return;
|
|
5373
|
+
}
|
|
5374
|
+
if (action === "migrate") {
|
|
5375
|
+
if (!username) {
|
|
5376
|
+
console.error("Usage: aicli user migrate <username>");
|
|
5377
|
+
process.exit(1);
|
|
5378
|
+
}
|
|
5379
|
+
const pw = await askPassword(`Password for new user ${username}:`);
|
|
5380
|
+
if (!pw || pw.length < 4) {
|
|
5381
|
+
console.error("Password must be at least 4 characters.");
|
|
5382
|
+
process.exit(1);
|
|
5383
|
+
}
|
|
5384
|
+
const err = auth.migrateExistingData(username, pw);
|
|
5385
|
+
if (err) {
|
|
5386
|
+
console.error(`Error: ${err}`);
|
|
5387
|
+
process.exit(1);
|
|
5388
|
+
}
|
|
5389
|
+
console.log(`\u2713 User '${username}' created with existing data migrated.`);
|
|
5390
|
+
return;
|
|
5391
|
+
}
|
|
5392
|
+
console.error(`Unknown action: ${action}`);
|
|
5393
|
+
console.error("Available: list, create, delete, reset-password, migrate");
|
|
5394
|
+
process.exit(1);
|
|
5395
|
+
});
|
|
5298
5396
|
program.command("sessions").description("List recent conversation sessions").action(async () => {
|
|
5299
5397
|
const config = new ConfigManager();
|
|
5300
5398
|
const sessions = new SessionManager(config);
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setupProxy,
|
|
24
24
|
spawnAgentContext,
|
|
25
25
|
truncateOutput
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-NGGVPDNF.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
29
29
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -35,14 +35,17 @@ import {
|
|
|
35
35
|
PLAN_MODE_SYSTEM_ADDON,
|
|
36
36
|
SKILLS_DIR_NAME,
|
|
37
37
|
VERSION
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-YSHQDGVZ.js";
|
|
39
|
+
import {
|
|
40
|
+
AuthManager
|
|
41
|
+
} from "./chunk-CPLT6CD3.js";
|
|
39
42
|
|
|
40
43
|
// src/web/server.ts
|
|
41
44
|
import express from "express";
|
|
42
45
|
import { createServer } from "http";
|
|
43
46
|
import { WebSocketServer } from "ws";
|
|
44
|
-
import { join as
|
|
45
|
-
import { existsSync as
|
|
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 =
|
|
1586
|
-
if (
|
|
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 =
|
|
1607
|
-
if (!
|
|
1608
|
-
clientDir =
|
|
1435
|
+
let clientDir = join3(moduleDir, "web", "client");
|
|
1436
|
+
if (!existsSync4(clientDir)) {
|
|
1437
|
+
clientDir = join3(moduleDir, "client");
|
|
1609
1438
|
}
|
|
1610
|
-
if (!
|
|
1611
|
-
clientDir =
|
|
1439
|
+
if (!existsSync4(clientDir)) {
|
|
1440
|
+
clientDir = join3(moduleDir, "..", "..", "src", "web", "client");
|
|
1612
1441
|
}
|
|
1613
|
-
if (!
|
|
1614
|
-
clientDir =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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(
|
|
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 =
|
|
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 =
|
|
1881
|
-
if (!
|
|
1729
|
+
const configPath = join3(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
1730
|
+
if (!existsSync4(configPath)) return null;
|
|
1882
1731
|
try {
|
|
1883
|
-
const raw = JSON.parse(
|
|
1732
|
+
const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
1884
1733
|
return raw.mcpServers ?? raw;
|
|
1885
1734
|
} catch {
|
|
1886
1735
|
return null;
|
package/dist/web/client/app.js
CHANGED
|
@@ -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">
|