jinzd-ai-cli 0.1.8 → 0.1.10

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 (2) hide show
  1. package/dist/index.js +161 -48
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -69,7 +69,13 @@ var ConfigSchema = z.object({
69
69
  // 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md 机制
70
70
  // 默认按顺序查找:AICLI.md → CLAUDE.md → .aicli/context.md
71
71
  // 设为 false 可禁用此功能
72
- contextFile: z.union([z.string(), z.literal(false)]).default("auto")
72
+ contextFile: z.union([z.string(), z.literal(false)]).default("auto"),
73
+ // 插件加载开关(安全控制)
74
+ // 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
75
+ // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
76
+ // 必须确认插件来源可信后,再设为 true 启用。
77
+ // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
78
+ allowPlugins: z.boolean().default(false)
73
79
  });
74
80
 
75
81
  // src/config/env-loader.ts
@@ -104,7 +110,7 @@ var EnvLoader = class {
104
110
  };
105
111
 
106
112
  // src/core/constants.ts
107
- var VERSION = "0.1.8";
113
+ var VERSION = "0.1.10";
108
114
  var CONFIG_DIR_NAME = ".aicli";
109
115
  var CONFIG_FILE_NAME = "config.json";
110
116
  var HISTORY_DIR_NAME = "history";
@@ -1333,7 +1339,7 @@ var SessionManager = class {
1333
1339
  // src/repl/repl.ts
1334
1340
  import * as readline from "readline";
1335
1341
  import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
1336
- import { join as join8, resolve as resolve3, extname as extname3 } from "path";
1342
+ import { join as join8, resolve as resolve4, extname as extname3 } from "path";
1337
1343
  import chalk7 from "chalk";
1338
1344
 
1339
1345
  // src/repl/renderer.ts
@@ -1508,8 +1514,8 @@ var Renderer = class {
1508
1514
  }
1509
1515
  process.stdout.write("\n\n");
1510
1516
  if (fileStream) {
1511
- await new Promise((resolve4, reject) => {
1512
- fileStream.end((err) => err ? reject(err) : resolve4());
1517
+ await new Promise((resolve5, reject) => {
1518
+ fileStream.end((err) => err ? reject(err) : resolve5());
1513
1519
  });
1514
1520
  const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
1515
1521
  process.stdout.write(chalk.green(` \u2705 \u5DF2\u4FDD\u5B58: ${options.saveToFile} (${kb} KB)
@@ -2134,7 +2140,7 @@ var IGNORE_ENTER_MS = 80;
2134
2140
  function selectFromList(prompt, items, initialIndex = 0) {
2135
2141
  if (items.length === 0) return Promise.resolve(null);
2136
2142
  const PAGE = 12;
2137
- return new Promise((resolve4) => {
2143
+ return new Promise((resolve5) => {
2138
2144
  let selected = Math.max(0, Math.min(initialIndex, items.length - 1));
2139
2145
  let windowStart = Math.max(0, selected - Math.floor(PAGE / 2));
2140
2146
  let lastRenderedLines = 0;
@@ -2209,7 +2215,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
2209
2215
  process.stdout.write(chalk3.dim(` \u2714 ${result}
2210
2216
  `));
2211
2217
  }
2212
- resolve4(result);
2218
+ resolve5(result);
2213
2219
  };
2214
2220
  const handleSequence = (seq) => {
2215
2221
  if (seq === "\x1B") {
@@ -2273,13 +2279,13 @@ function selectFromList(prompt, items, initialIndex = 0) {
2273
2279
  }
2274
2280
  };
2275
2281
  if (!process.stdin.isTTY) {
2276
- resolve4(items[0]?.value ?? null);
2282
+ resolve5(items[0]?.value ?? null);
2277
2283
  return;
2278
2284
  }
2279
2285
  try {
2280
2286
  process.stdin.setRawMode(true);
2281
2287
  } catch {
2282
- resolve4(items[0]?.value ?? null);
2288
+ resolve5(items[0]?.value ?? null);
2283
2289
  return;
2284
2290
  }
2285
2291
  savedDataListeners = process.stdin.rawListeners("data");
@@ -2332,7 +2338,8 @@ var bashTool = {
2332
2338
  },
2333
2339
  async execute(args) {
2334
2340
  const command = String(args["command"] ?? "");
2335
- const timeout = Number(args["timeout"] ?? 3e4);
2341
+ const MAX_TIMEOUT = 3e5;
2342
+ const timeout = Math.min(Math.max(Number(args["timeout"] ?? 3e4), 1e3), MAX_TIMEOUT);
2336
2343
  const cwdArg = args["cwd"] ? String(args["cwd"]) : void 0;
2337
2344
  if (!command.trim()) {
2338
2345
  throw new Error("command is required");
@@ -2398,15 +2405,16 @@ function fixWindowsDeleteCommand(command) {
2398
2405
  pathValue = pathMatch[2] ?? "";
2399
2406
  }
2400
2407
  if (!pathValue) return match;
2401
- return `cmd /c rmdir /s /q "${pathValue}"`;
2408
+ const safePath = pathValue.replace(/"/g, '\\"');
2409
+ return `cmd /c rmdir /s /q "${safePath}"`;
2402
2410
  }
2403
2411
  );
2404
2412
  }
2405
2413
  function updateCwdFromCommand(command, baseCwd) {
2406
- const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+([^\s;&|]+)/g)];
2414
+ const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+(['"]?)([^\s;&|'"]+)\1/g)];
2407
2415
  if (cdMatches.length === 0) return;
2408
2416
  const lastMatch = cdMatches[cdMatches.length - 1];
2409
- const target = lastMatch?.[1];
2417
+ const target = lastMatch?.[2];
2410
2418
  if (!target || target.startsWith("$") || target === "~") return;
2411
2419
  try {
2412
2420
  const newDir = resolve2(baseCwd, target);
@@ -2418,8 +2426,28 @@ function updateCwdFromCommand(command, baseCwd) {
2418
2426
  }
2419
2427
 
2420
2428
  // src/tools/builtin/read-file.ts
2421
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2422
- import { extname } from "path";
2429
+ import { readFileSync as readFileSync4, existsSync as existsSync5, statSync } from "fs";
2430
+ import { extname, resolve as resolve3, basename, sep } from "path";
2431
+ import { homedir as homedir2 } from "os";
2432
+ var MAX_FILE_BYTES = 10 * 1024 * 1024;
2433
+ function getSensitiveWarning(normalizedPath) {
2434
+ const home = homedir2();
2435
+ const p = normalizedPath.toLowerCase();
2436
+ const base = basename(normalizedPath).toLowerCase();
2437
+ if (normalizedPath.startsWith(home) && p.includes(".aicli") && base === "config.json") {
2438
+ return "[\u26A0 Security Warning: This file contains API keys. Be careful not to share this content.]\n\n";
2439
+ }
2440
+ if (base === ".env" || base.startsWith(".env.") || base.endsWith(".env")) {
2441
+ return "[\u26A0 Security Warning: .env files may contain secrets and credentials.]\n\n";
2442
+ }
2443
+ if (normalizedPath.includes(`${sep}.ssh${sep}`) && (base.startsWith("id_") || base === "identity")) {
2444
+ return "[\u26A0 Security Warning: This may be an SSH private key.]\n\n";
2445
+ }
2446
+ if (base === "credentials" && p.includes(".aws")) {
2447
+ return "[\u26A0 Security Warning: This file contains AWS credentials.]\n\n";
2448
+ }
2449
+ return "";
2450
+ }
2423
2451
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2424
2452
  ".pdf",
2425
2453
  ".doc",
@@ -2490,8 +2518,20 @@ var readFileTool = {
2490
2518
  const filePath = String(args["path"] ?? "");
2491
2519
  const encoding = args["encoding"] ?? "utf-8";
2492
2520
  if (!filePath) throw new Error("path is required");
2493
- if (!existsSync5(filePath)) throw new Error(`File not found: ${filePath}`);
2494
- const ext = extname(filePath).toLowerCase();
2521
+ const normalizedPath = resolve3(filePath);
2522
+ if (!existsSync5(normalizedPath)) throw new Error(`File not found: ${filePath}`);
2523
+ const { size } = statSync(normalizedPath);
2524
+ if (size > MAX_FILE_BYTES) {
2525
+ const mb = (size / 1024 / 1024).toFixed(1);
2526
+ return `[File too large: ${filePath} (${mb} MB)]
2527
+ \u8D85\u8FC7\u5355\u6B21\u8BFB\u53D6\u4E0A\u9650 ${MAX_FILE_BYTES / 1024 / 1024} MB\u3002
2528
+ \u8BF7\u4F7F\u7528 bash \u5DE5\u5177\u5206\u6BB5\u8BFB\u53D6\uFF0C\u4F8B\u5982\uFF1A
2529
+ head -n 100 "${normalizedPath}" # \u8BFB\u524D 100 \u884C
2530
+ tail -n 100 "${normalizedPath}" # \u8BFB\u672B 100 \u884C
2531
+ sed -n '200,300p' "${normalizedPath}" # \u8BFB 200-300 \u884C`;
2532
+ }
2533
+ const sensitiveWarning = getSensitiveWarning(normalizedPath);
2534
+ const ext = extname(normalizedPath).toLowerCase();
2495
2535
  if (BINARY_EXTENSIONS.has(ext)) {
2496
2536
  return `[Binary file: ${filePath}]
2497
2537
  \u6B64\u6587\u4EF6\u4E3A\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08${ext}\uFF09\uFF0C\u65E0\u6CD5\u4F5C\u4E3A\u6587\u672C\u8BFB\u53D6\u3002
@@ -2500,7 +2540,7 @@ var readFileTool = {
2500
2540
  2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pdftotext\u3001pandoc \u7B49\uFF09
2501
2541
  3. \u82E5\u6709\u5BF9\u5E94\u7684\u7EAF\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u90A3\u4E2A\u6587\u4EF6`;
2502
2542
  }
2503
- const buf = readFileSync4(filePath);
2543
+ const buf = readFileSync4(normalizedPath);
2504
2544
  if (encoding === "base64") {
2505
2545
  return `[File: ${filePath} | base64]
2506
2546
 
@@ -2513,7 +2553,7 @@ ${buf.toString("base64")}`;
2513
2553
  }
2514
2554
  const content = buf.toString(encoding);
2515
2555
  const lines = content.split("\n").length;
2516
- return `[File: ${filePath} | ${lines} lines]
2556
+ return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
2517
2557
 
2518
2558
  ${content}`;
2519
2559
  }
@@ -2698,7 +2738,7 @@ function truncatePreview(str, maxLen = 80) {
2698
2738
  }
2699
2739
 
2700
2740
  // src/tools/builtin/list-dir.ts
2701
- import { readdirSync as readdirSync2, statSync, existsSync as existsSync7 } from "fs";
2741
+ import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync7 } from "fs";
2702
2742
  import { join as join3 } from "path";
2703
2743
  var listDirTool = {
2704
2744
  definition: {
@@ -2754,7 +2794,7 @@ function listRecursive(basePath, indent, recursive, lines) {
2754
2794
  }
2755
2795
  } else {
2756
2796
  try {
2757
- const stat = statSync(join3(basePath, entry.name));
2797
+ const stat = statSync2(join3(basePath, entry.name));
2758
2798
  const size = formatSize(stat.size);
2759
2799
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
2760
2800
  } catch {
@@ -2770,7 +2810,7 @@ function formatSize(bytes) {
2770
2810
  }
2771
2811
 
2772
2812
  // src/tools/builtin/grep-files.ts
2773
- import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync8 } from "fs";
2813
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3, existsSync as existsSync8 } from "fs";
2774
2814
  import { join as join4, relative } from "path";
2775
2815
  var grepFilesTool = {
2776
2816
  definition: {
@@ -2831,7 +2871,7 @@ var grepFilesTool = {
2831
2871
  regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
2832
2872
  }
2833
2873
  const results = [];
2834
- const stat = statSync2(rootPath);
2874
+ const stat = statSync3(rootPath);
2835
2875
  if (stat.isFile()) {
2836
2876
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
2837
2877
  } else {
@@ -2940,8 +2980,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
2940
2980
  }
2941
2981
 
2942
2982
  // src/tools/builtin/glob-files.ts
2943
- import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync9 } from "fs";
2944
- import { join as join5, relative as relative2, basename } from "path";
2983
+ import { readdirSync as readdirSync4, statSync as statSync4, existsSync as existsSync9 } from "fs";
2984
+ import { join as join5, relative as relative2, basename as basename2 } from "path";
2945
2985
  var globFilesTool = {
2946
2986
  definition: {
2947
2987
  name: "glob_files",
@@ -3057,9 +3097,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3057
3097
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
3058
3098
  } else if (entry.isFile()) {
3059
3099
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
3060
- if (regex.test(relPath) || regex.test(basename(relPath))) {
3100
+ if (regex.test(relPath) || regex.test(basename2(relPath))) {
3061
3101
  try {
3062
- const stat = statSync3(fullPath);
3102
+ const stat = statSync4(fullPath);
3063
3103
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
3064
3104
  } catch {
3065
3105
  results.push({ relPath, absPath: fullPath, mtime: 0 });
@@ -3133,7 +3173,7 @@ var runInteractiveTool = {
3133
3173
  PYTHONDONTWRITEBYTECODE: "1"
3134
3174
  };
3135
3175
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
3136
- return new Promise((resolve4) => {
3176
+ return new Promise((resolve5) => {
3137
3177
  const child = spawn(executable, cmdArgs.map(String), {
3138
3178
  cwd: process.cwd(),
3139
3179
  env,
@@ -3163,22 +3203,22 @@ var runInteractiveTool = {
3163
3203
  setTimeout(writeNextLine, 400);
3164
3204
  const timer = setTimeout(() => {
3165
3205
  child.kill();
3166
- resolve4(`${prefixWarnings}[Timeout after ${timeout}ms]
3206
+ resolve5(`${prefixWarnings}[Timeout after ${timeout}ms]
3167
3207
  ${buildOutput(stdout, stderr)}`);
3168
3208
  }, timeout);
3169
3209
  child.on("close", (code) => {
3170
3210
  clearTimeout(timer);
3171
3211
  const output = buildOutput(stdout, stderr);
3172
3212
  if (code !== 0 && code !== null) {
3173
- resolve4(`${prefixWarnings}Exit code ${code}:
3213
+ resolve5(`${prefixWarnings}Exit code ${code}:
3174
3214
  ${output}`);
3175
3215
  } else {
3176
- resolve4(`${prefixWarnings}${output || "(no output)"}`);
3216
+ resolve5(`${prefixWarnings}${output || "(no output)"}`);
3177
3217
  }
3178
3218
  });
3179
3219
  child.on("error", (err) => {
3180
3220
  clearTimeout(timer);
3181
- resolve4(
3221
+ resolve5(
3182
3222
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
3183
3223
  Hint: On Windows, use the full path to the executable, e.g.:
3184
3224
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -3235,6 +3275,22 @@ function extractDescription(html) {
3235
3275
  return m ? m[1].trim() : "";
3236
3276
  }
3237
3277
  var MAX_OUTPUT = 16e3;
3278
+ function isPrivateHost(hostname) {
3279
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
3280
+ if (h === "localhost" || h === "0.0.0.0" || h === "::1") return true;
3281
+ if (h.startsWith("fe80:")) return true;
3282
+ const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
3283
+ if (m) {
3284
+ const [o1, o2] = [Number(m[1]), Number(m[2])];
3285
+ if (o1 === 127) return true;
3286
+ if (o1 === 10) return true;
3287
+ if (o1 === 172 && o2 >= 16 && o2 <= 31) return true;
3288
+ if (o1 === 192 && o2 === 168) return true;
3289
+ if (o1 === 169 && o2 === 254) return true;
3290
+ if (o1 === 0) return true;
3291
+ }
3292
+ return false;
3293
+ }
3238
3294
  var webFetchTool = {
3239
3295
  definition: {
3240
3296
  name: "web_fetch",
@@ -3258,6 +3314,15 @@ var webFetchTool = {
3258
3314
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
3259
3315
  throw new Error(`Invalid URL: "${url}". URL must start with http:// or https://`);
3260
3316
  }
3317
+ try {
3318
+ const parsedUrl = new URL(url);
3319
+ if (isPrivateHost(parsedUrl.hostname)) {
3320
+ throw new Error(`Blocked: "${url}" resolves to a private/internal address. web_fetch is restricted to public URLs.`);
3321
+ }
3322
+ } catch (e) {
3323
+ if (e.message.startsWith("Blocked:")) throw e;
3324
+ throw new Error(`Invalid URL: "${url}"`);
3325
+ }
3261
3326
  const controller = new AbortController();
3262
3327
  const timeoutId = setTimeout(() => controller.abort(), 2e4);
3263
3328
  let rawHtml;
@@ -3276,6 +3341,14 @@ var webFetchTool = {
3276
3341
  clearTimeout(timeoutId);
3277
3342
  finalUrl = resp.url;
3278
3343
  contentType = resp.headers.get("content-type") ?? "";
3344
+ try {
3345
+ const finalParsed = new URL(finalUrl);
3346
+ if (isPrivateHost(finalParsed.hostname)) {
3347
+ throw new Error(`Blocked: redirect landed on private address "${finalUrl}".`);
3348
+ }
3349
+ } catch (e) {
3350
+ if (e.message.startsWith("Blocked:")) throw e;
3351
+ }
3279
3352
  if (!resp.ok) {
3280
3353
  throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
3281
3354
  }
@@ -3450,10 +3523,10 @@ var streamToFileTool = {
3450
3523
  showProgress(false);
3451
3524
  }
3452
3525
  }
3453
- await new Promise((resolve4, reject) => {
3526
+ await new Promise((resolve5, reject) => {
3454
3527
  writeStream.end((err) => {
3455
3528
  if (err) reject(err);
3456
- else resolve4();
3529
+ else resolve5();
3457
3530
  });
3458
3531
  });
3459
3532
  process.stdout.write("\r\x1B[2K");
@@ -3505,10 +3578,17 @@ var ToolRegistry = class {
3505
3578
  }
3506
3579
  /**
3507
3580
  * Dynamically loads .js plugin files from pluginsDir.
3581
+ *
3582
+ * Security notes:
3583
+ * - Only loads when allowPlugins=true (must be explicitly enabled in config).
3584
+ * - Plugins run with FULL Node.js privileges in the main process.
3585
+ * - Prints a prominent warning listing every file before loading.
3586
+ * - Built-in tool names cannot be overridden by plugins.
3587
+ *
3508
3588
  * Creates the dir if missing. Skips invalid plugins with a warning.
3509
3589
  * Returns the number of successfully loaded plugins.
3510
3590
  */
3511
- async loadPlugins(pluginsDir) {
3591
+ async loadPlugins(pluginsDir, allowPlugins = false) {
3512
3592
  if (!existsSync10(pluginsDir)) {
3513
3593
  try {
3514
3594
  mkdirSync8(pluginsDir, { recursive: true });
@@ -3522,6 +3602,21 @@ var ToolRegistry = class {
3522
3602
  } catch {
3523
3603
  return 0;
3524
3604
  }
3605
+ if (files.length === 0) return 0;
3606
+ if (!allowPlugins) {
3607
+ process.stderr.write(
3608
+ `[plugins] Found ${files.length} plugin(s) in ${pluginsDir} but loading is disabled.
3609
+ To enable, set "allowPlugins": true in ~/.aicli/config.json (or via /config).
3610
+ \u26A0 Plugins run with full system privileges. Only enable for trusted sources.
3611
+ `
3612
+ );
3613
+ return 0;
3614
+ }
3615
+ process.stderr.write(
3616
+ `
3617
+ [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
3618
+ ` + files.map((f) => ` + ${join6(pluginsDir, f)}`).join("\n") + "\n\n"
3619
+ );
3525
3620
  let loaded = 0;
3526
3621
  for (const file of files) {
3527
3622
  try {
@@ -3541,6 +3636,8 @@ var ToolRegistry = class {
3541
3636
  this.register(tool);
3542
3637
  this.pluginToolNames.add(tool.definition.name);
3543
3638
  loaded++;
3639
+ process.stderr.write(`[plugins] Loaded: ${tool.definition.name} (${file})
3640
+ `);
3544
3641
  } catch (err) {
3545
3642
  process.stderr.write(`[plugins] Failed to load ${file}: ${err.message}
3546
3643
  `);
@@ -3558,10 +3655,13 @@ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
3558
3655
  function getDangerLevel(toolName, args) {
3559
3656
  if (toolName === "bash") {
3560
3657
  const cmd = String(args["command"] ?? "");
3561
- if (/\brm\s+(-\w*f\w*|-\w*r\w*f\w*)\b|\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
3562
- if (/\bRemove-Item\b|\bri\s+.*-Recurse\b|\brd\s+\/s\b|\brmdir\s+\/s\b/.test(cmd)) return "destructive";
3563
- if (/\bdel\s+\S/.test(cmd) && !/\bmkdir\b/.test(cmd)) return "destructive";
3658
+ if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
3659
+ if (/\brm\s+\S/.test(cmd)) return "destructive";
3660
+ if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
3661
+ if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
3662
+ if (/\bdel\s+\S/.test(cmd)) return "destructive";
3564
3663
  if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
3664
+ if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
3565
3665
  return "safe";
3566
3666
  }
3567
3667
  if (toolName === "write_file") return "write";
@@ -3787,7 +3887,18 @@ var ToolExecutor = class {
3787
3887
  };
3788
3888
  }
3789
3889
  const dangerLevel = getDangerLevel(call.name, call.arguments);
3790
- if (dangerLevel === "destructive") {
3890
+ if (dangerLevel === "write") {
3891
+ this.printToolCall(call);
3892
+ this.printDiffPreview(call);
3893
+ const confirmed = await this.confirm(call, dangerLevel);
3894
+ if (!confirmed) {
3895
+ return {
3896
+ callId: call.id,
3897
+ content: "User cancelled the operation.",
3898
+ isError: false
3899
+ };
3900
+ }
3901
+ } else if (dangerLevel === "destructive") {
3791
3902
  const confirmed = await this.confirm(call, dangerLevel);
3792
3903
  if (!confirmed) {
3793
3904
  return {
@@ -3796,9 +3907,10 @@ var ToolExecutor = class {
3796
3907
  isError: false
3797
3908
  };
3798
3909
  }
3910
+ this.printToolCall(call);
3911
+ } else {
3912
+ this.printToolCall(call);
3799
3913
  }
3800
- this.printToolCall(call);
3801
- this.printDiffPreview(call);
3802
3914
  try {
3803
3915
  const rawContent = await tool.execute(call.arguments);
3804
3916
  const content = truncateOutput(rawContent, call.name);
@@ -3951,14 +4063,14 @@ var ToolExecutor = class {
3951
4063
  rl.resume();
3952
4064
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
3953
4065
  this.confirming = true;
3954
- return new Promise((resolve4) => {
4066
+ return new Promise((resolve5) => {
3955
4067
  const cleanup = (answer) => {
3956
4068
  rl.removeListener("line", onLine);
3957
4069
  this.cancelConfirmFn = null;
3958
4070
  rl.pause();
3959
4071
  rlAny.output = savedOutput;
3960
4072
  this.confirming = false;
3961
- resolve4(answer === "y");
4073
+ resolve5(answer === "y");
3962
4074
  };
3963
4075
  const onLine = (line) => cleanup(line.trim().toLowerCase());
3964
4076
  this.cancelConfirmFn = () => {
@@ -4236,7 +4348,7 @@ function parseAtReferences(input2, cwd) {
4236
4348
  let match;
4237
4349
  while ((match = atPattern.exec(input2)) !== null) {
4238
4350
  const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
4239
- const absPath = resolve3(cwd, rawPath);
4351
+ const absPath = resolve4(cwd, rawPath);
4240
4352
  const ext = extname3(rawPath).toLowerCase();
4241
4353
  const mime = IMAGE_MIME[ext];
4242
4354
  if (!existsSync13(absPath)) {
@@ -4406,7 +4518,8 @@ ${this.activeSystemPrompt}`;
4406
4518
  ctx?.content ?? null,
4407
4519
  gitContextStr
4408
4520
  );
4409
- const pluginCount = await this.toolRegistry.loadPlugins(this.config.getPluginsDir());
4521
+ const allowPlugins = this.config.get("allowPlugins");
4522
+ const pluginCount = await this.toolRegistry.loadPlugins(this.config.getPluginsDir(), allowPlugins);
4410
4523
  const welcomeProvider = this.providers.get(this.currentProvider);
4411
4524
  const welcomeModelInfo = welcomeProvider?.info.models.find((m) => m.id === this.currentModel);
4412
4525
  this.renderer.printWelcome(this.currentProvider, this.currentModel, welcomeModelInfo?.contextWindow);
@@ -4434,7 +4547,7 @@ ${this.activeSystemPrompt}`;
4434
4547
  this.handleExit();
4435
4548
  });
4436
4549
  this.showPrompt();
4437
- await new Promise((resolve4) => {
4550
+ await new Promise((resolve5) => {
4438
4551
  let processing = false;
4439
4552
  this.rl.on("line", async (line) => {
4440
4553
  if (this.toolExecutor.confirming) return;
@@ -4469,12 +4582,12 @@ ${this.activeSystemPrompt}`;
4469
4582
  process.stdin.resume();
4470
4583
  this.showPrompt();
4471
4584
  } else {
4472
- resolve4();
4585
+ resolve5();
4473
4586
  }
4474
4587
  });
4475
4588
  this.rl.on("close", () => {
4476
4589
  if (!processing) {
4477
- resolve4();
4590
+ resolve5();
4478
4591
  }
4479
4592
  });
4480
4593
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",