jinzd-ai-cli 0.4.113 → 0.4.115

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,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AuthManager,
4
+ TOKEN_EXPIRY_MS,
5
+ __resetLoginAttemptsForTests
6
+ } from "./chunk-O7NM4WTS.js";
7
+ import "./chunk-PDX44BCA.js";
8
+ export {
9
+ AuthManager,
10
+ TOKEN_EXPIRY_MS,
11
+ __resetLoginAttemptsForTests
12
+ };
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigManager
4
- } from "./chunk-WLZ2PWQV.js";
4
+ } from "./chunk-UZLNS3QG.js";
5
5
  import "./chunk-2ZD3YTVM.js";
6
- import "./chunk-SRU5SYZI.js";
6
+ import "./chunk-OHUHYWBR.js";
7
7
  import "./chunk-PDX44BCA.js";
8
8
 
9
9
  // src/cli/batch.ts
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  schemaToJsonSchema,
4
4
  truncateForPersist
5
- } from "./chunk-3GUNDGUV.js";
5
+ } from "./chunk-TJGRPTJS.js";
6
6
  import {
7
7
  AuthError,
8
8
  ProviderError,
@@ -18,7 +18,7 @@ import {
18
18
  MCP_PROTOCOL_VERSION,
19
19
  MCP_TOOL_PREFIX,
20
20
  VERSION
21
- } from "./chunk-SRU5SYZI.js";
21
+ } from "./chunk-OHUHYWBR.js";
22
22
  import {
23
23
  redactJson
24
24
  } from "./chunk-7ZJN4KLV.js";
@@ -72,10 +72,20 @@ var ClaudeProvider = class extends BaseProvider {
72
72
  ]
73
73
  };
74
74
  async initialize(apiKey, options) {
75
- this.client = new Anthropic({
75
+ const clientOptions = {
76
76
  apiKey,
77
77
  baseURL: options?.baseUrl
78
- });
78
+ };
79
+ const proxyUrl = options?.proxy;
80
+ try {
81
+ const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
82
+ const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
83
+ const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
84
+ const dispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT }) : new Agent({ bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT });
85
+ clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
86
+ } catch {
87
+ }
88
+ this.client = new Anthropic(clientOptions);
79
89
  }
80
90
  /**
81
91
  * 将内部 MessageContentPart[] 格式转换为 Anthropic SDK 期望的 ContentBlockParam[]。
@@ -932,13 +942,20 @@ var OpenAICompatibleProvider = class extends BaseProvider {
932
942
  timeout: this.defaultTimeout
933
943
  };
934
944
  const proxyUrl = options?.proxy;
935
- if (proxyUrl) {
936
- try {
937
- const { ProxyAgent, fetch: undiciFetch } = await import("undici");
938
- const agent = new ProxyAgent({ uri: proxyUrl });
939
- clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher: agent }));
940
- } catch {
941
- }
945
+ try {
946
+ const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
947
+ const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
948
+ const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
949
+ const dispatcher = proxyUrl ? new ProxyAgent({
950
+ uri: proxyUrl,
951
+ bodyTimeout: STREAM_BODY_TIMEOUT,
952
+ headersTimeout: STREAM_HEADERS_TIMEOUT
953
+ }) : new Agent({
954
+ bodyTimeout: STREAM_BODY_TIMEOUT,
955
+ headersTimeout: STREAM_HEADERS_TIMEOUT
956
+ });
957
+ clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
958
+ } catch {
942
959
  }
943
960
  this.client = new OpenAI(clientOptions);
944
961
  }
@@ -1854,6 +1871,40 @@ function peelMetaNarration(content) {
1854
1871
  }
1855
1872
  return out.trim();
1856
1873
  }
1874
+ var META_NARRATION_HARD_MARKERS = [
1875
+ /\[⚠️\s*CONTENT GENERATION MODE\]/,
1876
+ /CONTENT_ONLY_STREAM_REMINDER\b/,
1877
+ /<system-reminder>/i
1878
+ ];
1879
+ var META_NARRATION_HEURISTICS = [
1880
+ /\bthe user (?:is asking me|wants me|is requesting|expects me)\b/i,
1881
+ /\blet me (?:re-?read|re-?consider|reconsider|think about|carefully (?:re-?read|consider))\b/i,
1882
+ /\bI'?m (?:in (?:a )?content-only|in CONTENT-ONLY|currently in)\b/i,
1883
+ /\bI think (?:there might be|I should|I cannot|the (?:user|best)|maybe)\b/i,
1884
+ /\bWait,?\s+let me\b/i,
1885
+ /\bActually,?\s+I\b/i,
1886
+ /\bI need to be honest with the user\b/i,
1887
+ /\bI(?:'m| am) in a special mode\b/i,
1888
+ /\bGiven that I cannot\b/i
1889
+ ];
1890
+ function detectMetaNarration(content) {
1891
+ if (!content) return null;
1892
+ const head = content.slice(0, 2e3);
1893
+ for (const re of META_NARRATION_HARD_MARKERS) {
1894
+ if (re.test(head)) return re.source;
1895
+ }
1896
+ if (/^#{1,3}\s+\S/m.test(head)) return null;
1897
+ let hits = 0;
1898
+ let firstMatch = "";
1899
+ for (const re of META_NARRATION_HEURISTICS) {
1900
+ if (re.test(head)) {
1901
+ hits++;
1902
+ if (!firstMatch) firstMatch = re.source;
1903
+ if (hits >= 2) return `meta-narration:${firstMatch}`;
1904
+ }
1905
+ }
1906
+ return null;
1907
+ }
1857
1908
  function looksLikeDocumentBody(content) {
1858
1909
  if (!content || content.length < 200) return false;
1859
1910
  if (/^#{1,6}\s+\S/m.test(content)) return true;
@@ -4156,6 +4207,7 @@ export {
4156
4207
  buildPhantomCorrectionMessage,
4157
4208
  detectPseudoToolCalls,
4158
4209
  stripPseudoToolCalls,
4210
+ detectMetaNarration,
4159
4211
  looksLikeDocumentBody,
4160
4212
  stripToolCallReminder,
4161
4213
  TEE_FINAL_USER_NUDGE,
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.113";
9
+ var VERSION = "0.4.115";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -1,12 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/web/auth.ts
4
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from "fs";
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, renameSync, unlinkSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
7
7
  var USERS_FILE = "users.json";
8
- var TOKEN_EXPIRY_HOURS = 24 * 7;
8
+ var TOKEN_EXPIRY_HOURS = 24;
9
+ var TOKEN_EXPIRY_MS = TOKEN_EXPIRY_HOURS * 3600 * 1e3;
9
10
  var USERS_DIR = "users";
11
+ var LOGIN_MAX_FAILS = 5;
12
+ var LOGIN_LOCKOUT_MS = 15 * 60 * 1e3;
13
+ var loginAttempts = /* @__PURE__ */ new Map();
14
+ function __resetLoginAttemptsForTests() {
15
+ loginAttempts.clear();
16
+ }
10
17
  var AuthManager = class {
11
18
  usersFile;
12
19
  baseDir;
@@ -57,16 +64,30 @@ var AuthManager = class {
57
64
  this.save();
58
65
  return null;
59
66
  }
60
- /** Authenticate user. Returns JWT token or null. */
67
+ /**
68
+ * Authenticate user. Returns JWT token or null on failure.
69
+ *
70
+ * Audit closure (5th audit, v0.4.114): integrates failed-login lockout
71
+ * (CWE-307). After {@link LOGIN_MAX_FAILS} consecutive failures within a
72
+ * lockout window, further attempts return null without checking the
73
+ * password (avoiding pbkdf2 work + leaking timing). Successful login
74
+ * resets the counter.
75
+ */
61
76
  login(username, password) {
62
77
  username = username.trim().toLowerCase();
78
+ const lockState = this.getLockState(username);
79
+ if (lockState.locked) return null;
63
80
  const user = this.db.users.find((u) => u.username === username);
64
- if (!user) return null;
81
+ if (!user) {
82
+ this.recordFailedLogin(username);
83
+ return null;
84
+ }
65
85
  const isLegacy = !user.hashVersion || user.hashVersion < 2;
66
86
  const hash = isLegacy ? this.hashPasswordLegacy(password, user.salt) : this.hashPassword(password, user.salt);
67
87
  const a = Buffer.from(hash, "utf-8");
68
88
  const b = Buffer.from(user.passwordHash, "utf-8");
69
89
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
90
+ this.recordFailedLogin(username);
70
91
  return null;
71
92
  }
72
93
  if (isLegacy) {
@@ -76,8 +97,34 @@ var AuthManager = class {
76
97
  user.hashVersion = 2;
77
98
  this.save();
78
99
  }
100
+ loginAttempts.delete(username);
79
101
  return this.createToken(username);
80
102
  }
103
+ /**
104
+ * Returns current lockout state for a username and lazily expires it.
105
+ * Exposed (read-only) for tests and the `aicli user` CLI status output.
106
+ */
107
+ getLockState(username) {
108
+ username = username.trim().toLowerCase();
109
+ const state = loginAttempts.get(username);
110
+ if (!state) return { locked: false, remainingMs: 0, fails: 0 };
111
+ if (state.lockedUntil > 0 && Date.now() >= state.lockedUntil) {
112
+ loginAttempts.delete(username);
113
+ return { locked: false, remainingMs: 0, fails: 0 };
114
+ }
115
+ if (state.lockedUntil > 0) {
116
+ return { locked: true, remainingMs: state.lockedUntil - Date.now(), fails: state.fails };
117
+ }
118
+ return { locked: false, remainingMs: 0, fails: state.fails };
119
+ }
120
+ recordFailedLogin(username) {
121
+ const state = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0 };
122
+ state.fails += 1;
123
+ if (state.fails >= LOGIN_MAX_FAILS) {
124
+ state.lockedUntil = Date.now() + LOGIN_LOCKOUT_MS;
125
+ }
126
+ loginAttempts.set(username, state);
127
+ }
81
128
  /** Verify a token. Returns username or null. */
82
129
  verifyToken(token) {
83
130
  try {
@@ -90,12 +137,29 @@ var AuthManager = class {
90
137
  Buffer.from(payloadB64, "base64url").toString("utf-8")
91
138
  );
92
139
  if (Date.now() > payload.exp) return null;
93
- if (!this.db.users.find((u) => u.username === payload.username)) return null;
140
+ const user = this.db.users.find((u) => u.username === payload.username);
141
+ if (!user) return null;
142
+ if (user.tokensRevokedBefore && (!payload.iat || payload.iat < user.tokensRevokedBefore)) {
143
+ return null;
144
+ }
94
145
  return payload.username;
95
146
  } catch {
96
147
  return null;
97
148
  }
98
149
  }
150
+ /**
151
+ * Revoke every outstanding token for the given user by bumping the
152
+ * `tokensRevokedBefore` watermark to now. Audit closure (5th audit,
153
+ * v0.4.114). Returns true if the user existed.
154
+ */
155
+ logoutAll(username) {
156
+ username = username.trim().toLowerCase();
157
+ const user = this.db.users.find((u) => u.username === username);
158
+ if (!user) return false;
159
+ user.tokensRevokedBefore = Date.now() + 1;
160
+ this.save();
161
+ return true;
162
+ }
99
163
  /** Get user's data directory (absolute path) */
100
164
  getUserDataDir(username) {
101
165
  const user = this.db.users.find((u) => u.username === username);
@@ -127,6 +191,7 @@ var AuthManager = class {
127
191
  user.passwordHash = this.hashPassword(newPassword, salt);
128
192
  user.salt = salt;
129
193
  user.hashVersion = 2;
194
+ user.tokensRevokedBefore = Date.now() + 1;
130
195
  this.save();
131
196
  return null;
132
197
  }
@@ -188,7 +253,17 @@ var AuthManager = class {
188
253
  }
189
254
  saveDB(db) {
190
255
  mkdirSync(this.baseDir, { recursive: true });
191
- writeFileSync(this.usersFile, JSON.stringify(db, null, 2), "utf-8");
256
+ const tmp = `${this.usersFile}.tmp`;
257
+ try {
258
+ writeFileSync(tmp, JSON.stringify(db, null, 2), "utf-8");
259
+ renameSync(tmp, this.usersFile);
260
+ } catch (err) {
261
+ try {
262
+ unlinkSync(tmp);
263
+ } catch {
264
+ }
265
+ throw err;
266
+ }
192
267
  }
193
268
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
194
269
  hashPasswordLegacy(password, salt) {
@@ -198,9 +273,11 @@ var AuthManager = class {
198
273
  return pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
199
274
  }
200
275
  createToken(username) {
276
+ const now = Date.now();
201
277
  const payload = {
202
278
  username,
203
- exp: Date.now() + TOKEN_EXPIRY_HOURS * 3600 * 1e3
279
+ iat: now,
280
+ exp: now + TOKEN_EXPIRY_HOURS * 3600 * 1e3
204
281
  };
205
282
  const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
206
283
  const signature = this.sign(payloadB64);
@@ -212,5 +289,7 @@ var AuthManager = class {
212
289
  };
213
290
 
214
291
  export {
292
+ TOKEN_EXPIRY_MS,
293
+ __resetLoginAttemptsForTests,
215
294
  AuthManager
216
295
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.113";
4
+ var VERSION = "0.4.115";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-SRU5SYZI.js";
4
+ } from "./chunk-OHUHYWBR.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync, spawnSync } from "child_process";
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-3BICTI5M.js";
6
6
  import {
7
7
  runTestsTool
8
- } from "./chunk-SIDKPVRD.js";
8
+ } from "./chunk-PEMNYHIS.js";
9
9
  import {
10
10
  EnvLoader,
11
11
  NetworkError,
@@ -18,7 +18,7 @@ import {
18
18
  SUBAGENT_ALLOWED_TOOLS,
19
19
  SUBAGENT_DEFAULT_MAX_ROUNDS,
20
20
  SUBAGENT_MAX_ROUNDS_LIMIT
21
- } from "./chunk-SRU5SYZI.js";
21
+ } from "./chunk-OHUHYWBR.js";
22
22
  import {
23
23
  fileCheckpoints
24
24
  } from "./chunk-4BKXL7SM.js";
@@ -229,10 +229,36 @@ function resetInterrupt() {
229
229
  interrupted = false;
230
230
  }
231
231
 
232
+ // src/tools/session-context.ts
233
+ import { AsyncLocalStorage } from "async_hooks";
234
+ var als = new AsyncLocalStorage();
235
+ var DEFAULT_SESSION_KEY = "__default__";
236
+ function getCurrentSessionKey() {
237
+ const ctx = als.getStore();
238
+ if (!ctx || !ctx.sessionKey) return DEFAULT_SESSION_KEY;
239
+ return ctx.sessionKey;
240
+ }
241
+ function runWithSessionKey(sessionKey, fn) {
242
+ const key = sessionKey && sessionKey.length > 0 ? sessionKey : DEFAULT_SESSION_KEY;
243
+ return als.run({ sessionKey: key }, fn);
244
+ }
245
+
232
246
  // src/tools/builtin/bash.ts
233
247
  var IS_WINDOWS = platform() === "win32";
234
248
  var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
235
- var persistentCwd = process.cwd();
249
+ var cwdBySession = /* @__PURE__ */ new Map();
250
+ function getCwd() {
251
+ const key = getCurrentSessionKey();
252
+ let cwd = cwdBySession.get(key);
253
+ if (!cwd) {
254
+ cwd = process.cwd();
255
+ cwdBySession.set(key, cwd);
256
+ }
257
+ return cwd;
258
+ }
259
+ function setCwd(next) {
260
+ cwdBySession.set(getCurrentSessionKey(), next);
261
+ }
236
262
  var bashTool = {
237
263
  definition: {
238
264
  name: "bash",
@@ -277,17 +303,19 @@ Important rules:
277
303
  if (!command.trim()) {
278
304
  throw new ToolError("bash", "command is required");
279
305
  }
280
- if (!existsSync2(persistentCwd)) {
306
+ let currentCwd = getCwd();
307
+ if (!existsSync2(currentCwd)) {
281
308
  const fallback = process.cwd();
282
309
  process.stderr.write(
283
- `[bash] Previous cwd "${persistentCwd}" no longer exists, reset to "${fallback}"
310
+ `[bash] Previous cwd "${currentCwd}" no longer exists, reset to "${fallback}"
284
311
  `
285
312
  );
286
- persistentCwd = fallback;
313
+ currentCwd = fallback;
314
+ setCwd(fallback);
287
315
  }
288
- let effectiveCwd = persistentCwd;
316
+ let effectiveCwd = currentCwd;
289
317
  if (cwdArg) {
290
- const resolved = resolve(persistentCwd, cwdArg);
318
+ const resolved = resolve(currentCwd, cwdArg);
291
319
  if (!existsSync2(resolved)) {
292
320
  throw new ToolError(
293
321
  "bash",
@@ -295,7 +323,7 @@ Important rules:
295
323
  );
296
324
  }
297
325
  effectiveCwd = resolved;
298
- persistentCwd = resolved;
326
+ setCwd(resolved);
299
327
  }
300
328
  let actualCommand;
301
329
  if (IS_WINDOWS) {
@@ -344,7 +372,7 @@ Important rules:
344
372
  }
345
373
  updateCwdFromCommand(command, effectiveCwd);
346
374
  pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
347
- const result = IS_WINDOWS && Buffer.isBuffer(stdout) ? stdout.toString("utf-8") : stdout;
375
+ const result = Buffer.isBuffer(stdout) ? stdout.toString("utf-8") : String(stdout ?? "");
348
376
  return result || "(command completed with no output)";
349
377
  } catch (err) {
350
378
  pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
@@ -551,7 +579,7 @@ function updateCwdFromCommand(command, baseCwd) {
551
579
  try {
552
580
  const newDir = resolve(baseCwd, target);
553
581
  if (existsSync2(newDir)) {
554
- persistentCwd = newDir;
582
+ setCwd(newDir);
555
583
  }
556
584
  } catch {
557
585
  }
@@ -1411,6 +1439,16 @@ var ToolExecutor = class {
1411
1439
  * 通过 /yolo 命令切换。destructive 操作仍会显示警告但不阻塞。
1412
1440
  */
1413
1441
  sessionAutoApprove = false;
1442
+ /**
1443
+ * Logical session key used to scope per-session state in stateful tools
1444
+ * (currently only `bash`'s persistent cwd). Web mode sets this per-tab so
1445
+ * concurrent tabs don't share cwd. CLI/REPL leaves it undefined (the bash
1446
+ * tool falls back to a default key).
1447
+ */
1448
+ sessionKey;
1449
+ setSessionKey(key) {
1450
+ this.sessionKey = key;
1451
+ }
1414
1452
  /**
1415
1453
  * 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
1416
1454
  * 若当前没有 confirm() 进行中,无操作。
@@ -1443,6 +1481,9 @@ var ToolExecutor = class {
1443
1481
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
1444
1482
  }
1445
1483
  async execute(call) {
1484
+ return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
1485
+ }
1486
+ async executeInner(call) {
1446
1487
  const tool = this.registry.get(call.name);
1447
1488
  if (!tool) {
1448
1489
  return {
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-SRU5SYZI.js";
11
+ } from "./chunk-OHUHYWBR.js";
12
12
 
13
13
  // src/config/config-manager.ts
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -36,7 +36,7 @@ import {
36
36
  TEST_TIMEOUT,
37
37
  VERSION,
38
38
  buildUserIdentityPrompt
39
- } from "./chunk-SRU5SYZI.js";
39
+ } from "./chunk-OHUHYWBR.js";
40
40
  import "./chunk-PDX44BCA.js";
41
41
  export {
42
42
  AGENTIC_BEHAVIOR_GUIDELINE,