jinzd-ai-cli 0.4.87 → 0.4.89

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.
@@ -36,7 +36,7 @@ import {
36
36
  VERSION,
37
37
  buildUserIdentityPrompt,
38
38
  runTestsTool
39
- } from "./chunk-AQX3GYRD.js";
39
+ } from "./chunk-4WVXTADR.js";
40
40
  import {
41
41
  hasSemanticIndex,
42
42
  semanticSearch
@@ -44,7 +44,10 @@ import {
44
44
  import {
45
45
  loadIndex
46
46
  } from "./chunk-BJAT4GNC.js";
47
- import "./chunk-XMA222FQ.js";
47
+ import {
48
+ EMBEDDING_DIM,
49
+ embedOne
50
+ } from "./chunk-XMA222FQ.js";
48
51
 
49
52
  // src/web/server.ts
50
53
  import express from "express";
@@ -223,6 +226,24 @@ var ConfigSchema = z.object({
223
226
  // 必须确认插件来源可信后,再设为 true 启用。
224
227
  // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
225
228
  allowPlugins: z.boolean().default(false),
229
+ // 敏感信息脱敏(v0.4.88+,2026-04 凭据泄漏事件后引入)
230
+ // 会话保存到磁盘 / 发送给 Provider 前,自动将 API key、密码、PEM 私钥等
231
+ // 按正则替换为 [REDACTED:kind] 占位符。模式定义在 src/security/redactor.ts。
232
+ //
233
+ // redactOnSave 默认 true:保存到 ~/.aicli/history/*.json 时脱敏(推荐开启)
234
+ // redactOnSend 默认 false:发送到 Provider 时脱敏(激进,可能影响工具结果)
235
+ // mode:
236
+ // 'default' — 使用内置 13 条模式(推荐)
237
+ // 'strict' — 同 default,但阈值更低(更激进,可能误报)
238
+ // 'off' — 禁用所有模式(等同 redactOnSave=false)
239
+ // customPatterns — 用户补充正则字符串(支持 /pattern/flags 形式),
240
+ // 无效正则会静默跳过,不会中断保存。
241
+ security: z.object({
242
+ redactOnSave: z.boolean().default(true),
243
+ redactOnSend: z.boolean().default(false),
244
+ mode: z.enum(["default", "strict", "off"]).default("default"),
245
+ customPatterns: z.array(z.string()).default([])
246
+ }).default({}),
226
247
  // 智能模型路由(v0.4.68+)
227
248
  // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
228
249
  // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
@@ -414,8 +435,8 @@ ${err}`
414
435
  return EnvLoader.getDefaultProvider() ?? this.config.defaultProvider;
415
436
  }
416
437
  /** 点分路径读取配置值,如 `ui.theme` → config.ui.theme */
417
- getByPath(path3) {
418
- const keys = path3.split(".");
438
+ getByPath(path4) {
439
+ const keys = path4.split(".");
419
440
  let current = this.config;
420
441
  for (const key of keys) {
421
442
  if (current == null || typeof current !== "object") return void 0;
@@ -424,8 +445,8 @@ ${err}`
424
445
  return current;
425
446
  }
426
447
  /** 点分路径写入配置值,自动类型转换(boolean/number/string)并持久化 */
427
- setByPath(path3, rawValue) {
428
- const keys = path3.split(".");
448
+ setByPath(path4, rawValue) {
449
+ const keys = path4.split(".");
429
450
  if (keys.length === 0) return;
430
451
  let value = rawValue;
431
452
  if (rawValue === "true") value = true;
@@ -444,7 +465,7 @@ ${err}`
444
465
  const result = ConfigSchema.safeParse(draft);
445
466
  if (!result.success) {
446
467
  const firstErr = result.error.errors[0];
447
- throw new ConfigError(`Invalid config value for "${path3}": ${firstErr?.message ?? "validation failed"}`);
468
+ throw new ConfigError(`Invalid config value for "${path4}": ${firstErr?.message ?? "validation failed"}`);
448
469
  }
449
470
  this.config = result.data;
450
471
  this.save();
@@ -3036,6 +3057,104 @@ var Session = class _Session {
3036
3057
  }
3037
3058
  };
3038
3059
 
3060
+ // src/security/redactor.ts
3061
+ var DEFAULT_PATTERNS = [
3062
+ // password: xxx / password = xxx / password="xxx"
3063
+ // Covers YAML / JSON / shell-ish / env-file forms.
3064
+ { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
3065
+ // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
3066
+ { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
3067
+ // JDBC/PG/MySQL/Mongo connection strings with inline credentials
3068
+ // postgresql://user:pass@host/db → redact pass
3069
+ { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
3070
+ // Anthropic API keys
3071
+ { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
3072
+ // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
3073
+ { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
3074
+ // GitHub personal access tokens
3075
+ { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
3076
+ { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
3077
+ { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
3078
+ // Slack tokens
3079
+ { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
3080
+ { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
3081
+ // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
3082
+ // we only catch the ID because secret key alone is indistinguishable from random base64.
3083
+ { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
3084
+ // Google API keys
3085
+ { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
3086
+ // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
3087
+ { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
3088
+ // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
3089
+ { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
3090
+ // Bearer <token> in Authorization headers
3091
+ { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
3092
+ // Private key PEM blocks — catch the header+footer together
3093
+ { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
3094
+ ];
3095
+ function render(placeholder, kind) {
3096
+ return placeholder.replace("{kind}", kind);
3097
+ }
3098
+ function redactString(input, options) {
3099
+ if (!options.enabled || !input) return { redacted: input, hits: [] };
3100
+ const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
3101
+ const patterns = [
3102
+ ...options.patterns ?? DEFAULT_PATTERNS,
3103
+ ...(options.customRegexes ?? []).flatMap((src, i) => {
3104
+ try {
3105
+ const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
3106
+ const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
3107
+ const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
3108
+ return [{ kind: `custom-${i}`, regex }];
3109
+ } catch {
3110
+ return [];
3111
+ }
3112
+ })
3113
+ ];
3114
+ let redacted = input;
3115
+ const hits = [];
3116
+ for (const { kind, regex } of patterns) {
3117
+ const rx = new RegExp(regex.source, regex.flags);
3118
+ redacted = redacted.replace(rx, (...args) => {
3119
+ const match = args[0];
3120
+ const probe = new RegExp(rx.source).exec(match);
3121
+ const captureCount = probe ? probe.length - 1 : 0;
3122
+ const g1 = captureCount >= 1 ? args[1] : void 0;
3123
+ const g2 = captureCount >= 2 ? args[2] : void 0;
3124
+ const offset = args[1 + captureCount];
3125
+ if (captureCount >= 2 && typeof g2 === "string") {
3126
+ hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
3127
+ return `${g1}${render(placeholder, kind)}`;
3128
+ }
3129
+ hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
3130
+ return render(placeholder, kind);
3131
+ });
3132
+ }
3133
+ return { redacted, hits };
3134
+ }
3135
+ function redactJson(value, options) {
3136
+ if (!options.enabled) return { value, hits: [] };
3137
+ const allHits = [];
3138
+ function walk(v) {
3139
+ if (typeof v === "string") {
3140
+ const r = redactString(v, options);
3141
+ allHits.push(...r.hits);
3142
+ return r.redacted;
3143
+ }
3144
+ if (Array.isArray(v)) return v.map(walk);
3145
+ if (v && typeof v === "object") {
3146
+ const out = {};
3147
+ for (const [k, vv] of Object.entries(v)) {
3148
+ out[k] = walk(vv);
3149
+ }
3150
+ return out;
3151
+ }
3152
+ return v;
3153
+ }
3154
+ const redacted = walk(value);
3155
+ return { value: redacted, hits: allHits };
3156
+ }
3157
+
3039
3158
  // src/session/session-manager.ts
3040
3159
  function safeDate(value) {
3041
3160
  const d = new Date(value);
@@ -3049,9 +3168,27 @@ function extractJsonField(header, field) {
3049
3168
  var SessionManager = class {
3050
3169
  _current = null;
3051
3170
  historyDir;
3171
+ config;
3172
+ /** Last save's redaction hit count — exposed for /security status reporting */
3173
+ lastRedactionHits = 0;
3052
3174
  constructor(config) {
3175
+ this.config = config;
3053
3176
  this.historyDir = config.getHistoryDir();
3054
3177
  }
3178
+ /**
3179
+ * Build redaction options from config. Returns `{ enabled: false }` when
3180
+ * `security.redactOnSave` is off or `security.mode` is 'off'.
3181
+ */
3182
+ redactOptionsForSave() {
3183
+ const security = this.config.get("security");
3184
+ if (!security || !security.redactOnSave || security.mode === "off") {
3185
+ return { enabled: false };
3186
+ }
3187
+ return {
3188
+ enabled: true,
3189
+ customRegexes: security.customPatterns ?? []
3190
+ };
3191
+ }
3055
3192
  get current() {
3056
3193
  return this._current;
3057
3194
  }
@@ -3077,8 +3214,12 @@ var SessionManager = class {
3077
3214
  if (!this._current) return;
3078
3215
  mkdirSync2(this.historyDir, { recursive: true });
3079
3216
  const filePath = join2(this.historyDir, `${this._current.id}.json`);
3217
+ const raw = this._current.toJSON();
3218
+ const opts = this.redactOptionsForSave();
3219
+ const { value: payload, hits } = redactJson(raw, opts);
3220
+ this.lastRedactionHits = hits.length;
3080
3221
  const tmpPath = filePath + ".tmp";
3081
- writeFileSync2(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
3222
+ writeFileSync2(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
3082
3223
  renameSync(tmpPath, filePath);
3083
3224
  }
3084
3225
  loadSession(id) {
@@ -4217,8 +4358,8 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
4217
4358
  if (rule.when) {
4218
4359
  if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
4219
4360
  if (rule.when.pathPattern) {
4220
- const path3 = String(args["path"] ?? args["command"] ?? "");
4221
- if (!path3.includes(rule.when.pathPattern)) continue;
4361
+ const path4 = String(args["path"] ?? args["command"] ?? "");
4362
+ if (!path4.includes(rule.when.pathPattern)) continue;
4222
4363
  }
4223
4364
  }
4224
4365
  return rule.action;
@@ -7718,6 +7859,160 @@ ${lines.join("\n")}`;
7718
7859
  }
7719
7860
  };
7720
7861
 
7862
+ // src/memory/chat-index.ts
7863
+ import fs2 from "fs";
7864
+ import path3 from "path";
7865
+ import os from "os";
7866
+ import crypto from "crypto";
7867
+ var MEMORY_DIR_NAME = "memory-index";
7868
+ var CHUNKS_FILE = "chunks.json";
7869
+ var VECTORS_FILE = "vectors.vec";
7870
+ var VEC_MAGIC = 1094929750;
7871
+ var VEC_VERSION = 1;
7872
+ var VEC_HEADER_BYTES = 16;
7873
+ function memoryIndexDir() {
7874
+ return path3.join(os.homedir(), ".aicli", MEMORY_DIR_NAME);
7875
+ }
7876
+ function chunksPath() {
7877
+ return path3.join(memoryIndexDir(), CHUNKS_FILE);
7878
+ }
7879
+ function vectorsPath() {
7880
+ return path3.join(memoryIndexDir(), VECTORS_FILE);
7881
+ }
7882
+ function readVectorsFile(expectedCount) {
7883
+ const p = vectorsPath();
7884
+ if (!fs2.existsSync(p)) return null;
7885
+ let buf;
7886
+ try {
7887
+ buf = fs2.readFileSync(p);
7888
+ } catch {
7889
+ return null;
7890
+ }
7891
+ if (buf.length < VEC_HEADER_BYTES) return null;
7892
+ const magic = buf.readUInt32LE(0);
7893
+ const version = buf.readUInt32LE(4);
7894
+ const count = buf.readUInt32LE(8);
7895
+ const dim = buf.readUInt32LE(12);
7896
+ if (magic !== VEC_MAGIC || version !== VEC_VERSION || dim !== EMBEDDING_DIM) return null;
7897
+ if (count !== expectedCount) return null;
7898
+ const expected = VEC_HEADER_BYTES + count * dim * 4;
7899
+ if (buf.length !== expected) return null;
7900
+ return new Float32Array(
7901
+ buf.buffer.slice(buf.byteOffset + VEC_HEADER_BYTES, buf.byteOffset + expected)
7902
+ );
7903
+ }
7904
+ function readIndexFile() {
7905
+ const p = chunksPath();
7906
+ if (!fs2.existsSync(p)) return null;
7907
+ try {
7908
+ const raw = fs2.readFileSync(p, "utf-8");
7909
+ const data = JSON.parse(raw);
7910
+ if (data.version !== 1) return null;
7911
+ return data;
7912
+ } catch {
7913
+ return null;
7914
+ }
7915
+ }
7916
+ function loadChatIndex() {
7917
+ const idx = readIndexFile();
7918
+ if (!idx) return null;
7919
+ const vectors = readVectorsFile(idx.chunks.length);
7920
+ if (!vectors) return null;
7921
+ return { idx, vectors };
7922
+ }
7923
+ async function searchChatMemory(query, options = {}) {
7924
+ const topK = options.topK ?? 5;
7925
+ const minScore = options.minScore ?? 0.25;
7926
+ const loaded = loadChatIndex();
7927
+ if (!loaded || loaded.idx.chunks.length === 0) return [];
7928
+ const { idx, vectors } = loaded;
7929
+ const { redacted } = redactString(query, { enabled: true });
7930
+ const qvec = await embedOne(redacted);
7931
+ const candidates = [];
7932
+ for (let i = 0; i < idx.chunks.length; i++) {
7933
+ const c = idx.chunks[i];
7934
+ if (options.sessionId && c.sessionId !== options.sessionId) continue;
7935
+ if (options.excludeSessionId && c.sessionId === options.excludeSessionId) continue;
7936
+ let score = 0;
7937
+ const base = i * EMBEDDING_DIM;
7938
+ for (let d = 0; d < EMBEDDING_DIM; d++) {
7939
+ score += vectors[base + d] * qvec[d];
7940
+ }
7941
+ if (score < minScore) continue;
7942
+ candidates.push({ chunk: c, score });
7943
+ }
7944
+ candidates.sort((a, b) => b.score - a.score);
7945
+ return candidates.slice(0, topK);
7946
+ }
7947
+
7948
+ // src/tools/builtin/recall-memory.ts
7949
+ function formatHit(h, i) {
7950
+ const ts = h.chunk.timestamp.slice(0, 16).replace("T", " ");
7951
+ const title = h.chunk.sessionTitle ? ` \xB7 ${h.chunk.sessionTitle}` : "";
7952
+ const sid = h.chunk.sessionId.slice(0, 8);
7953
+ const score = h.score.toFixed(3);
7954
+ const body = h.chunk.text.length > 600 ? h.chunk.text.slice(0, 600) + "\u2026" : h.chunk.text;
7955
+ return `\u2500\u2500\u2500 Hit ${i + 1} (score ${score}, session ${sid}${title}, ${ts}) \u2500\u2500\u2500
7956
+ ` + body;
7957
+ }
7958
+ var recallMemoryTool = {
7959
+ definition: {
7960
+ name: "recall_memory",
7961
+ description: 'Semantic search over past chat sessions. Call this whenever the user references something that may have been discussed before ("last time", "remember", "\u4E4B\u524D", "\u4E0A\u6B21"), when context is ambiguous and continuity matters, or when you want to check what decisions or preferences have been established across prior conversations. Returns up to `topK` relevant snippets with session id, timestamp, and cosine similarity score. Prefer this over asking the user "can you remind me".',
7962
+ parameters: {
7963
+ query: {
7964
+ type: "string",
7965
+ description: "Natural-language description of what to recall. Chinese or English both work.",
7966
+ required: true
7967
+ },
7968
+ topK: {
7969
+ type: "number",
7970
+ description: "Max number of snippets to return (default 5, max 20).",
7971
+ required: false
7972
+ },
7973
+ excludeCurrentSession: {
7974
+ type: "boolean",
7975
+ description: "If true, exclude the current session from results (avoid echoing what you just said). Default false.",
7976
+ required: false
7977
+ },
7978
+ currentSessionId: {
7979
+ type: "string",
7980
+ description: "Session ID to exclude when excludeCurrentSession=true. Usually the active session.",
7981
+ required: false
7982
+ },
7983
+ minScore: {
7984
+ type: "number",
7985
+ description: "Drop hits below this cosine score. Default 0.25. Raise to 0.35+ for stricter matches.",
7986
+ required: false
7987
+ }
7988
+ },
7989
+ dangerous: false
7990
+ },
7991
+ async execute(args) {
7992
+ const query = String(args["query"] ?? "").trim();
7993
+ if (!query) throw new ToolError("recall_memory", "query is required");
7994
+ const topK = Math.max(1, Math.min(20, Number(args["topK"] ?? 5)));
7995
+ const excludeCurrent = Boolean(args["excludeCurrentSession"]);
7996
+ const currentId = args["currentSessionId"] ? String(args["currentSessionId"]) : void 0;
7997
+ const minScore = args["minScore"] !== void 0 ? Number(args["minScore"]) : 0.25;
7998
+ const status = loadChatIndex();
7999
+ if (!status) {
8000
+ return "No chat memory index found. The index is built on REPL startup, or run `/memory rebuild` manually. If you have no past sessions yet, this is expected.";
8001
+ }
8002
+ const hits = await searchChatMemory(query, {
8003
+ topK,
8004
+ minScore,
8005
+ excludeSessionId: excludeCurrent ? currentId : void 0
8006
+ });
8007
+ if (hits.length === 0) {
8008
+ return `No memories matched "${query}" above score ${minScore}. Index has ${status.idx.chunks.length} chunks across ${Object.keys(status.idx.sessionMtimes).length} sessions. Consider lowering minScore to 0.15 or rephrasing the query.`;
8009
+ }
8010
+ const header = `Found ${hits.length} memory hit(s) for "${query}" (min-score ${minScore}):
8011
+ `;
8012
+ return header + "\n" + hits.map(formatHit).join("\n\n");
8013
+ }
8014
+ };
8015
+
7721
8016
  // src/core/token-estimator.ts
7722
8017
  var CJK_REGEX = /[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7FF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
7723
8018
  function estimateTokens(text) {
@@ -7777,6 +8072,7 @@ var ToolRegistry = class {
7777
8072
  this.register(getOutlineTool);
7778
8073
  this.register(findReferencesTool);
7779
8074
  this.register(searchCodeTool);
8075
+ this.register(recallMemoryTool);
7780
8076
  }
7781
8077
  register(tool) {
7782
8078
  this.tools.set(tool.definition.name, tool);
@@ -8944,9 +9240,9 @@ function getDevStatePath() {
8944
9240
  return join10(homedir4(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
8945
9241
  }
8946
9242
  function loadDevState() {
8947
- const path3 = getDevStatePath();
8948
- if (!existsSync17(path3)) return null;
8949
- const content = readFileSync11(path3, "utf-8").trim();
9243
+ const path4 = getDevStatePath();
9244
+ if (!existsSync17(path4)) return null;
9245
+ const content = readFileSync11(path4, "utf-8").trim();
8950
9246
  return content || null;
8951
9247
  }
8952
9248
 
@@ -11108,7 +11404,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
11108
11404
  case "test": {
11109
11405
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
11110
11406
  try {
11111
- const { executeTests } = await import("./run-tests-P7GIZ6UH.js");
11407
+ const { executeTests } = await import("./run-tests-TGGXTOFF.js");
11112
11408
  const argStr = args.join(" ").trim();
11113
11409
  let testArgs = {};
11114
11410
  if (argStr) {
@@ -385,7 +385,7 @@ ${content}`);
385
385
  }
386
386
  }
387
387
  async function runTaskMode(config, providers, configManager, topic) {
388
- const { TaskOrchestrator } = await import("./task-orchestrator-36SFPCP7.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-ODU45UQG.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {