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.
- package/dist/index.js +161 -48
- 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.
|
|
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
|
|
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((
|
|
1512
|
-
fileStream.end((err) => err ? reject(err) :
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
2282
|
+
resolve5(items[0]?.value ?? null);
|
|
2277
2283
|
return;
|
|
2278
2284
|
}
|
|
2279
2285
|
try {
|
|
2280
2286
|
process.stdin.setRawMode(true);
|
|
2281
2287
|
} catch {
|
|
2282
|
-
|
|
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
|
|
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
|
-
|
|
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?.[
|
|
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
|
-
|
|
2494
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
3100
|
+
if (regex.test(relPath) || regex.test(basename2(relPath))) {
|
|
3061
3101
|
try {
|
|
3062
|
-
const stat =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
3213
|
+
resolve5(`${prefixWarnings}Exit code ${code}:
|
|
3174
3214
|
${output}`);
|
|
3175
3215
|
} else {
|
|
3176
|
-
|
|
3216
|
+
resolve5(`${prefixWarnings}${output || "(no output)"}`);
|
|
3177
3217
|
}
|
|
3178
3218
|
});
|
|
3179
3219
|
child.on("error", (err) => {
|
|
3180
3220
|
clearTimeout(timer);
|
|
3181
|
-
|
|
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((
|
|
3526
|
+
await new Promise((resolve5, reject) => {
|
|
3454
3527
|
writeStream.end((err) => {
|
|
3455
3528
|
if (err) reject(err);
|
|
3456
|
-
else
|
|
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+(
|
|
3562
|
-
if (/\
|
|
3563
|
-
if (/\
|
|
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 === "
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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((
|
|
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
|
-
|
|
4585
|
+
resolve5();
|
|
4473
4586
|
}
|
|
4474
4587
|
});
|
|
4475
4588
|
this.rl.on("close", () => {
|
|
4476
4589
|
if (!processing) {
|
|
4477
|
-
|
|
4590
|
+
resolve5();
|
|
4478
4591
|
}
|
|
4479
4592
|
});
|
|
4480
4593
|
});
|