jinzd-ai-cli 0.4.87 → 0.4.88

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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConfigManager
4
- } from "./chunk-Y75YPB5F.js";
4
+ } from "./chunk-QT2KNL3V.js";
5
5
  import "./chunk-2ZD3YTVM.js";
6
- import "./chunk-BAOIXQHD.js";
6
+ import "./chunk-VGXNE37B.js";
7
7
 
8
8
  // src/cli/batch.ts
9
9
  import Anthropic from "@anthropic-ai/sdk";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  schemaToJsonSchema,
4
4
  truncateForPersist
5
- } from "./chunk-SP6RFAKW.js";
5
+ } from "./chunk-YDHIU24C.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-BAOIXQHD.js";
21
+ } from "./chunk-VGXNE37B.js";
22
22
 
23
23
  // src/providers/claude.ts
24
24
  import Anthropic from "@anthropic-ai/sdk";
@@ -2655,6 +2655,108 @@ var Session = class _Session {
2655
2655
  }
2656
2656
  };
2657
2657
 
2658
+ // src/security/redactor.ts
2659
+ var DEFAULT_PATTERNS = [
2660
+ // password: xxx / password = xxx / password="xxx"
2661
+ // Covers YAML / JSON / shell-ish / env-file forms.
2662
+ { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
2663
+ // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
2664
+ { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
2665
+ // JDBC/PG/MySQL/Mongo connection strings with inline credentials
2666
+ // postgresql://user:pass@host/db → redact pass
2667
+ { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
2668
+ // Anthropic API keys
2669
+ { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
2670
+ // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
2671
+ { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
2672
+ // GitHub personal access tokens
2673
+ { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
2674
+ { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
2675
+ { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
2676
+ // Slack tokens
2677
+ { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
2678
+ { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
2679
+ // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
2680
+ // we only catch the ID because secret key alone is indistinguishable from random base64.
2681
+ { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
2682
+ // Google API keys
2683
+ { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
2684
+ // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
2685
+ { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
2686
+ // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
2687
+ { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
2688
+ // Bearer <token> in Authorization headers
2689
+ { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
2690
+ // Private key PEM blocks — catch the header+footer together
2691
+ { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
2692
+ ];
2693
+ function render(placeholder, kind) {
2694
+ return placeholder.replace("{kind}", kind);
2695
+ }
2696
+ function redactString(input, options) {
2697
+ if (!options.enabled || !input) return { redacted: input, hits: [] };
2698
+ const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
2699
+ const patterns = [
2700
+ ...options.patterns ?? DEFAULT_PATTERNS,
2701
+ ...(options.customRegexes ?? []).flatMap((src, i) => {
2702
+ try {
2703
+ const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
2704
+ const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
2705
+ const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
2706
+ return [{ kind: `custom-${i}`, regex }];
2707
+ } catch {
2708
+ return [];
2709
+ }
2710
+ })
2711
+ ];
2712
+ let redacted = input;
2713
+ const hits = [];
2714
+ for (const { kind, regex } of patterns) {
2715
+ const rx = new RegExp(regex.source, regex.flags);
2716
+ redacted = redacted.replace(rx, (...args) => {
2717
+ const match = args[0];
2718
+ const probe = new RegExp(rx.source).exec(match);
2719
+ const captureCount = probe ? probe.length - 1 : 0;
2720
+ const g1 = captureCount >= 1 ? args[1] : void 0;
2721
+ const g2 = captureCount >= 2 ? args[2] : void 0;
2722
+ const offset = args[1 + captureCount];
2723
+ if (captureCount >= 2 && typeof g2 === "string") {
2724
+ hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
2725
+ return `${g1}${render(placeholder, kind)}`;
2726
+ }
2727
+ hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
2728
+ return render(placeholder, kind);
2729
+ });
2730
+ }
2731
+ return { redacted, hits };
2732
+ }
2733
+ function redactJson(value, options) {
2734
+ if (!options.enabled) return { value, hits: [] };
2735
+ const allHits = [];
2736
+ function walk(v) {
2737
+ if (typeof v === "string") {
2738
+ const r = redactString(v, options);
2739
+ allHits.push(...r.hits);
2740
+ return r.redacted;
2741
+ }
2742
+ if (Array.isArray(v)) return v.map(walk);
2743
+ if (v && typeof v === "object") {
2744
+ const out = {};
2745
+ for (const [k, vv] of Object.entries(v)) {
2746
+ out[k] = walk(vv);
2747
+ }
2748
+ return out;
2749
+ }
2750
+ return v;
2751
+ }
2752
+ const redacted = walk(value);
2753
+ return { value: redacted, hits: allHits };
2754
+ }
2755
+ function scanString(input, options) {
2756
+ const { hits } = redactString(input, { ...options, enabled: true });
2757
+ return hits;
2758
+ }
2759
+
2658
2760
  // src/session/session-manager.ts
2659
2761
  function safeDate(value) {
2660
2762
  const d = new Date(value);
@@ -2668,9 +2770,27 @@ function extractJsonField(header, field) {
2668
2770
  var SessionManager = class {
2669
2771
  _current = null;
2670
2772
  historyDir;
2773
+ config;
2774
+ /** Last save's redaction hit count — exposed for /security status reporting */
2775
+ lastRedactionHits = 0;
2671
2776
  constructor(config) {
2777
+ this.config = config;
2672
2778
  this.historyDir = config.getHistoryDir();
2673
2779
  }
2780
+ /**
2781
+ * Build redaction options from config. Returns `{ enabled: false }` when
2782
+ * `security.redactOnSave` is off or `security.mode` is 'off'.
2783
+ */
2784
+ redactOptionsForSave() {
2785
+ const security = this.config.get("security");
2786
+ if (!security || !security.redactOnSave || security.mode === "off") {
2787
+ return { enabled: false };
2788
+ }
2789
+ return {
2790
+ enabled: true,
2791
+ customRegexes: security.customPatterns ?? []
2792
+ };
2793
+ }
2674
2794
  get current() {
2675
2795
  return this._current;
2676
2796
  }
@@ -2696,8 +2816,12 @@ var SessionManager = class {
2696
2816
  if (!this._current) return;
2697
2817
  mkdirSync(this.historyDir, { recursive: true });
2698
2818
  const filePath = join(this.historyDir, `${this._current.id}.json`);
2819
+ const raw = this._current.toJSON();
2820
+ const opts = this.redactOptionsForSave();
2821
+ const { value: payload, hits } = redactJson(raw, opts);
2822
+ this.lastRedactionHits = hits.length;
2699
2823
  const tmpPath = filePath + ".tmp";
2700
- writeFileSync(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2824
+ writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
2701
2825
  renameSync(tmpPath, filePath);
2702
2826
  }
2703
2827
  loadSession(id) {
@@ -3975,6 +4099,8 @@ export {
3975
4099
  buildPhantomCorrectionMessage,
3976
4100
  ProviderRegistry,
3977
4101
  getContentText,
4102
+ DEFAULT_PATTERNS,
4103
+ scanString,
3978
4104
  SessionManager,
3979
4105
  getGitRoot,
3980
4106
  getGitContext,
@@ -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.87";
9
+ var VERSION = "0.4.88";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-BAOIXQHD.js";
11
+ } from "./chunk-VGXNE37B.js";
12
12
 
13
13
  // src/config/config-manager.ts
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -179,6 +179,24 @@ var ConfigSchema = z.object({
179
179
  // 必须确认插件来源可信后,再设为 true 启用。
180
180
  // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
181
181
  allowPlugins: z.boolean().default(false),
182
+ // 敏感信息脱敏(v0.4.88+,2026-04 凭据泄漏事件后引入)
183
+ // 会话保存到磁盘 / 发送给 Provider 前,自动将 API key、密码、PEM 私钥等
184
+ // 按正则替换为 [REDACTED:kind] 占位符。模式定义在 src/security/redactor.ts。
185
+ //
186
+ // redactOnSave 默认 true:保存到 ~/.aicli/history/*.json 时脱敏(推荐开启)
187
+ // redactOnSend 默认 false:发送到 Provider 时脱敏(激进,可能影响工具结果)
188
+ // mode:
189
+ // 'default' — 使用内置 13 条模式(推荐)
190
+ // 'strict' — 同 default,但阈值更低(更激进,可能误报)
191
+ // 'off' — 禁用所有模式(等同 redactOnSave=false)
192
+ // customPatterns — 用户补充正则字符串(支持 /pattern/flags 形式),
193
+ // 无效正则会静默跳过,不会中断保存。
194
+ security: z.object({
195
+ redactOnSave: z.boolean().default(true),
196
+ redactOnSend: z.boolean().default(false),
197
+ mode: z.enum(["default", "strict", "off"]).default("default"),
198
+ customPatterns: z.array(z.string()).default([])
199
+ }).default({}),
182
200
  // 智能模型路由(v0.4.68+)
183
201
  // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
184
202
  // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-BAOIXQHD.js";
4
+ } from "./chunk-VGXNE37B.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync } from "child_process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.87";
4
+ var VERSION = "0.4.88";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-6VRJGH25.js";
20
20
  import {
21
21
  runTestsTool
22
- } from "./chunk-TFLBQRQM.js";
22
+ } from "./chunk-V3NMERIB.js";
23
23
  import {
24
24
  CONFIG_DIR_NAME,
25
25
  DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
@@ -27,7 +27,7 @@ import {
27
27
  SUBAGENT_ALLOWED_TOOLS,
28
28
  SUBAGENT_DEFAULT_MAX_ROUNDS,
29
29
  SUBAGENT_MAX_ROUNDS_LIMIT
30
- } from "./chunk-BAOIXQHD.js";
30
+ } from "./chunk-VGXNE37B.js";
31
31
 
32
32
  // src/tools/types.ts
33
33
  function isFileWriteTool(name) {
@@ -36,7 +36,7 @@ import {
36
36
  VERSION,
37
37
  buildUserIdentityPrompt,
38
38
  runTestsTool
39
- } from "./chunk-AQX3GYRD.js";
39
+ } from "./chunk-P6EQZKKG.js";
40
40
  import {
41
41
  hasSemanticIndex,
42
42
  semanticSearch
@@ -223,6 +223,24 @@ var ConfigSchema = z.object({
223
223
  // 必须确认插件来源可信后,再设为 true 启用。
224
224
  // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
225
225
  allowPlugins: z.boolean().default(false),
226
+ // 敏感信息脱敏(v0.4.88+,2026-04 凭据泄漏事件后引入)
227
+ // 会话保存到磁盘 / 发送给 Provider 前,自动将 API key、密码、PEM 私钥等
228
+ // 按正则替换为 [REDACTED:kind] 占位符。模式定义在 src/security/redactor.ts。
229
+ //
230
+ // redactOnSave 默认 true:保存到 ~/.aicli/history/*.json 时脱敏(推荐开启)
231
+ // redactOnSend 默认 false:发送到 Provider 时脱敏(激进,可能影响工具结果)
232
+ // mode:
233
+ // 'default' — 使用内置 13 条模式(推荐)
234
+ // 'strict' — 同 default,但阈值更低(更激进,可能误报)
235
+ // 'off' — 禁用所有模式(等同 redactOnSave=false)
236
+ // customPatterns — 用户补充正则字符串(支持 /pattern/flags 形式),
237
+ // 无效正则会静默跳过,不会中断保存。
238
+ security: z.object({
239
+ redactOnSave: z.boolean().default(true),
240
+ redactOnSend: z.boolean().default(false),
241
+ mode: z.enum(["default", "strict", "off"]).default("default"),
242
+ customPatterns: z.array(z.string()).default([])
243
+ }).default({}),
226
244
  // 智能模型路由(v0.4.68+)
227
245
  // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
228
246
  // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
@@ -3036,6 +3054,104 @@ var Session = class _Session {
3036
3054
  }
3037
3055
  };
3038
3056
 
3057
+ // src/security/redactor.ts
3058
+ var DEFAULT_PATTERNS = [
3059
+ // password: xxx / password = xxx / password="xxx"
3060
+ // Covers YAML / JSON / shell-ish / env-file forms.
3061
+ { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
3062
+ // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
3063
+ { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
3064
+ // JDBC/PG/MySQL/Mongo connection strings with inline credentials
3065
+ // postgresql://user:pass@host/db → redact pass
3066
+ { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
3067
+ // Anthropic API keys
3068
+ { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
3069
+ // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
3070
+ { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
3071
+ // GitHub personal access tokens
3072
+ { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
3073
+ { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
3074
+ { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
3075
+ // Slack tokens
3076
+ { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
3077
+ { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
3078
+ // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
3079
+ // we only catch the ID because secret key alone is indistinguishable from random base64.
3080
+ { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
3081
+ // Google API keys
3082
+ { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
3083
+ // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
3084
+ { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
3085
+ // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
3086
+ { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
3087
+ // Bearer <token> in Authorization headers
3088
+ { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
3089
+ // Private key PEM blocks — catch the header+footer together
3090
+ { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
3091
+ ];
3092
+ function render(placeholder, kind) {
3093
+ return placeholder.replace("{kind}", kind);
3094
+ }
3095
+ function redactString(input, options) {
3096
+ if (!options.enabled || !input) return { redacted: input, hits: [] };
3097
+ const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
3098
+ const patterns = [
3099
+ ...options.patterns ?? DEFAULT_PATTERNS,
3100
+ ...(options.customRegexes ?? []).flatMap((src, i) => {
3101
+ try {
3102
+ const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
3103
+ const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
3104
+ const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
3105
+ return [{ kind: `custom-${i}`, regex }];
3106
+ } catch {
3107
+ return [];
3108
+ }
3109
+ })
3110
+ ];
3111
+ let redacted = input;
3112
+ const hits = [];
3113
+ for (const { kind, regex } of patterns) {
3114
+ const rx = new RegExp(regex.source, regex.flags);
3115
+ redacted = redacted.replace(rx, (...args) => {
3116
+ const match = args[0];
3117
+ const probe = new RegExp(rx.source).exec(match);
3118
+ const captureCount = probe ? probe.length - 1 : 0;
3119
+ const g1 = captureCount >= 1 ? args[1] : void 0;
3120
+ const g2 = captureCount >= 2 ? args[2] : void 0;
3121
+ const offset = args[1 + captureCount];
3122
+ if (captureCount >= 2 && typeof g2 === "string") {
3123
+ hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
3124
+ return `${g1}${render(placeholder, kind)}`;
3125
+ }
3126
+ hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
3127
+ return render(placeholder, kind);
3128
+ });
3129
+ }
3130
+ return { redacted, hits };
3131
+ }
3132
+ function redactJson(value, options) {
3133
+ if (!options.enabled) return { value, hits: [] };
3134
+ const allHits = [];
3135
+ function walk(v) {
3136
+ if (typeof v === "string") {
3137
+ const r = redactString(v, options);
3138
+ allHits.push(...r.hits);
3139
+ return r.redacted;
3140
+ }
3141
+ if (Array.isArray(v)) return v.map(walk);
3142
+ if (v && typeof v === "object") {
3143
+ const out = {};
3144
+ for (const [k, vv] of Object.entries(v)) {
3145
+ out[k] = walk(vv);
3146
+ }
3147
+ return out;
3148
+ }
3149
+ return v;
3150
+ }
3151
+ const redacted = walk(value);
3152
+ return { value: redacted, hits: allHits };
3153
+ }
3154
+
3039
3155
  // src/session/session-manager.ts
3040
3156
  function safeDate(value) {
3041
3157
  const d = new Date(value);
@@ -3049,9 +3165,27 @@ function extractJsonField(header, field) {
3049
3165
  var SessionManager = class {
3050
3166
  _current = null;
3051
3167
  historyDir;
3168
+ config;
3169
+ /** Last save's redaction hit count — exposed for /security status reporting */
3170
+ lastRedactionHits = 0;
3052
3171
  constructor(config) {
3172
+ this.config = config;
3053
3173
  this.historyDir = config.getHistoryDir();
3054
3174
  }
3175
+ /**
3176
+ * Build redaction options from config. Returns `{ enabled: false }` when
3177
+ * `security.redactOnSave` is off or `security.mode` is 'off'.
3178
+ */
3179
+ redactOptionsForSave() {
3180
+ const security = this.config.get("security");
3181
+ if (!security || !security.redactOnSave || security.mode === "off") {
3182
+ return { enabled: false };
3183
+ }
3184
+ return {
3185
+ enabled: true,
3186
+ customRegexes: security.customPatterns ?? []
3187
+ };
3188
+ }
3055
3189
  get current() {
3056
3190
  return this._current;
3057
3191
  }
@@ -3077,8 +3211,12 @@ var SessionManager = class {
3077
3211
  if (!this._current) return;
3078
3212
  mkdirSync2(this.historyDir, { recursive: true });
3079
3213
  const filePath = join2(this.historyDir, `${this._current.id}.json`);
3214
+ const raw = this._current.toJSON();
3215
+ const opts = this.redactOptionsForSave();
3216
+ const { value: payload, hits } = redactJson(raw, opts);
3217
+ this.lastRedactionHits = hits.length;
3080
3218
  const tmpPath = filePath + ".tmp";
3081
- writeFileSync2(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
3219
+ writeFileSync2(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
3082
3220
  renameSync(tmpPath, filePath);
3083
3221
  }
3084
3222
  loadSession(id) {
@@ -11108,7 +11246,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
11108
11246
  case "test": {
11109
11247
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
11110
11248
  try {
11111
- const { executeTests } = await import("./run-tests-P7GIZ6UH.js");
11249
+ const { executeTests } = await import("./run-tests-JVWIGY7P.js");
11112
11250
  const argStr = args.join(" ").trim();
11113
11251
  let testArgs = {};
11114
11252
  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-6MI6LD7T.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ DEFAULT_PATTERNS,
3
4
  HALLUCINATION_CORRECTION_MESSAGE,
4
5
  McpManager,
5
6
  ProviderRegistry,
@@ -28,12 +29,13 @@ import {
28
29
  persistToolRound,
29
30
  rebuildExtraMessages,
30
31
  saveDevState,
32
+ scanString,
31
33
  sessionHasMeaningfulContent,
32
34
  setupProxy
33
- } from "./chunk-3DGNN4RM.js";
35
+ } from "./chunk-L3MBIO36.js";
34
36
  import {
35
37
  ConfigManager
36
- } from "./chunk-Y75YPB5F.js";
38
+ } from "./chunk-QT2KNL3V.js";
37
39
  import {
38
40
  ToolExecutor,
39
41
  ToolRegistry,
@@ -49,7 +51,7 @@ import {
49
51
  spawnAgentContext,
50
52
  theme,
51
53
  undoStack
52
- } from "./chunk-SP6RFAKW.js";
54
+ } from "./chunk-YDHIU24C.js";
53
55
  import "./chunk-2ZD3YTVM.js";
54
56
  import {
55
57
  fileCheckpoints
@@ -58,7 +60,7 @@ import "./chunk-NHNWUBXB.js";
58
60
  import "./chunk-CQQQFNND.js";
59
61
  import "./chunk-6VRJGH25.js";
60
62
  import "./chunk-PFYAAX2S.js";
61
- import "./chunk-TFLBQRQM.js";
63
+ import "./chunk-V3NMERIB.js";
62
64
  import {
63
65
  AGENTIC_BEHAVIOR_GUIDELINE,
64
66
  AUTHOR,
@@ -80,7 +82,7 @@ import {
80
82
  SKILLS_DIR_NAME,
81
83
  VERSION,
82
84
  buildUserIdentityPrompt
83
- } from "./chunk-BAOIXQHD.js";
85
+ } from "./chunk-VGXNE37B.js";
84
86
 
85
87
  // src/index.ts
86
88
  import { program } from "commander";
@@ -980,6 +982,7 @@ function createDefaultCommands() {
980
982
  " /fork [checkpoint] - Fork session from checkpoint or current position",
981
983
  " /branch [list|new|switch|delete|rename|diff|cherry-pick] - Manage conversation branches (fork tree, cross-branch picks)",
982
984
  " /index [status|rebuild|clear|semantic-rebuild|semantic-clear] - Symbol + semantic index (find_symbol / search_code)",
985
+ " /security [status|scan|on|off] - Sensitive-data redaction status & session scan",
983
986
  " /yolo [on|off] - Toggle session auto-approve (skip confirmations)",
984
987
  " /exit - Exit"
985
988
  ] : [];
@@ -2592,7 +2595,7 @@ ${hint}` : "")
2592
2595
  usage: "/test [command|filter]",
2593
2596
  async execute(args, ctx) {
2594
2597
  try {
2595
- const { executeTests } = await import("./run-tests-7PTRRO4D.js");
2598
+ const { executeTests } = await import("./run-tests-FQHDUYOG.js");
2596
2599
  const argStr = args.join(" ").trim();
2597
2600
  let testArgs = {};
2598
2601
  if (argStr) {
@@ -3031,6 +3034,94 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
3031
3034
  }
3032
3035
  }
3033
3036
  },
3037
+ {
3038
+ name: "security",
3039
+ description: "Sensitive-data redaction \u2014 status, scan sessions, toggle on/off",
3040
+ usage: "/security [status|scan [--all]|on|off]",
3041
+ async execute(args, ctx) {
3042
+ const sub = (args[0] || "status").toLowerCase();
3043
+ const security = ctx.config.get("security") ?? {
3044
+ redactOnSave: true,
3045
+ redactOnSend: false,
3046
+ mode: "default",
3047
+ customPatterns: []
3048
+ };
3049
+ if (sub === "status") {
3050
+ console.log(theme.heading(" \u{1F6E1} Security / Redaction"));
3051
+ console.log(` redactOnSave : ${security.redactOnSave ? theme.success("on") : theme.warning("off")}`);
3052
+ console.log(` redactOnSend : ${security.redactOnSend ? theme.success("on") : theme.dim("off")}`);
3053
+ console.log(` mode : ${security.mode}`);
3054
+ console.log(` patterns : ${DEFAULT_PATTERNS.length} built-in + ${security.customPatterns?.length ?? 0} custom`);
3055
+ console.log(` last save : ${ctx.sessions.lastRedactionHits} hit(s) redacted`);
3056
+ console.log();
3057
+ console.log(theme.dim(" /security scan \u2014 scan current session for leaks"));
3058
+ console.log(theme.dim(" /security scan --all \u2014 scan all saved sessions"));
3059
+ console.log(theme.dim(" /security on|off \u2014 toggle redactOnSave"));
3060
+ console.log();
3061
+ return;
3062
+ }
3063
+ if (sub === "on" || sub === "off") {
3064
+ ctx.config.set("security", { ...security, redactOnSave: sub === "on" });
3065
+ console.log(theme.success(` \u2713 redactOnSave = ${sub}`));
3066
+ console.log();
3067
+ return;
3068
+ }
3069
+ if (sub === "scan") {
3070
+ const scanAll = args.includes("--all");
3071
+ const opts = { customRegexes: security.customPatterns ?? [] };
3072
+ let totalHits = 0;
3073
+ let filesWithHits = 0;
3074
+ if (scanAll) {
3075
+ const metas = ctx.sessions.listSessions();
3076
+ console.log(theme.info(` Scanning ${metas.length} session(s)\u2026`));
3077
+ const { readFileSync: readFileSync5 } = await import("fs");
3078
+ const { join: join6 } = await import("path");
3079
+ const historyDir = ctx.config.getHistoryDir();
3080
+ for (const m of metas) {
3081
+ try {
3082
+ const content = readFileSync5(join6(historyDir, `${m.id}.json`), "utf-8");
3083
+ const hits2 = scanString(content, opts);
3084
+ if (hits2.length) {
3085
+ filesWithHits++;
3086
+ totalHits += hits2.length;
3087
+ const kinds = [...new Set(hits2.map((h) => h.kind))].join(", ");
3088
+ console.log(` ${theme.warning("\u26A0")} ${m.id.slice(0, 8)}\u2026 ${theme.dim(m.title || "")} \u2014 ${hits2.length} hit(s) [${kinds}]`);
3089
+ }
3090
+ } catch {
3091
+ }
3092
+ }
3093
+ console.log();
3094
+ console.log(
3095
+ totalHits === 0 ? theme.success(` \u2713 No secrets found in ${metas.length} session(s).`) : theme.warning(` Found ${totalHits} hit(s) across ${filesWithHits} session(s). Re-save them with redactOnSave=on to sanitize.`)
3096
+ );
3097
+ console.log();
3098
+ return;
3099
+ }
3100
+ const current = ctx.sessions.current;
3101
+ if (!current) {
3102
+ console.log(theme.warning(" No active session to scan."));
3103
+ console.log();
3104
+ return;
3105
+ }
3106
+ const json = JSON.stringify(current.toJSON());
3107
+ const hits = scanString(json, opts);
3108
+ if (hits.length === 0) {
3109
+ console.log(theme.success(" \u2713 Current session: no secrets detected."));
3110
+ } else {
3111
+ const byKind = /* @__PURE__ */ new Map();
3112
+ for (const h of hits) byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
3113
+ console.log(theme.warning(` \u26A0 Current session: ${hits.length} hit(s)`));
3114
+ for (const [k, n] of byKind) console.log(` \u2022 ${k}: ${n}`);
3115
+ console.log(theme.dim(" (save the session to replace them with placeholders)"));
3116
+ }
3117
+ console.log();
3118
+ return;
3119
+ }
3120
+ console.log(theme.error(` Unknown subcommand: ${sub}`));
3121
+ console.log(theme.dim(" Usage: /security [status|scan [--all]|on|off]"));
3122
+ console.log();
3123
+ }
3124
+ },
3034
3125
  {
3035
3126
  name: "yolo",
3036
3127
  description: "Toggle session auto-approve (skip all confirmations)",
@@ -6485,7 +6576,7 @@ program.command("web").description("Start Web UI server with browser-based chat
6485
6576
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
6486
6577
  process.exit(1);
6487
6578
  }
6488
- const { startWebServer } = await import("./server-TRTN3RVO.js");
6579
+ const { startWebServer } = await import("./server-UWKRV5DK.js");
6489
6580
  await startWebServer({ port, host: options.host });
6490
6581
  });
6491
6582
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -6608,7 +6699,7 @@ program.command("sessions").description("List recent conversation sessions").act
6608
6699
  });
6609
6700
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
6610
6701
  try {
6611
- const batch = await import("./batch-FNHSLCMV.js");
6702
+ const batch = await import("./batch-7XCYSPJU.js");
6612
6703
  switch (action) {
6613
6704
  case "submit":
6614
6705
  if (!arg) {
@@ -6651,7 +6742,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
6651
6742
  }
6652
6743
  });
6653
6744
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
6654
- const { startMcpServer } = await import("./server-GJRBVTTQ.js");
6745
+ const { startMcpServer } = await import("./server-HTVVWKFN.js");
6655
6746
  await startMcpServer({
6656
6747
  allowDestructive: !!options.allowDestructive,
6657
6748
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -6778,7 +6869,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
6778
6869
  }),
6779
6870
  config.get("customProviders")
6780
6871
  );
6781
- const { startHub } = await import("./hub-HEC4GYKR.js");
6872
+ const { startHub } = await import("./hub-IR4INXSU.js");
6782
6873
  await startHub(
6783
6874
  {
6784
6875
  topic: topic ?? "",
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-TFLBQRQM.js";
6
- import "./chunk-BAOIXQHD.js";
5
+ } from "./chunk-V3NMERIB.js";
6
+ import "./chunk-VGXNE37B.js";
7
7
  export {
8
8
  executeTests,
9
9
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-AQX3GYRD.js";
4
+ } from "./chunk-P6EQZKKG.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -3,17 +3,17 @@ import {
3
3
  ToolRegistry,
4
4
  getDangerLevel,
5
5
  schemaToJsonSchema
6
- } from "./chunk-SP6RFAKW.js";
6
+ } from "./chunk-YDHIU24C.js";
7
7
  import "./chunk-2ZD3YTVM.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import "./chunk-NHNWUBXB.js";
10
10
  import "./chunk-CQQQFNND.js";
11
11
  import "./chunk-6VRJGH25.js";
12
12
  import "./chunk-PFYAAX2S.js";
13
- import "./chunk-TFLBQRQM.js";
13
+ import "./chunk-V3NMERIB.js";
14
14
  import {
15
15
  VERSION
16
- } from "./chunk-BAOIXQHD.js";
16
+ } from "./chunk-VGXNE37B.js";
17
17
 
18
18
  // src/mcp/server.ts
19
19
  import { createInterface } from "readline";
@@ -20,10 +20,10 @@ import {
20
20
  persistToolRound,
21
21
  rebuildExtraMessages,
22
22
  setupProxy
23
- } from "./chunk-3DGNN4RM.js";
23
+ } from "./chunk-L3MBIO36.js";
24
24
  import {
25
25
  ConfigManager
26
- } from "./chunk-Y75YPB5F.js";
26
+ } from "./chunk-QT2KNL3V.js";
27
27
  import {
28
28
  ToolExecutor,
29
29
  ToolRegistry,
@@ -41,14 +41,14 @@ import {
41
41
  spawnAgentContext,
42
42
  truncateOutput,
43
43
  undoStack
44
- } from "./chunk-SP6RFAKW.js";
44
+ } from "./chunk-YDHIU24C.js";
45
45
  import "./chunk-2ZD3YTVM.js";
46
46
  import "./chunk-4BKXL7SM.js";
47
47
  import "./chunk-NHNWUBXB.js";
48
48
  import "./chunk-CQQQFNND.js";
49
49
  import "./chunk-6VRJGH25.js";
50
50
  import "./chunk-PFYAAX2S.js";
51
- import "./chunk-TFLBQRQM.js";
51
+ import "./chunk-V3NMERIB.js";
52
52
  import {
53
53
  AGENTIC_BEHAVIOR_GUIDELINE,
54
54
  AUTHOR,
@@ -67,7 +67,7 @@ import {
67
67
  SKILLS_DIR_NAME,
68
68
  VERSION,
69
69
  buildUserIdentityPrompt
70
- } from "./chunk-BAOIXQHD.js";
70
+ } from "./chunk-VGXNE37B.js";
71
71
  import {
72
72
  AuthManager
73
73
  } from "./chunk-BYNY5JPB.js";
@@ -2229,7 +2229,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
2229
2229
  case "test": {
2230
2230
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
2231
2231
  try {
2232
- const { executeTests } = await import("./run-tests-7PTRRO4D.js");
2232
+ const { executeTests } = await import("./run-tests-FQHDUYOG.js");
2233
2233
  const argStr = args.join(" ").trim();
2234
2234
  let testArgs = {};
2235
2235
  if (argStr) {
@@ -4,17 +4,17 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-SP6RFAKW.js";
7
+ } from "./chunk-YDHIU24C.js";
8
8
  import "./chunk-2ZD3YTVM.js";
9
9
  import "./chunk-4BKXL7SM.js";
10
10
  import "./chunk-NHNWUBXB.js";
11
11
  import "./chunk-CQQQFNND.js";
12
12
  import "./chunk-6VRJGH25.js";
13
13
  import "./chunk-PFYAAX2S.js";
14
- import "./chunk-TFLBQRQM.js";
14
+ import "./chunk-V3NMERIB.js";
15
15
  import {
16
16
  SUBAGENT_ALLOWED_TOOLS
17
- } from "./chunk-BAOIXQHD.js";
17
+ } from "./chunk-VGXNE37B.js";
18
18
 
19
19
  // src/hub/task-orchestrator.ts
20
20
  import { createInterface } from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.87",
3
+ "version": "0.4.88",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",