jinzd-ai-cli 0.4.306 → 0.4.308

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.
Files changed (95) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +3 -0
  3. package/dist/{agent-client-6GX6QQDU.js → agent-client-TIEAYZXV.js} +4 -1
  4. package/dist/{auth-PE3Z5OHS.js → auth-QTQNZ6IX.js} +2 -2
  5. package/dist/{batch-JLGWQ7HU.js → batch-S7XLEEOH.js} +3 -3
  6. package/dist/{chat-index-2D5GK2DB.js → chat-index-7CLR4CBG.js} +4 -3
  7. package/dist/{chat-index-DGUOUUQW.js → chat-index-RLVLQRRC.js} +3 -2
  8. package/dist/{chunk-JDT6UG6R.js → chunk-2DYQDLLF.js} +1 -1
  9. package/dist/{chunk-T6WQEQI4.js → chunk-2MNUMLN4.js} +1 -1
  10. package/dist/chunk-2XMUCC42.js +67 -0
  11. package/dist/{chunk-7SSCSSC6.js → chunk-3537CQ5M.js} +2 -2
  12. package/dist/{chunk-OAXLMF3U.js → chunk-3YVNDYOY.js} +1 -1
  13. package/dist/{chunk-RYQBBEMM.js → chunk-4PR5AVJ5.js} +115 -19
  14. package/dist/{chunk-GHWZVPUV.js → chunk-6JHACXB3.js} +7 -5
  15. package/dist/{chunk-WZWRA7R2.js → chunk-7OHXFTZO.js} +2 -2
  16. package/dist/{chunk-IUVDZVZ4.js → chunk-AXTLYOA7.js} +24 -17
  17. package/dist/{chunk-XJOK5353.js → chunk-B3MPYI4E.js} +2 -2
  18. package/dist/{chunk-3RDJEYF7.js → chunk-BGZN4K7Q.js} +4 -4
  19. package/dist/chunk-BMLQPSHP.js +69 -0
  20. package/dist/{chunk-NRSYBKWA.js → chunk-C7CKDFJS.js} +1 -1
  21. package/dist/{chunk-JFNBJHR6.js → chunk-E2PNXNKI.js} +4 -4
  22. package/dist/chunk-FVXNEZEW.js +161 -0
  23. package/dist/{chunk-YKJO3SI4.js → chunk-G6DVGQ4I.js} +1 -1
  24. package/dist/{chunk-AYLK4EGA.js → chunk-HC33C4U3.js} +18 -18
  25. package/dist/chunk-JJFJWD35.js +162 -0
  26. package/dist/{chunk-W6PLV3EY.js → chunk-JJNUASMJ.js} +263 -55
  27. package/dist/{chunk-NYNSYLFY.js → chunk-JL44HGZC.js} +1 -1
  28. package/dist/{chunk-NUJTNH76.js → chunk-LBWKMTT3.js} +1 -1
  29. package/dist/{chunk-LAP7TPPK.js → chunk-LR3TGZWQ.js} +2 -2
  30. package/dist/{chunk-WQ3OMEC4.js → chunk-M6HLYG33.js} +1 -1
  31. package/dist/{chunk-I4GQRMFV.js → chunk-M6YV5KSS.js} +6 -4
  32. package/dist/{chunk-K5SDDTN7.js → chunk-NLK45GUS.js} +1 -1
  33. package/dist/{chunk-KBDKKHXB.js → chunk-O6CGOVD4.js} +2 -2
  34. package/dist/chunk-QMFMST2T.js +39 -0
  35. package/dist/{chunk-MI4SFZGX.js → chunk-RMEJR3AW.js} +5 -2
  36. package/dist/{chunk-UFW4S3RU.js → chunk-RVMCY6DP.js} +1 -1
  37. package/dist/{chunk-LF67HWWR.js → chunk-RWRZVQCB.js} +1 -1
  38. package/dist/chunk-SXIJLSVF.js +41 -0
  39. package/dist/chunk-TDQW2YHK.js +65 -0
  40. package/dist/{chunk-VLDB2SVN.js → chunk-TPTFV3Y6.js} +383 -513
  41. package/dist/{chunk-45UVJXFQ.js → chunk-W2UOVGRC.js} +1 -1
  42. package/dist/{chunk-VXEF2SWG.js → chunk-WT252ZDV.js} +2 -2
  43. package/dist/{chunk-NLB7JZSO.js → chunk-WVEIUSXE.js} +246 -55
  44. package/dist/{chunk-OC2XHG5L.js → chunk-YBZM7PHI.js} +4 -4
  45. package/dist/{ci-6PHEL3C7.js → ci-5HONSUAS.js} +7 -7
  46. package/dist/{ci-format-AZYUP6BL.js → ci-format-URUH3ZZ5.js} +2 -2
  47. package/dist/{constants-CPNWIUGZ.js → constants-BLDBI7IG.js} +1 -1
  48. package/dist/{constants-AXBD2EEB.js → constants-FG27DQMS.js} +1 -1
  49. package/dist/doctor-cli-QQLMNJN7.js +18 -0
  50. package/dist/electron-server.js +1003 -829
  51. package/dist/file-checkpoint-64A2AXHE.js +7 -0
  52. package/dist/{file-checkpoint-NKBHGC7L.js → file-checkpoint-VVPUJNCI.js} +2 -1
  53. package/dist/{hub-DDYAWLF2.js → hub-GGFW3DCC.js} +3 -3
  54. package/dist/{hub-server-CBKCP5VD.js → hub-server-JLC7CNP7.js} +55 -17
  55. package/dist/index.js +42 -40
  56. package/dist/{indexer-P3FPRTPJ.js → indexer-GKIWSSRP.js} +3 -3
  57. package/dist/indexer-VRXDZ74A.js +10 -0
  58. package/dist/{persist-S77WRVLD.js → persist-ZZXRWFBX.js} +4 -4
  59. package/dist/{persistent-memory-XZISXRWT.js → persistent-memory-UNLCXUWH.js} +3 -3
  60. package/dist/{persistent-memory-6XYYOMSC.js → persistent-memory-YYWIN6BG.js} +3 -3
  61. package/dist/{pr-UY4ZCZNI.js → pr-YCFGPFI7.js} +7 -7
  62. package/dist/{project-trust-MUG325AW.js → project-trust-6CJB5ZMX.js} +1 -1
  63. package/dist/{project-trust-NKYHL3VZ.js → project-trust-KGG2BWKC.js} +1 -1
  64. package/dist/run-tests-GHY4BYDE.js +13 -0
  65. package/dist/run-tests-KRNMFXAN.js +12 -0
  66. package/dist/{semantic-7GM5M4MU.js → semantic-N227CLSC.js} +4 -4
  67. package/dist/{semantic-QOBAXEQ4.js → semantic-TRQAZSRY.js} +4 -4
  68. package/dist/{server-6IC33CJK.js → server-73UBHSFV.js} +16 -14
  69. package/dist/{server-DBHLVQVP.js → server-QRAWSG4Z.js} +231 -103
  70. package/dist/{store-SIKHCUYJ.js → store-2N6PPPBP.js} +2 -2
  71. package/dist/{store-CMSSWZF7.js → store-76VVFDXM.js} +2 -2
  72. package/dist/{task-orchestrator-RDDETDW2.js → task-orchestrator-GI3RGLZI.js} +15 -14
  73. package/dist/{usage-D473ZVIP.js → usage-GWTIGZF5.js} +4 -4
  74. package/dist/{vector-store-JAXWDZYW.js → vector-store-T2DVUMY4.js} +2 -2
  75. package/dist/{vector-store-PWIOQFTP.js → vector-store-V6UGEYWK.js} +2 -2
  76. package/dist/web/client/app.js +7 -0
  77. package/dist/web/client/app.js.br +0 -0
  78. package/dist/web/client/app.js.gz +0 -0
  79. package/dist/web/client/util.js +14 -23
  80. package/dist/web/client/util.js.br +0 -0
  81. package/dist/web/client/util.js.gz +0 -0
  82. package/dist/web/client/vendor/DOMPurify-LICENSE.txt +202 -0
  83. package/dist/web/client/vendor/purify.es.mjs +2722 -0
  84. package/dist/web/client/vendor/purify.es.mjs.br +0 -0
  85. package/dist/web/client/vendor/purify.es.mjs.gz +0 -0
  86. package/package.json +4 -2
  87. package/dist/chunk-4BKXL7SM.js +0 -98
  88. package/dist/chunk-6BUTA5VW.js +0 -38
  89. package/dist/chunk-IW3Q7AE5.js +0 -40
  90. package/dist/chunk-SKET65WZ.js +0 -96
  91. package/dist/doctor-cli-DDHHDMMJ.js +0 -18
  92. package/dist/file-checkpoint-CGH6OJVI.js +0 -6
  93. package/dist/indexer-FPKY7LCT.js +0 -10
  94. package/dist/run-tests-BAMLVLO6.js +0 -10
  95. package/dist/run-tests-UEE3OMLS.js +0 -9
package/README.md CHANGED
@@ -578,6 +578,7 @@ Two groups: what **you** write by hand (the script never touches these, and Guar
578
578
  | `CHANGELOG.md` | **By hand, before releasing** | A new `## [X.Y.Z] - YYYY-MM-DD` section with the full narrative: root cause / fix / tests. This is the only place detail lives |
579
579
  | `CLAUDE.md` → `## 最近里程碑` | **By hand, before releasing** | **One** index line at the top, which must carry the full-width marker `(vX.Y.Z)` — this exact line is what Guard A checks |
580
580
  | `package.json` | Script | `"version"` |
581
+ | `package-lock.json` | Script | Root `version` and `packages[""].version`, including rollback on failed verification |
581
582
  | `src/core/constants.ts` | Script | The `VERSION` constant (what `aicli --version` and the help banner actually read — drift here ships a package that reports the wrong version) |
582
583
  | `CLAUDE.md` → `**当前版本**` | Script | The version number (a *second*, separate spot from the hand-written milestone line) |
583
584
  | `docs/USAGE.md`, `docs/USAGE.zh-CN.md`, `docs/ADVANCED*.md`, `docs/TUTORIAL*.md`, `docs/RECIPES*.md`, `SECURITY.md`, `docs/SECURITY*.md` | Script | Only the **document-header** baseline lines (`> **Version**: vX.Y.Z`, security baseline, `当前 vX.Y.Z`). Deliberately first-match-only — "feature introduced in v0.4.246+" markers in the body must keep their original versions |
@@ -8,6 +8,9 @@ ai-cli includes the following vendored browser assets. Their licenses apply to t
8
8
  | daisyUI | `src/web/client/vendor/daisyui-full.min.css` | 4.12.23 | MIT | https://github.com/saadeghi/daisyui |
9
9
  | highlight.js and GitHub themes | `highlight.min.js`, `github.min.css`, `github-dark.min.css` | 11.9.0 | BSD-3-Clause | https://github.com/highlightjs/highlight.js |
10
10
  | marked | `src/web/client/vendor/marked.min.js` | 15.0.12 | MIT | https://github.com/markedjs/marked |
11
+ | DOMPurify | `src/web/client/vendor/purify.es.mjs` | 3.4.15 | Apache-2.0 OR MPL-2.0 | https://github.com/cure53/DOMPurify |
12
+
13
+ DOMPurify's full license is included in `src/web/client/vendor/DOMPurify-LICENSE.txt` and copied into the distributed Web client. The build copies the browser module from the installed, lockfile-pinned package.
11
14
 
12
15
  When replacing a vendored asset, update this file and preserve its upstream license header.
13
16
 
@@ -40,7 +40,9 @@ async function joinHub(options, configManager, providers) {
40
40
  console.log(chalk.dim(` Provider: ${defaultProvider} / ${defaultModel}`));
41
41
  }
42
42
  console.log();
43
- const ws = new WebSocket(url);
43
+ const token = process.env.AICLI_HUB_TOKEN;
44
+ if (!token) throw new Error("Set AICLI_HUB_TOKEN to the credential printed by the hub server.");
45
+ const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` }, maxPayload: 1024 * 1024 });
44
46
  let myRoleId = "";
45
47
  let myRoleName = "";
46
48
  let topic = "";
@@ -133,6 +135,7 @@ async function joinHub(options, configManager, providers) {
133
135
  }
134
136
  const reply = {
135
137
  type: "response",
138
+ turnId: msg.turnId,
136
139
  content: passed ? "" : response.trim(),
137
140
  passed
138
141
  };
@@ -3,8 +3,8 @@ import {
3
3
  AuthManager,
4
4
  TOKEN_EXPIRY_MS,
5
5
  __resetLoginAttemptsForTests
6
- } from "./chunk-RYQBBEMM.js";
7
- import "./chunk-IW3Q7AE5.js";
6
+ } from "./chunk-4PR5AVJ5.js";
7
+ import "./chunk-BMLQPSHP.js";
8
8
  export {
9
9
  AuthManager,
10
10
  TOKEN_EXPIRY_MS,
@@ -4,12 +4,12 @@ import {
4
4
  } from "./chunk-RS7SKYTS.js";
5
5
  import {
6
6
  ConfigManager
7
- } from "./chunk-OC2XHG5L.js";
7
+ } from "./chunk-YBZM7PHI.js";
8
8
  import "./chunk-TU3L3PW7.js";
9
9
  import {
10
10
  atomicWriteFileSync
11
- } from "./chunk-IW3Q7AE5.js";
12
- import "./chunk-T6WQEQI4.js";
11
+ } from "./chunk-BMLQPSHP.js";
12
+ import "./chunk-2MNUMLN4.js";
13
13
 
14
14
  // src/cli/batch.ts
15
15
  import Anthropic from "@anthropic-ai/sdk";
@@ -6,11 +6,12 @@ import {
6
6
  getChatIndexStatus,
7
7
  loadChatIndex,
8
8
  searchChatMemory
9
- } from "./chunk-GHWZVPUV.js";
9
+ } from "./chunk-6JHACXB3.js";
10
10
  import "./chunk-KHYD3WXE.js";
11
- import "./chunk-NYNSYLFY.js";
11
+ import "./chunk-JL44HGZC.js";
12
12
  import "./chunk-3FEZSLGI.js";
13
- import "./chunk-IW3Q7AE5.js";
13
+ import "./chunk-BMLQPSHP.js";
14
+ import "./chunk-SXIJLSVF.js";
14
15
  export {
15
16
  buildChatIndex,
16
17
  chunkSession,
@@ -5,10 +5,11 @@ import {
5
5
  getChatIndexStatus,
6
6
  loadChatIndex,
7
7
  searchChatMemory
8
- } from "./chunk-I4GQRMFV.js";
8
+ } from "./chunk-M6YV5KSS.js";
9
9
  import "./chunk-JV5N65KN.js";
10
10
  import "./chunk-FSC6KEWU.js";
11
- import "./chunk-6BUTA5VW.js";
11
+ import "./chunk-2XMUCC42.js";
12
+ import "./chunk-QMFMST2T.js";
12
13
  export {
13
14
  buildChatIndex,
14
15
  chunkSession,
@@ -1,5 +1,5 @@
1
1
  // src/core/constants.ts
2
- var VERSION = "0.4.306";
2
+ var VERSION = "0.4.308";
3
3
  var APP_NAME = "ai-cli";
4
4
  var CONFIG_DIR_NAME = ".aicli";
5
5
  var CONFIG_FILE_NAME = "config.json";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.306";
4
+ var VERSION = "0.4.308";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -0,0 +1,67 @@
1
+ // src/core/atomic-write.ts
2
+ import fs from "fs";
3
+ import { randomUUID } from "crypto";
4
+ import { setTimeout as delay } from "timers/promises";
5
+ function modeFor(target, options) {
6
+ if (options.mode !== void 0) return options.mode;
7
+ try {
8
+ return fs.statSync(target).mode & 511;
9
+ } catch {
10
+ return 384;
11
+ }
12
+ }
13
+ function atomicWriteFileSync(target, data, options = {}) {
14
+ const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
15
+ try {
16
+ fs.writeFileSync(tmp, data, { flag: "wx", mode: modeFor(target, options) });
17
+ fs.renameSync(tmp, target);
18
+ } finally {
19
+ try {
20
+ fs.unlinkSync(tmp);
21
+ } catch {
22
+ }
23
+ }
24
+ }
25
+ var pending = /* @__PURE__ */ new Map();
26
+ async function atomicWriteFile(target, data, options = {}) {
27
+ const previous = pending.get(target) ?? Promise.resolve();
28
+ const operation = previous.catch(() => {
29
+ }).then(async () => {
30
+ const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
31
+ try {
32
+ let mode = options.mode ?? 384;
33
+ if (options.mode === void 0) {
34
+ try {
35
+ mode = (await fs.promises.stat(target)).mode & 511;
36
+ } catch {
37
+ }
38
+ }
39
+ await fs.promises.writeFile(tmp, data, { flag: "wx", mode });
40
+ for (let attempt = 0; ; attempt++) {
41
+ try {
42
+ await fs.promises.rename(tmp, target);
43
+ break;
44
+ } catch (error) {
45
+ if (attempt >= 10 || !["EPERM", "EBUSY", "EACCES"].includes(error.code ?? "")) throw error;
46
+ await delay(5 * (attempt + 1));
47
+ }
48
+ }
49
+ } finally {
50
+ try {
51
+ await fs.promises.unlink(tmp);
52
+ } catch {
53
+ }
54
+ }
55
+ });
56
+ pending.set(target, operation);
57
+ try {
58
+ await operation;
59
+ } finally {
60
+ if (pending.get(target) === operation) pending.delete(target);
61
+ }
62
+ }
63
+
64
+ export {
65
+ atomicWriteFileSync,
66
+ atomicWriteFile
67
+ };
@@ -4,11 +4,11 @@ import {
4
4
  } from "./chunk-RS7SKYTS.js";
5
5
  import {
6
6
  atomicWriteFileSync
7
- } from "./chunk-IW3Q7AE5.js";
7
+ } from "./chunk-BMLQPSHP.js";
8
8
  import {
9
9
  CONFIG_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-T6WQEQI4.js";
11
+ } from "./chunk-2MNUMLN4.js";
12
12
 
13
13
  // src/diagnostics/tool-stats.ts
14
14
  import { mkdirSync } from "fs";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-JV5N65KN.js";
4
4
  import {
5
5
  atomicWriteFileSync
6
- } from "./chunk-6BUTA5VW.js";
6
+ } from "./chunk-2XMUCC42.js";
7
7
 
8
8
  // src/symbols/vector-store.ts
9
9
  import fs from "fs";
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  atomicWriteFileSync
4
- } from "./chunk-IW3Q7AE5.js";
4
+ } from "./chunk-BMLQPSHP.js";
5
5
 
6
6
  // src/web/auth.ts
7
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from "fs";
7
+ import { existsSync, readFileSync, mkdirSync, readdirSync, openSync, closeSync, unlinkSync } from "fs";
8
8
  import { isAbsolute, join, relative } from "path";
9
- import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
9
+ import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync, pbkdf2 } from "crypto";
10
10
  var USERS_FILE = "users.json";
11
11
  var TOKEN_EXPIRY_HOURS = 24;
12
12
  var TOKEN_EXPIRY_MS = TOKEN_EXPIRY_HOURS * 3600 * 1e3;
@@ -21,6 +21,7 @@ var AuthManager = class {
21
21
  usersFile;
22
22
  baseDir;
23
23
  db;
24
+ loadedRaw;
24
25
  constructor(baseDir) {
25
26
  this.baseDir = baseDir;
26
27
  this.usersFile = join(baseDir, USERS_FILE);
@@ -29,6 +30,7 @@ var AuthManager = class {
29
30
  // ── Public API ─────────────────────────────────────────────────
30
31
  /** Check if any users exist (first-run detection) */
31
32
  hasUsers() {
33
+ this.refresh();
32
34
  return this.db.users.length > 0;
33
35
  }
34
36
  /** Check if auth is enabled (has users.json with at least 1 user) */
@@ -37,6 +39,18 @@ var AuthManager = class {
37
39
  }
38
40
  /** Register a new user. Returns error string or null on success. */
39
41
  register(username, password) {
42
+ return this.registerPrepared(username, password);
43
+ }
44
+ async registerAsync(username, password) {
45
+ if (typeof username !== "string" || typeof password !== "string" || password.length > 1024) return "Invalid username or password";
46
+ if (!/^[a-z0-9_-]{2,32}$/i.test(username.trim()) || password.length < 4) return "Invalid username or password";
47
+ const salt = randomBytes(16).toString("hex");
48
+ const hash = await hashPasswordAsync(password, salt);
49
+ return this.registerPrepared(username, password, { salt, hash });
50
+ }
51
+ registerPrepared(username, password, prepared) {
52
+ this.refresh();
53
+ if (typeof username !== "string" || typeof password !== "string" || password.length > 1024) return "Invalid username or password";
40
54
  username = username.trim().toLowerCase();
41
55
  if (!username || username.length < 2 || username.length > 32) {
42
56
  return "Username must be 2-32 characters";
@@ -50,11 +64,12 @@ var AuthManager = class {
50
64
  if (this.db.users.find((u) => u.username === username)) {
51
65
  return "Username already exists";
52
66
  }
53
- const salt = randomBytes(16).toString("hex");
54
- const passwordHash = this.hashPassword(password, salt);
67
+ const salt = prepared?.salt ?? randomBytes(16).toString("hex");
68
+ const passwordHash = prepared?.hash ?? this.hashPassword(password, salt);
55
69
  const dataDir = join(USERS_DIR, username);
56
70
  const fullDataDir = join(this.baseDir, dataDir);
57
- mkdirSync(join(fullDataDir, "history"), { recursive: true });
71
+ mkdirSync(fullDataDir, { recursive: true, mode: 448 });
72
+ mkdirSync(join(fullDataDir, "history"), { recursive: true, mode: 448 });
58
73
  const user = {
59
74
  username,
60
75
  passwordHash,
@@ -77,6 +92,8 @@ var AuthManager = class {
77
92
  * resets the counter.
78
93
  */
79
94
  login(username, password) {
95
+ this.refresh();
96
+ if (!validCredentials(username, password)) return null;
80
97
  username = username.trim().toLowerCase();
81
98
  const lockState = this.getLockState(username);
82
99
  if (lockState.locked) return null;
@@ -100,19 +117,51 @@ var AuthManager = class {
100
117
  user.hashVersion = 2;
101
118
  this.save();
102
119
  }
103
- loginAttempts.delete(username);
120
+ loginAttempts.delete(this.attemptKey(username));
104
121
  return this.createToken(username);
105
122
  }
106
123
  /**
107
124
  * Returns current lockout state for a username and lazily expires it.
108
125
  * Exposed (read-only) for tests and the `aicli user` CLI status output.
109
126
  */
127
+ async loginAsync(username, password) {
128
+ this.refresh();
129
+ if (!validCredentials(username, password)) return null;
130
+ username = username.trim().toLowerCase();
131
+ if (this.getLockState(username).locked) return null;
132
+ const user = this.db.users.find((u) => u.username === username);
133
+ if (!user) {
134
+ this.recordFailedLogin(username);
135
+ return null;
136
+ }
137
+ const snapshot = JSON.stringify(user);
138
+ const legacy = !user.hashVersion || user.hashVersion < 2;
139
+ const hash = legacy ? this.hashPasswordLegacy(password, user.salt) : await hashPasswordAsync(password, user.salt);
140
+ const salt = legacy ? randomBytes(16).toString("hex") : user.salt;
141
+ const upgraded = legacy ? await hashPasswordAsync(password, salt) : hash;
142
+ this.refresh();
143
+ const current = this.db.users.find((u) => u.username === username);
144
+ if (!current || JSON.stringify(current) !== snapshot || this.getLockState(username).locked) return null;
145
+ const a = Buffer.from(hash), b = Buffer.from(current.passwordHash);
146
+ if (a.length !== b.length || !timingSafeEqual(a, b)) {
147
+ this.recordFailedLogin(username);
148
+ return null;
149
+ }
150
+ if (legacy) {
151
+ current.salt = salt;
152
+ current.passwordHash = upgraded;
153
+ current.hashVersion = 2;
154
+ this.save();
155
+ }
156
+ loginAttempts.delete(this.attemptKey(username));
157
+ return this.createToken(username);
158
+ }
110
159
  getLockState(username) {
111
160
  username = username.trim().toLowerCase();
112
- const state = loginAttempts.get(username);
161
+ const state = loginAttempts.get(this.attemptKey(username));
113
162
  if (!state) return { locked: false, remainingMs: 0, fails: 0 };
114
- if (state.lockedUntil > 0 && Date.now() >= state.lockedUntil) {
115
- loginAttempts.delete(username);
163
+ if (Date.now() >= state.expiresAt) {
164
+ loginAttempts.delete(this.attemptKey(username));
116
165
  return { locked: false, remainingMs: 0, fails: 0 };
117
166
  }
118
167
  if (state.lockedUntil > 0) {
@@ -120,8 +169,15 @@ var AuthManager = class {
120
169
  }
121
170
  return { locked: false, remainingMs: 0, fails: state.fails };
122
171
  }
172
+ attemptKey(username) {
173
+ return this.usersFile + "\0" + username;
174
+ }
123
175
  recordFailedLogin(username) {
124
- const state = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0 };
176
+ username = this.attemptKey(username);
177
+ const now = Date.now();
178
+ for (const [key, value] of loginAttempts) if (value.expiresAt <= now) loginAttempts.delete(key);
179
+ if (!loginAttempts.has(username) && loginAttempts.size >= 1e4) loginAttempts.delete(loginAttempts.keys().next().value);
180
+ const state = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0, expiresAt: now + LOGIN_LOCKOUT_MS };
125
181
  state.fails += 1;
126
182
  if (state.fails >= LOGIN_MAX_FAILS) {
127
183
  state.lockedUntil = Date.now() + LOGIN_LOCKOUT_MS;
@@ -131,6 +187,8 @@ var AuthManager = class {
131
187
  /** Verify a token. Returns username or null. */
132
188
  verifyToken(token) {
133
189
  try {
190
+ this.refresh();
191
+ if (typeof token !== "string" || token.length > 4096) return null;
134
192
  const parts = token.split(".");
135
193
  if (parts.length !== 2) return null;
136
194
  const [payloadB64, signature] = parts;
@@ -141,7 +199,7 @@ var AuthManager = class {
141
199
  const payload = JSON.parse(
142
200
  Buffer.from(payloadB64, "base64url").toString("utf-8")
143
201
  );
144
- if (Date.now() > payload.exp) return null;
202
+ if (!Number.isFinite(payload.exp) || Date.now() >= payload.exp) return null;
145
203
  const user = this.db.users.find((u) => u.username === payload.username);
146
204
  if (!user) return null;
147
205
  if (user.tokensRevokedBefore && (!payload.iat || payload.iat < user.tokensRevokedBefore)) {
@@ -158,6 +216,7 @@ var AuthManager = class {
158
216
  * v0.4.114). Returns true if the user existed.
159
217
  */
160
218
  logoutAll(username) {
219
+ this.refresh();
161
220
  username = username.trim().toLowerCase();
162
221
  const user = this.db.users.find((u) => u.username === username);
163
222
  if (!user) return false;
@@ -167,16 +226,19 @@ var AuthManager = class {
167
226
  }
168
227
  /** Get user's data directory (absolute path) */
169
228
  getUserDataDir(username) {
229
+ this.refresh();
170
230
  const user = this.db.users.find((u) => u.username === username);
171
231
  if (!user) throw new Error(`User not found: ${username}`);
172
232
  return join(this.baseDir, user.dataDir);
173
233
  }
174
234
  /** List all usernames */
175
235
  listUsers() {
236
+ this.refresh();
176
237
  return this.db.users.map((u) => u.username);
177
238
  }
178
239
  /** Delete a user. Returns error string or null on success. */
179
240
  deleteUser(username) {
241
+ this.refresh();
180
242
  username = username.trim().toLowerCase();
181
243
  const idx = this.db.users.findIndex((u) => u.username === username);
182
244
  if (idx === -1) return `User '${username}' not found`;
@@ -186,10 +248,11 @@ var AuthManager = class {
186
248
  }
187
249
  /** Reset a user's password. Returns error string or null on success. */
188
250
  resetPassword(username, newPassword) {
251
+ this.refresh();
189
252
  username = username.trim().toLowerCase();
190
253
  const user = this.db.users.find((u) => u.username === username);
191
254
  if (!user) return `User '${username}' not found`;
192
- if (!newPassword || newPassword.length < 4) {
255
+ if (typeof newPassword !== "string" || newPassword.length > 1024 || newPassword.length < 4) {
193
256
  return "Password must be at least 4 characters";
194
257
  }
195
258
  const salt = randomBytes(16).toString("hex");
@@ -209,7 +272,7 @@ var AuthManager = class {
209
272
  if (existsSync(globalConfig)) {
210
273
  try {
211
274
  const content = readFileSync(globalConfig, "utf-8");
212
- writeFileSync(join(userDir, "config.json"), content, "utf-8");
275
+ atomicWriteFileSync(join(userDir, "config.json"), content, { mode: 384 });
213
276
  } catch {
214
277
  }
215
278
  }
@@ -217,7 +280,7 @@ var AuthManager = class {
217
280
  if (existsSync(globalMemory)) {
218
281
  try {
219
282
  const content = readFileSync(globalMemory, "utf-8");
220
- writeFileSync(join(userDir, "memory.md"), content, "utf-8");
283
+ atomicWriteFileSync(join(userDir, "memory.md"), content, { mode: 384 });
221
284
  } catch {
222
285
  }
223
286
  }
@@ -228,7 +291,7 @@ var AuthManager = class {
228
291
  const userHistory = join(userDir, "history");
229
292
  for (const f of files) {
230
293
  try {
231
- copyFileSync(join(globalHistory, f), join(userHistory, f));
294
+ atomicWriteFileSync(join(userHistory, f), readFileSync(join(globalHistory, f), "utf8"), { mode: 384 });
232
295
  } catch {
233
296
  }
234
297
  }
@@ -238,13 +301,22 @@ var AuthManager = class {
238
301
  return null;
239
302
  }
240
303
  // ── Private methods ────────────────────────────────────────────
304
+ refresh() {
305
+ const raw = readFileSync(this.usersFile, "utf8");
306
+ const parsed = JSON.parse(raw);
307
+ if (!this.isValidUsersDB(parsed)) throw new Error("Authentication database is invalid; access refused");
308
+ this.db = parsed;
309
+ this.loadedRaw = raw;
310
+ }
241
311
  loadOrCreate() {
242
312
  if (existsSync(this.usersFile)) {
243
313
  try {
244
- const parsed = JSON.parse(readFileSync(this.usersFile, "utf-8"));
314
+ const raw = readFileSync(this.usersFile, "utf-8");
315
+ const parsed = JSON.parse(raw);
245
316
  if (!this.isValidUsersDB(parsed)) {
246
317
  throw new Error("database structure is invalid");
247
318
  }
319
+ this.loadedRaw = raw;
248
320
  return parsed;
249
321
  } catch (err) {
250
322
  const detail = err instanceof Error ? err.message : String(err);
@@ -287,8 +359,19 @@ var AuthManager = class {
287
359
  this.saveDB(this.db);
288
360
  }
289
361
  saveDB(db) {
290
- mkdirSync(this.baseDir, { recursive: true });
291
- atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
362
+ mkdirSync(this.baseDir, { recursive: true, mode: 448 });
363
+ const lock = this.usersFile + ".lock";
364
+ const fd = openSync(lock, "wx", 384);
365
+ try {
366
+ const current = existsSync(this.usersFile) ? readFileSync(this.usersFile, "utf8") : void 0;
367
+ if (current !== this.loadedRaw) throw new Error("Authentication database changed concurrently; retry the operation");
368
+ const raw = JSON.stringify(db, null, 2);
369
+ atomicWriteFileSync(this.usersFile, raw, { mode: 384 });
370
+ this.loadedRaw = raw;
371
+ } finally {
372
+ closeSync(fd);
373
+ unlinkSync(lock);
374
+ }
292
375
  }
293
376
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
294
377
  hashPasswordLegacy(password, salt) {
@@ -312,6 +395,19 @@ var AuthManager = class {
312
395
  return createHmac("sha256", this.db.secret).update(data).digest("base64url");
313
396
  }
314
397
  };
398
+ function validCredentials(username, password) {
399
+ return typeof username === "string" && /^[a-z0-9_-]{2,32}$/i.test(username.trim()) && typeof password === "string" && password.length > 0 && password.length <= 1024;
400
+ }
401
+ var activeHashes = 0;
402
+ async function hashPasswordAsync(password, salt) {
403
+ if (activeHashes >= 4) throw new Error("Authentication capacity reached; try again later");
404
+ activeHashes++;
405
+ try {
406
+ return await new Promise((resolve, reject) => pbkdf2(password, salt, 1e5, 64, "sha512", (error, hash) => error ? reject(error) : resolve(hash.toString("hex"))));
407
+ } finally {
408
+ activeHashes--;
409
+ }
410
+ }
315
411
 
316
412
  export {
317
413
  TOKEN_EXPIRY_MS,
@@ -6,18 +6,20 @@ import {
6
6
  } from "./chunk-KHYD3WXE.js";
7
7
  import {
8
8
  getIndexMtimeMap
9
- } from "./chunk-NYNSYLFY.js";
9
+ } from "./chunk-JL44HGZC.js";
10
10
  import {
11
11
  redactString
12
12
  } from "./chunk-3FEZSLGI.js";
13
13
  import {
14
14
  atomicWriteFileSync
15
- } from "./chunk-IW3Q7AE5.js";
15
+ } from "./chunk-BMLQPSHP.js";
16
+ import {
17
+ getExecutionConfigDir
18
+ } from "./chunk-SXIJLSVF.js";
16
19
 
17
20
  // src/memory/chat-index.ts
18
21
  import fs from "fs";
19
22
  import path from "path";
20
- import os from "os";
21
23
  import crypto from "crypto";
22
24
  var MEMORY_DIR_NAME = "memory-index";
23
25
  var CHUNKS_FILE = "chunks.json";
@@ -26,7 +28,7 @@ var VEC_MAGIC = 1094929750;
26
28
  var VEC_VERSION = 1;
27
29
  var VEC_HEADER_BYTES = 16;
28
30
  function memoryIndexDir() {
29
- return path.join(os.homedir(), ".aicli", MEMORY_DIR_NAME);
31
+ return path.join(getExecutionConfigDir(), MEMORY_DIR_NAME);
30
32
  }
31
33
  function chunksPath() {
32
34
  return path.join(memoryIndexDir(), CHUNKS_FILE);
@@ -35,7 +37,7 @@ function vectorsPath() {
35
37
  return path.join(memoryIndexDir(), VECTORS_FILE);
36
38
  }
37
39
  function historyDir() {
38
- return path.join(os.homedir(), ".aicli", "history");
40
+ return path.join(getExecutionConfigDir(), "history");
39
41
  }
40
42
  var MAX_CHUNK_CHARS = 1200;
41
43
  var MIN_CHUNK_CHARS = 40;
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-FSC6KEWU.js";
5
5
  import {
6
6
  atomicWriteFileSync
7
- } from "./chunk-6BUTA5VW.js";
7
+ } from "./chunk-2XMUCC42.js";
8
8
  import {
9
9
  getGitRoot
10
10
  } from "./chunk-CGLYJRFA.js";
@@ -12,7 +12,7 @@ import {
12
12
  MEMORY_FILE_NAME,
13
13
  MEMORY_MAX_CHARS,
14
14
  MEMORY_STORE_FILE_NAME
15
- } from "./chunk-JDT6UG6R.js";
15
+ } from "./chunk-2DYQDLLF.js";
16
16
 
17
17
  // src/memory/persistent-memory.ts
18
18
  import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
@@ -6,13 +6,13 @@ import {
6
6
  safeDate,
7
7
  searchCandidates,
8
8
  updateSessionInIndex
9
- } from "./chunk-NYNSYLFY.js";
9
+ } from "./chunk-JL44HGZC.js";
10
10
  import {
11
11
  redactJson
12
12
  } from "./chunk-3FEZSLGI.js";
13
13
  import {
14
- atomicWriteFileSync
15
- } from "./chunk-IW3Q7AE5.js";
14
+ atomicWriteFile
15
+ } from "./chunk-BMLQPSHP.js";
16
16
 
17
17
  // src/session/session-manager.ts
18
18
  import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from "fs";
@@ -616,26 +616,33 @@ var SessionManager = class {
616
616
  }
617
617
  /** P1-PERF-03: `force` 跳过脏检查(用于进程退出、fork 等必须落盘场景) */
618
618
  async save(force = false) {
619
- if (!this._current) return;
620
- if (this._current.messages.length === 0) return;
621
- if (!force && !this._current.dirty) return;
619
+ const session = this._current;
620
+ if (!session) return;
621
+ if (session.messages.length === 0) return;
622
+ if (!force && !session.dirty) return;
622
623
  mkdirSync(this.historyDir, { recursive: true });
623
- const filePath = join(this.historyDir, `${this._current.id}.json`);
624
- const raw = this._current.toJSON();
624
+ const filePath = join(this.historyDir, `${session.id}.json`);
625
+ const raw = session.toJSON();
625
626
  const opts = this.redactOptionsForSave();
626
627
  const { value: payload, hits } = redactJson(raw, opts);
627
628
  this.lastRedactionHits = hits.length;
628
- atomicWriteFileSync(filePath, JSON.stringify(payload, null, 2));
629
- this._current.dirty = false;
629
+ const serialized = JSON.stringify(payload, null, 2);
630
+ session.dirty = false;
631
+ try {
632
+ await atomicWriteFile(filePath, serialized, { mode: 384 });
633
+ } catch (error) {
634
+ session.dirty = true;
635
+ throw error;
636
+ }
630
637
  try {
631
638
  updateSessionInIndex(this.historyDir, {
632
- id: this._current.id,
633
- provider: this._current.provider,
634
- model: this._current.model,
635
- title: this._current.title,
636
- messageCount: this._current.messages.length,
637
- created: this._current.created,
638
- updated: this._current.updated
639
+ id: session.id,
640
+ provider: session.provider,
641
+ model: session.model,
642
+ title: session.title,
643
+ messageCount: session.messages.length,
644
+ created: session.created,
645
+ updated: session.updated
639
646
  }, filePath);
640
647
  } catch {
641
648
  }
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  loadIndex,
4
4
  symbolFingerprint
5
- } from "./chunk-WQ3OMEC4.js";
5
+ } from "./chunk-M6HLYG33.js";
6
6
  import {
7
7
  loadVectorStore,
8
8
  saveVectorStore,
9
9
  searchVectorStore
10
- } from "./chunk-NRSYBKWA.js";
10
+ } from "./chunk-C7CKDFJS.js";
11
11
  import {
12
12
  EMBEDDING_DIM,
13
13
  embed,