jinzd-ai-cli 0.4.209 → 0.4.211
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/README.md +104 -6
- package/README.zh-CN.md +45 -5
- package/dist/{batch-45WLJUVP.js → batch-DE4RXKZD.js} +2 -2
- package/dist/{chunk-EN63JA7N.js → chunk-6NS6643Y.js} +1 -1
- package/dist/{chunk-D6M4E7SH.js → chunk-BE6ERF7M.js} +1 -1
- package/dist/{chunk-ATNYI6JL.js → chunk-C3OU2OPF.js} +374 -22
- package/dist/{chunk-UPMBIS4T.js → chunk-E5XCM4A6.js} +5 -3
- package/dist/{chunk-STIEOOOL.js → chunk-JBWA73GK.js} +1 -1
- package/dist/{chunk-DUZRQXIP.js → chunk-UOROWTGG.js} +45 -6
- package/dist/{chunk-JVLRMIHA.js → chunk-ZOPYREL5.js} +312 -38
- package/dist/{chunk-EYID2AIR.js → chunk-ZY2N2N6T.js} +5 -3
- package/dist/{ci-PUE3MQ7J.js → ci-XMUEX526.js} +2 -2
- package/dist/{constants-VTZLKDAG.js → constants-AWTIQIWG.js} +3 -1
- package/dist/{doctor-cli-IU6FSVLF.js → doctor-cli-SSI6ETFT.js} +4 -4
- package/dist/electron-server.js +1006 -471
- package/dist/{hub-4ETK4R2J.js → hub-INUJND2G.js} +1 -1
- package/dist/index.js +137 -367
- package/dist/{run-tests-NDS2IEMK.js → run-tests-3MHWUF43.js} +2 -2
- package/dist/{run-tests-UAS5NJBM.js → run-tests-PACN4UYX.js} +1 -1
- package/dist/{server-72SAPLPS.js → server-7USZJJAH.js} +4 -4
- package/dist/{server-4URLNC7P.js → server-BTSKOPQI.js} +139 -311
- package/dist/{task-orchestrator-6PZTB35T.js → task-orchestrator-OV4O25MX.js} +4 -4
- package/dist/{usage-ZNCKGF6U.js → usage-T2P6FTE7.js} +2 -2
- package/dist/web/client/app.js +1 -1
- package/package.json +1 -1
package/dist/electron-server.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CONFIG_DIR_NAME,
|
|
13
13
|
CONFIG_FILE_NAME,
|
|
14
14
|
CONTEXT_FILE_CANDIDATES,
|
|
15
|
+
CONTEXT_FILE_MAX_BYTES,
|
|
15
16
|
CUSTOM_COMMANDS_DIR_NAME,
|
|
16
17
|
DEFAULT_MAX_TOKENS,
|
|
17
18
|
DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
VERSION,
|
|
37
38
|
buildUserIdentityPrompt,
|
|
38
39
|
runTestsTool
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-ZY2N2N6T.js";
|
|
40
41
|
import {
|
|
41
42
|
hasSemanticIndex,
|
|
42
43
|
semanticSearch
|
|
@@ -59,8 +60,8 @@ import {
|
|
|
59
60
|
import express from "express";
|
|
60
61
|
import { createServer } from "http";
|
|
61
62
|
import { WebSocketServer } from "ws";
|
|
62
|
-
import { join as
|
|
63
|
-
import { existsSync as
|
|
63
|
+
import { join as join19, dirname as dirname6, resolve as resolve8, relative as relative4, sep as sep3 } from "path";
|
|
64
|
+
import { existsSync as existsSync25, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
|
|
64
65
|
import { networkInterfaces } from "os";
|
|
65
66
|
|
|
66
67
|
// src/config/config-manager.ts
|
|
@@ -172,10 +173,15 @@ var ConfigSchema = z.object({
|
|
|
172
173
|
systemPrompt: z.string().optional()
|
|
173
174
|
}).default({}),
|
|
174
175
|
// 项目上下文文件配置
|
|
175
|
-
// 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md 机制
|
|
176
|
-
// 默认按顺序查找:AICLI.md →
|
|
177
|
-
// 设为 false
|
|
176
|
+
// 启动时自动读取并注入 system prompt,类似 Claude Code 的 CLAUDE.md / Codex AGENTS.md 机制
|
|
177
|
+
// 默认按顺序查找:AICLI.override.md → AGENTS.override.md → AICLI.md → CLAUDE.md → AGENTS.md
|
|
178
|
+
// 设为 false 可禁用此功能;指定字符串时仅加载 cwd 下的该文件
|
|
178
179
|
contextFile: z.union([z.string(), z.literal(false)]).default("auto"),
|
|
180
|
+
context: z.object({
|
|
181
|
+
projectDocFallbackFilenames: z.array(z.string()).default([]),
|
|
182
|
+
projectDocMaxBytes: z.number().int().positive().default(32 * 1024),
|
|
183
|
+
showLoadedContextSources: z.boolean().default(false)
|
|
184
|
+
}).default({}),
|
|
179
185
|
// Google Custom Search API 的 Search Engine ID (cx 参数)
|
|
180
186
|
// API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
|
|
181
187
|
// CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
|
|
@@ -207,7 +213,41 @@ var ConfigSchema = z.object({
|
|
|
207
213
|
preToolExecution: z.string().optional(),
|
|
208
214
|
postToolExecution: z.string().optional()
|
|
209
215
|
}).optional(),
|
|
210
|
-
//
|
|
216
|
+
// 网络访问治理(v0.4.211+):默认关闭以保持兼容;开启后统一治理 web/search/MCP/shell 网络出口。
|
|
217
|
+
networkPolicy: z.object({
|
|
218
|
+
enabled: z.boolean().default(false),
|
|
219
|
+
defaultAction: z.enum(["allow", "confirm", "deny"]).default("confirm"),
|
|
220
|
+
allowDomains: z.array(z.string()).default([]),
|
|
221
|
+
denyDomains: z.array(z.string()).default([]),
|
|
222
|
+
allowPorts: z.array(z.number().int().min(1).max(65535)).default([]),
|
|
223
|
+
denyPorts: z.array(z.number().int().min(1).max(65535)).default([]),
|
|
224
|
+
allowPrivateNetwork: z.boolean().default(false),
|
|
225
|
+
tools: z.object({
|
|
226
|
+
web_fetch: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
227
|
+
web_search: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
228
|
+
google_search: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
229
|
+
shell: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
230
|
+
mcp: z.enum(["allow", "confirm", "deny"]).optional()
|
|
231
|
+
}).default({})
|
|
232
|
+
}).default({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
|
|
233
|
+
// 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
|
|
234
|
+
defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
|
|
235
|
+
allowedPermissionProfiles: z.array(z.string()).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
|
|
236
|
+
permissionProfiles: z.record(z.object({
|
|
237
|
+
extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
238
|
+
defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
|
|
239
|
+
workspaceRoots: z.array(z.string()).optional(),
|
|
240
|
+
allowTemp: z.boolean().optional(),
|
|
241
|
+
rules: z.array(z.object({
|
|
242
|
+
tool: z.string(),
|
|
243
|
+
action: z.enum(["auto-approve", "deny", "confirm"]),
|
|
244
|
+
when: z.object({
|
|
245
|
+
dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
|
|
246
|
+
pathPattern: z.string().optional()
|
|
247
|
+
}).optional()
|
|
248
|
+
})).optional()
|
|
249
|
+
})).default({}),
|
|
250
|
+
// 工具权限规则(按顺序匹配第一个生效;作为当前权限 Profile 内的细粒度规则)
|
|
211
251
|
permissionRules: z.array(z.object({
|
|
212
252
|
tool: z.string(),
|
|
213
253
|
action: z.enum(["auto-approve", "deny", "confirm"]),
|
|
@@ -216,7 +256,7 @@ var ConfigSchema = z.object({
|
|
|
216
256
|
pathPattern: z.string().optional()
|
|
217
257
|
}).optional()
|
|
218
258
|
})).default([]),
|
|
219
|
-
//
|
|
259
|
+
// 无规则匹配时的默认权限动作(legacy profile 兼容旧行为)
|
|
220
260
|
defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
|
|
221
261
|
// 自动上下文压缩开关
|
|
222
262
|
// 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
|
|
@@ -5727,6 +5767,7 @@ import { dirname as dirname3 } from "path";
|
|
|
5727
5767
|
// src/tools/executor.ts
|
|
5728
5768
|
import chalk3 from "chalk";
|
|
5729
5769
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
5770
|
+
import { tmpdir } from "os";
|
|
5730
5771
|
|
|
5731
5772
|
// src/core/readline-internal.ts
|
|
5732
5773
|
function rlInternal(rl) {
|
|
@@ -5961,6 +6002,7 @@ function runHook(template, vars) {
|
|
|
5961
6002
|
}
|
|
5962
6003
|
|
|
5963
6004
|
// src/tools/permissions.ts
|
|
6005
|
+
import { isAbsolute, resolve as resolve3 } from "path";
|
|
5964
6006
|
function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
|
|
5965
6007
|
for (const rule of rules) {
|
|
5966
6008
|
if (rule.tool !== "*" && rule.tool !== toolName) continue;
|
|
@@ -5981,6 +6023,225 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
|
|
|
5981
6023
|
}
|
|
5982
6024
|
return defaultAction;
|
|
5983
6025
|
}
|
|
6026
|
+
var BUILTIN_PROFILE_NAMES = /* @__PURE__ */ new Set(["legacy", "read-only", "workspace-write", "danger-full-access"]);
|
|
6027
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
6028
|
+
"read_file",
|
|
6029
|
+
"list_dir",
|
|
6030
|
+
"grep_files",
|
|
6031
|
+
"glob_files",
|
|
6032
|
+
"find_symbol",
|
|
6033
|
+
"get_outline",
|
|
6034
|
+
"find_references",
|
|
6035
|
+
"search_code",
|
|
6036
|
+
"git_status",
|
|
6037
|
+
"git_diff",
|
|
6038
|
+
"git_log",
|
|
6039
|
+
"task_list",
|
|
6040
|
+
"web_fetch",
|
|
6041
|
+
"web_search",
|
|
6042
|
+
"google_search",
|
|
6043
|
+
"ask_user",
|
|
6044
|
+
"write_todos",
|
|
6045
|
+
"recall_memory"
|
|
6046
|
+
]);
|
|
6047
|
+
function normalizePathForPermission(value) {
|
|
6048
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
6049
|
+
}
|
|
6050
|
+
function isInsidePath(root, target) {
|
|
6051
|
+
const absoluteTarget = isAbsolute(target) ? target : resolve3(root, target);
|
|
6052
|
+
const r = normalizePathForPermission(resolve3(root));
|
|
6053
|
+
const t = normalizePathForPermission(absoluteTarget);
|
|
6054
|
+
return t === r || t.startsWith(`${r}/`);
|
|
6055
|
+
}
|
|
6056
|
+
function extractPathArg(toolName, args) {
|
|
6057
|
+
const raw = args["path"] ?? args["filePath"] ?? args["notebook_path"];
|
|
6058
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
6059
|
+
if (toolName === "save_last_response") {
|
|
6060
|
+
const output = args["outputPath"] ?? args["file"] ?? args["path"];
|
|
6061
|
+
if (typeof output === "string" && output.trim()) return output.trim();
|
|
6062
|
+
}
|
|
6063
|
+
return null;
|
|
6064
|
+
}
|
|
6065
|
+
function isExplicitFileWriteTool(toolName) {
|
|
6066
|
+
return toolName === "write_file" || toolName === "edit_file" || toolName === "notebook_edit" || toolName === "save_last_response";
|
|
6067
|
+
}
|
|
6068
|
+
function resolveProfileName(ctx) {
|
|
6069
|
+
const requested = ctx?.profileName ?? "legacy";
|
|
6070
|
+
if (ctx?.allowedProfiles && ctx.allowedProfiles.length > 0 && !ctx.allowedProfiles.includes(requested)) {
|
|
6071
|
+
return "legacy";
|
|
6072
|
+
}
|
|
6073
|
+
if (BUILTIN_PROFILE_NAMES.has(requested)) return requested;
|
|
6074
|
+
const ext = ctx?.profile?.extends;
|
|
6075
|
+
return ext && BUILTIN_PROFILE_NAMES.has(ext) ? ext : "legacy";
|
|
6076
|
+
}
|
|
6077
|
+
function getProfileRules(ctx) {
|
|
6078
|
+
return ctx?.profile?.rules ?? [];
|
|
6079
|
+
}
|
|
6080
|
+
function getProfileDefaultAction(name, ctx) {
|
|
6081
|
+
if (ctx?.profile?.defaultAction) return ctx.profile.defaultAction;
|
|
6082
|
+
if (name === "danger-full-access") return "auto-approve";
|
|
6083
|
+
return "confirm";
|
|
6084
|
+
}
|
|
6085
|
+
function getWorkspaceRoots(ctx) {
|
|
6086
|
+
const roots = [ctx?.workspaceRoot, ...ctx?.profile?.workspaceRoots ?? []].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
6087
|
+
return roots;
|
|
6088
|
+
}
|
|
6089
|
+
function getAllowedWriteRoots(ctx) {
|
|
6090
|
+
const roots = [...getWorkspaceRoots(ctx)];
|
|
6091
|
+
const allowTemp = ctx?.profile?.allowTemp !== false;
|
|
6092
|
+
if (allowTemp) roots.push(...ctx?.tempDirs ?? []);
|
|
6093
|
+
return roots.filter((v) => v.trim().length > 0);
|
|
6094
|
+
}
|
|
6095
|
+
function profileHardDecision(profileName, toolName, args, dangerLevel, ctx) {
|
|
6096
|
+
if (profileName === "legacy") return null;
|
|
6097
|
+
if (profileName === "read-only") {
|
|
6098
|
+
const mcpReadOnly = toolName.startsWith("mcp__") && dangerLevel === "safe";
|
|
6099
|
+
if (dangerLevel !== "safe" || !READ_ONLY_TOOLS.has(toolName) && !mcpReadOnly) {
|
|
6100
|
+
return { action: "deny", reason: "blocked by read-only permission profile", profileName };
|
|
6101
|
+
}
|
|
6102
|
+
return null;
|
|
6103
|
+
}
|
|
6104
|
+
if (profileName === "workspace-write") {
|
|
6105
|
+
if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools always require confirmation", profileName };
|
|
6106
|
+
if (dangerLevel === "write" && isExplicitFileWriteTool(toolName)) {
|
|
6107
|
+
const target = extractPathArg(toolName, args);
|
|
6108
|
+
if (!target) return { action: "confirm", reason: "file write target is not explicit", profileName };
|
|
6109
|
+
const allowedRoots = getAllowedWriteRoots(ctx);
|
|
6110
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsidePath(root, target))) {
|
|
6111
|
+
return { action: "deny", reason: "file write target is outside workspace/temp roots", profileName };
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
return null;
|
|
6115
|
+
}
|
|
6116
|
+
if (profileName === "danger-full-access") {
|
|
6117
|
+
if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools still require confirmation", profileName };
|
|
6118
|
+
return null;
|
|
6119
|
+
}
|
|
6120
|
+
return null;
|
|
6121
|
+
}
|
|
6122
|
+
function checkPermissionWithProfile(toolName, args, dangerLevel, rules, defaultAction = "confirm", profileContext) {
|
|
6123
|
+
const profileName = resolveProfileName(profileContext);
|
|
6124
|
+
const hard = profileHardDecision(profileName, toolName, args, dangerLevel, profileContext);
|
|
6125
|
+
if (hard?.action === "deny") return hard;
|
|
6126
|
+
const combinedRules = [...getProfileRules(profileContext), ...rules];
|
|
6127
|
+
const profileDefault = profileName === "legacy" ? defaultAction : getProfileDefaultAction(profileName, profileContext);
|
|
6128
|
+
const action = checkPermission(toolName, args, dangerLevel, combinedRules, profileDefault);
|
|
6129
|
+
if (hard?.action === "confirm" && action === "auto-approve") return hard;
|
|
6130
|
+
return { action, profileName };
|
|
6131
|
+
}
|
|
6132
|
+
function formatPermissionProfileWarning(profileName) {
|
|
6133
|
+
if (profileName === "danger-full-access") {
|
|
6134
|
+
return "\u26A0 danger-full-access permission profile is active. Write tools may auto-run; destructive tools still require confirmation.";
|
|
6135
|
+
}
|
|
6136
|
+
if (profileName === "read-only") return "Read-only permission profile is active. Write and destructive tools are blocked.";
|
|
6137
|
+
if (profileName === "workspace-write") return "Workspace-write permission profile is active. Writes outside workspace/temp roots are blocked.";
|
|
6138
|
+
return null;
|
|
6139
|
+
}
|
|
6140
|
+
var NETWORK_TOOL_HOSTS = {
|
|
6141
|
+
web_search: ["cn.bing.com", "www.google.com"],
|
|
6142
|
+
google_search: ["www.googleapis.com"]
|
|
6143
|
+
};
|
|
6144
|
+
var NETWORK_TOOL_PORTS = {
|
|
6145
|
+
web_search: [443],
|
|
6146
|
+
google_search: [443]
|
|
6147
|
+
};
|
|
6148
|
+
function networkActionToPermission(action) {
|
|
6149
|
+
if (action === "allow") return null;
|
|
6150
|
+
return action;
|
|
6151
|
+
}
|
|
6152
|
+
function domainMatches(pattern, host) {
|
|
6153
|
+
const p = pattern.trim().toLowerCase().replace(/^\*\./, "");
|
|
6154
|
+
const h = host.trim().toLowerCase().replace(/:\d+$/, "");
|
|
6155
|
+
return h === p || h.endsWith(`.${p}`);
|
|
6156
|
+
}
|
|
6157
|
+
function isPrivateNetworkHost(host) {
|
|
6158
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
6159
|
+
if (h === "" || h === "localhost") return true;
|
|
6160
|
+
if (/^(127|10)\./.test(h)) return true;
|
|
6161
|
+
if (/^192\.168\./.test(h)) return true;
|
|
6162
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
|
|
6163
|
+
if (/^169\.254\./.test(h)) return true;
|
|
6164
|
+
if (h === "::1" || h === "::") return true;
|
|
6165
|
+
if (/^f[cd][0-9a-f]*:/i.test(h) || /^fe[89ab][0-9a-f]*:/i.test(h)) return true;
|
|
6166
|
+
return false;
|
|
6167
|
+
}
|
|
6168
|
+
function extractNetworkHosts(toolName, args) {
|
|
6169
|
+
if (toolName === "web_fetch") {
|
|
6170
|
+
const raw = String(args["url"] ?? "").trim();
|
|
6171
|
+
try {
|
|
6172
|
+
return raw ? [new URL(raw).hostname] : [];
|
|
6173
|
+
} catch {
|
|
6174
|
+
return [];
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
return NETWORK_TOOL_HOSTS[toolName] ?? [];
|
|
6178
|
+
}
|
|
6179
|
+
function extractNetworkPorts(toolName, args) {
|
|
6180
|
+
if (toolName === "web_fetch") {
|
|
6181
|
+
const raw = String(args["url"] ?? "").trim();
|
|
6182
|
+
try {
|
|
6183
|
+
if (!raw) return [];
|
|
6184
|
+
const url = new URL(raw);
|
|
6185
|
+
if (url.port) return [Number(url.port)];
|
|
6186
|
+
return [url.protocol === "http:" ? 80 : 443];
|
|
6187
|
+
} catch {
|
|
6188
|
+
return [];
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
return NETWORK_TOOL_PORTS[toolName] ?? [];
|
|
6192
|
+
}
|
|
6193
|
+
function isShellNetworkCommand(command) {
|
|
6194
|
+
return /\b(curl|wget|aria2c|Invoke-WebRequest|Invoke-RestMethod|iwr|irm|ssh|scp|sftp|ftp|telnet|nc|ncat|netcat)\b/i.test(command) || /\b(git\s+(clone|fetch|pull|push)|npm\s+(install|publish|view)|pnpm\s+(add|install)|yarn\s+(add|install)|pip\s+install|python\s+-m\s+pip\s+install)\b/i.test(command);
|
|
6195
|
+
}
|
|
6196
|
+
function networkToolAction(toolName, policy) {
|
|
6197
|
+
if (toolName === "web_fetch") return policy.tools?.web_fetch ?? policy.defaultAction ?? "confirm";
|
|
6198
|
+
if (toolName === "web_search") return policy.tools?.web_search ?? policy.defaultAction ?? "confirm";
|
|
6199
|
+
if (toolName === "google_search") return policy.tools?.google_search ?? policy.defaultAction ?? "confirm";
|
|
6200
|
+
if (toolName.startsWith("mcp__")) return policy.tools?.mcp ?? policy.defaultAction ?? "confirm";
|
|
6201
|
+
return null;
|
|
6202
|
+
}
|
|
6203
|
+
function checkNetworkPolicy(toolName, args, policy) {
|
|
6204
|
+
if (!policy?.enabled) return null;
|
|
6205
|
+
let action = networkToolAction(toolName, policy);
|
|
6206
|
+
let reason = "network access requires confirmation";
|
|
6207
|
+
if (toolName === "bash") {
|
|
6208
|
+
const command = String(args["command"] ?? "");
|
|
6209
|
+
if (!isShellNetworkCommand(command)) return null;
|
|
6210
|
+
action = policy.tools?.shell ?? policy.defaultAction ?? "confirm";
|
|
6211
|
+
reason = "shell command may access the network";
|
|
6212
|
+
}
|
|
6213
|
+
if (!action) return null;
|
|
6214
|
+
const hosts = extractNetworkHosts(toolName, args);
|
|
6215
|
+
for (const host of hosts) {
|
|
6216
|
+
if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) {
|
|
6217
|
+
return { action: "deny", reason: `network host "${host}" is denied by networkPolicy`, profileName: "networkPolicy" };
|
|
6218
|
+
}
|
|
6219
|
+
if (!policy.allowPrivateNetwork && isPrivateNetworkHost(host)) {
|
|
6220
|
+
return { action: "deny", reason: `network host "${host}" is private/internal`, profileName: "networkPolicy" };
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
if (hosts.length > 0 && policy.allowDomains && policy.allowDomains.length > 0) {
|
|
6224
|
+
const allAllowed = hosts.every((host) => policy.allowDomains.some((pattern) => domainMatches(pattern, host)));
|
|
6225
|
+
if (!allAllowed) {
|
|
6226
|
+
return { action: "deny", reason: `network host is not in networkPolicy.allowDomains`, profileName: "networkPolicy" };
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
const ports = extractNetworkPorts(toolName, args);
|
|
6230
|
+
for (const port of ports) {
|
|
6231
|
+
if (policy.denyPorts?.includes(port)) {
|
|
6232
|
+
return { action: "deny", reason: `network port ${port} is denied by networkPolicy`, profileName: "networkPolicy" };
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
if (ports.length > 0 && policy.allowPorts && policy.allowPorts.length > 0) {
|
|
6236
|
+
const allPortsAllowed = ports.every((port) => policy.allowPorts.includes(port));
|
|
6237
|
+
if (!allPortsAllowed) {
|
|
6238
|
+
return { action: "deny", reason: `network port is not in networkPolicy.allowPorts`, profileName: "networkPolicy" };
|
|
6239
|
+
}
|
|
6240
|
+
}
|
|
6241
|
+
const permissionAction = networkActionToPermission(action);
|
|
6242
|
+
if (!permissionAction) return null;
|
|
6243
|
+
return { action: permissionAction, reason, profileName: "networkPolicy" };
|
|
6244
|
+
}
|
|
5984
6245
|
|
|
5985
6246
|
// src/tools/truncate.ts
|
|
5986
6247
|
var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
|
|
@@ -6283,11 +6544,21 @@ var ToolExecutor = class {
|
|
|
6283
6544
|
/** 权限规则(可选) */
|
|
6284
6545
|
permissionRules = [];
|
|
6285
6546
|
defaultPermission = "confirm";
|
|
6547
|
+
permissionProfileName = "legacy";
|
|
6548
|
+
permissionProfile;
|
|
6549
|
+
allowedPermissionProfiles = [];
|
|
6550
|
+
workspaceRoot = process.cwd();
|
|
6551
|
+
networkPolicy;
|
|
6286
6552
|
/** 注入 hooks 和 permission rules 配置 */
|
|
6287
6553
|
setConfig(opts) {
|
|
6288
6554
|
this.hookConfig = opts.hookConfig;
|
|
6289
6555
|
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
6290
6556
|
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
6557
|
+
if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
|
|
6558
|
+
if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
|
|
6559
|
+
if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
|
|
6560
|
+
if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
|
|
6561
|
+
if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
|
|
6291
6562
|
}
|
|
6292
6563
|
async execute(call) {
|
|
6293
6564
|
return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
|
|
@@ -6302,31 +6573,69 @@ var ToolExecutor = class {
|
|
|
6302
6573
|
};
|
|
6303
6574
|
}
|
|
6304
6575
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
6576
|
+
let toolCallAlreadyPrinted = false;
|
|
6305
6577
|
runHook(this.hookConfig?.preToolExecution, {
|
|
6306
6578
|
tool: call.name,
|
|
6307
6579
|
dangerLevel,
|
|
6308
6580
|
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
6309
6581
|
});
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6582
|
+
const permission = checkPermissionWithProfile(
|
|
6583
|
+
call.name,
|
|
6584
|
+
call.arguments,
|
|
6585
|
+
dangerLevel,
|
|
6586
|
+
this.permissionRules,
|
|
6587
|
+
this.defaultPermission,
|
|
6588
|
+
{
|
|
6589
|
+
profileName: this.permissionProfileName,
|
|
6590
|
+
profile: this.permissionProfile,
|
|
6591
|
+
allowedProfiles: this.allowedPermissionProfiles,
|
|
6592
|
+
workspaceRoot: this.workspaceRoot,
|
|
6593
|
+
tempDirs: [tmpdir()]
|
|
6314
6594
|
}
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
}
|
|
6595
|
+
);
|
|
6596
|
+
const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
|
|
6597
|
+
if (networkPermission?.action === "deny") {
|
|
6598
|
+
const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
|
|
6599
|
+
return {
|
|
6600
|
+
callId: call.id,
|
|
6601
|
+
content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
|
|
6602
|
+
isError: true
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6605
|
+
if (permission.action === "deny") {
|
|
6606
|
+
const reason = permission.reason ? ` (${permission.reason})` : "";
|
|
6607
|
+
return {
|
|
6608
|
+
callId: call.id,
|
|
6609
|
+
content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
|
|
6610
|
+
isError: true
|
|
6611
|
+
};
|
|
6612
|
+
}
|
|
6613
|
+
if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
|
|
6614
|
+
this.printToolCall(call);
|
|
6615
|
+
try {
|
|
6616
|
+
const rawContent = await runTool(tool, call.arguments, call.name);
|
|
6617
|
+
const content = truncateOutput(rawContent, call.name);
|
|
6618
|
+
const wasTruncated = content !== rawContent;
|
|
6619
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
6620
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
6621
|
+
return { callId: call.id, content, isError: false };
|
|
6622
|
+
} catch (err) {
|
|
6623
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6624
|
+
this.printToolResult(call.name, message, true, false);
|
|
6625
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
6626
|
+
return { callId: call.id, content: message, isError: true };
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
|
|
6630
|
+
this.printToolCall(call);
|
|
6631
|
+
toolCallAlreadyPrinted = true;
|
|
6632
|
+
const confirmed = await this.confirm(call, "write");
|
|
6633
|
+
if (!confirmed) {
|
|
6634
|
+
return {
|
|
6635
|
+
callId: call.id,
|
|
6636
|
+
content: `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`,
|
|
6637
|
+
isError: true
|
|
6638
|
+
};
|
|
6330
6639
|
}
|
|
6331
6640
|
}
|
|
6332
6641
|
if (this.sessionAutoApprove && dangerLevel === "write") {
|
|
@@ -6416,6 +6725,9 @@ var ToolExecutor = class {
|
|
|
6416
6725
|
* 然后让用户 approve all / reject all / 选择性 approve。
|
|
6417
6726
|
*/
|
|
6418
6727
|
async executeBatchFileWrites(calls) {
|
|
6728
|
+
if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
|
|
6729
|
+
return Promise.all(calls.map((call) => this.execute(call)));
|
|
6730
|
+
}
|
|
6419
6731
|
console.log();
|
|
6420
6732
|
console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
|
|
6421
6733
|
console.log(theme.dim("\u2500".repeat(50)));
|
|
@@ -6489,7 +6801,7 @@ var ToolExecutor = class {
|
|
|
6489
6801
|
rl.resume();
|
|
6490
6802
|
process.stdout.write(prompt);
|
|
6491
6803
|
this.confirming = true;
|
|
6492
|
-
return new Promise((
|
|
6804
|
+
return new Promise((resolve9) => {
|
|
6493
6805
|
let completed = false;
|
|
6494
6806
|
const cleanup = (result) => {
|
|
6495
6807
|
if (completed) return;
|
|
@@ -6499,7 +6811,7 @@ var ToolExecutor = class {
|
|
|
6499
6811
|
rl.pause();
|
|
6500
6812
|
rlAny.output = savedOutput;
|
|
6501
6813
|
this.confirming = false;
|
|
6502
|
-
|
|
6814
|
+
resolve9(result);
|
|
6503
6815
|
};
|
|
6504
6816
|
const onLine = (line) => {
|
|
6505
6817
|
const trimmed = line.trim();
|
|
@@ -6673,7 +6985,7 @@ var ToolExecutor = class {
|
|
|
6673
6985
|
rl.resume();
|
|
6674
6986
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
6675
6987
|
this.confirming = true;
|
|
6676
|
-
return new Promise((
|
|
6988
|
+
return new Promise((resolve9) => {
|
|
6677
6989
|
let completed = false;
|
|
6678
6990
|
const cleanup = (answer) => {
|
|
6679
6991
|
if (completed) return;
|
|
@@ -6683,7 +6995,7 @@ var ToolExecutor = class {
|
|
|
6683
6995
|
rl.pause();
|
|
6684
6996
|
rlAny.output = savedOutput;
|
|
6685
6997
|
this.confirming = false;
|
|
6686
|
-
|
|
6998
|
+
resolve9(answer === "y");
|
|
6687
6999
|
};
|
|
6688
7000
|
const onLine = (line) => {
|
|
6689
7001
|
const trimmed = line.trim();
|
|
@@ -6711,11 +7023,11 @@ var ToolExecutor = class {
|
|
|
6711
7023
|
};
|
|
6712
7024
|
|
|
6713
7025
|
// src/tools/sensitive-paths.ts
|
|
6714
|
-
import { resolve as
|
|
7026
|
+
import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
|
|
6715
7027
|
import { homedir as homedir4 } from "os";
|
|
6716
7028
|
var home = homedir4();
|
|
6717
7029
|
function norm(p) {
|
|
6718
|
-
const abs =
|
|
7030
|
+
const abs = resolve4(p);
|
|
6719
7031
|
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
6720
7032
|
}
|
|
6721
7033
|
function homeRel(p) {
|
|
@@ -7666,7 +7978,7 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
|
|
|
7666
7978
|
walk(rootPath);
|
|
7667
7979
|
return paths;
|
|
7668
7980
|
}
|
|
7669
|
-
async function searchInFileAsync(fullPath,
|
|
7981
|
+
async function searchInFileAsync(fullPath, displayPath2, regex, contextLines, maxResults) {
|
|
7670
7982
|
let content;
|
|
7671
7983
|
try {
|
|
7672
7984
|
content = await readFile(fullPath, "utf-8");
|
|
@@ -7680,7 +7992,7 @@ async function searchInFileAsync(fullPath, displayPath, regex, contextLines, max
|
|
|
7680
7992
|
regex.lastIndex = 0;
|
|
7681
7993
|
if (regex.test(lines[i])) {
|
|
7682
7994
|
const match = {
|
|
7683
|
-
file:
|
|
7995
|
+
file: displayPath2.replace(/\\/g, "/"),
|
|
7684
7996
|
lineNumber: i + 1,
|
|
7685
7997
|
lineText: lines[i]
|
|
7686
7998
|
};
|
|
@@ -7699,7 +8011,7 @@ async function searchInFileAsync(fullPath, displayPath, regex, contextLines, max
|
|
|
7699
8011
|
}
|
|
7700
8012
|
return results;
|
|
7701
8013
|
}
|
|
7702
|
-
function searchInFile(fullPath,
|
|
8014
|
+
function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, results) {
|
|
7703
8015
|
try {
|
|
7704
8016
|
const stat = statSync4(fullPath);
|
|
7705
8017
|
if (stat.size > 1e6) return;
|
|
@@ -7719,7 +8031,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
7719
8031
|
regex.lastIndex = 0;
|
|
7720
8032
|
if (regex.test(lines[i])) {
|
|
7721
8033
|
const result = {
|
|
7722
|
-
file:
|
|
8034
|
+
file: displayPath2,
|
|
7723
8035
|
lineNumber: i + 1,
|
|
7724
8036
|
lineText: lines[i].trimEnd()
|
|
7725
8037
|
};
|
|
@@ -7932,7 +8244,7 @@ var runInteractiveTool = {
|
|
|
7932
8244
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
7933
8245
|
};
|
|
7934
8246
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
7935
|
-
return new Promise((
|
|
8247
|
+
return new Promise((resolve9) => {
|
|
7936
8248
|
const child = spawn2(executable, cmdArgs.map(String), {
|
|
7937
8249
|
cwd: process.cwd(),
|
|
7938
8250
|
env,
|
|
@@ -7965,22 +8277,22 @@ var runInteractiveTool = {
|
|
|
7965
8277
|
setTimeout(writeNextLine, 400);
|
|
7966
8278
|
const timer = setTimeout(() => {
|
|
7967
8279
|
child.kill();
|
|
7968
|
-
|
|
8280
|
+
resolve9(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
7969
8281
|
${buildOutput(stdout, stderr)}`);
|
|
7970
8282
|
}, timeout);
|
|
7971
8283
|
child.on("close", (code) => {
|
|
7972
8284
|
clearTimeout(timer);
|
|
7973
8285
|
const output = buildOutput(stdout, stderr);
|
|
7974
8286
|
if (code !== 0 && code !== null) {
|
|
7975
|
-
|
|
8287
|
+
resolve9(`${prefixWarnings}Exit code ${code}:
|
|
7976
8288
|
${output}`);
|
|
7977
8289
|
} else {
|
|
7978
|
-
|
|
8290
|
+
resolve9(`${prefixWarnings}${output || "(no output)"}`);
|
|
7979
8291
|
}
|
|
7980
8292
|
});
|
|
7981
8293
|
child.on("error", (err) => {
|
|
7982
8294
|
clearTimeout(timer);
|
|
7983
|
-
|
|
8295
|
+
resolve9(
|
|
7984
8296
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
7985
8297
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
7986
8298
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -8732,7 +9044,7 @@ function promptUser(rl, question) {
|
|
|
8732
9044
|
console.log();
|
|
8733
9045
|
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
8734
9046
|
process.stdout.write(chalk4.cyan("> "));
|
|
8735
|
-
return new Promise((
|
|
9047
|
+
return new Promise((resolve9) => {
|
|
8736
9048
|
let completed = false;
|
|
8737
9049
|
const cleanup = (answer) => {
|
|
8738
9050
|
if (completed) return;
|
|
@@ -8742,7 +9054,7 @@ function promptUser(rl, question) {
|
|
|
8742
9054
|
rl.pause();
|
|
8743
9055
|
rlAny.output = savedOutput;
|
|
8744
9056
|
askUserContext.prompting = false;
|
|
8745
|
-
|
|
9057
|
+
resolve9(answer);
|
|
8746
9058
|
};
|
|
8747
9059
|
const onLine = (line) => {
|
|
8748
9060
|
cleanup(line);
|
|
@@ -9753,7 +10065,7 @@ ${commitOutput.trim()}`;
|
|
|
9753
10065
|
// src/tools/builtin/notebook-edit.ts
|
|
9754
10066
|
import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
|
|
9755
10067
|
import { writeFile } from "fs/promises";
|
|
9756
|
-
import { resolve as
|
|
10068
|
+
import { resolve as resolve5, extname as extname2 } from "path";
|
|
9757
10069
|
var notebookEditTool = {
|
|
9758
10070
|
definition: {
|
|
9759
10071
|
name: "notebook_edit",
|
|
@@ -9802,7 +10114,7 @@ var notebookEditTool = {
|
|
|
9802
10114
|
if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
|
|
9803
10115
|
throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
|
|
9804
10116
|
}
|
|
9805
|
-
const absPath =
|
|
10117
|
+
const absPath = resolve5(filePath);
|
|
9806
10118
|
if (extname2(absPath).toLowerCase() !== ".ipynb") {
|
|
9807
10119
|
throw new ToolError("notebook_edit", "path must point to a .ipynb file");
|
|
9808
10120
|
}
|
|
@@ -10581,7 +10893,7 @@ var McpClient = class {
|
|
|
10581
10893
|
// 内部方法:JSON-RPC 通信
|
|
10582
10894
|
// ══════════════════════════════════════════════════════════════════
|
|
10583
10895
|
sendRequest(method, params) {
|
|
10584
|
-
return new Promise((
|
|
10896
|
+
return new Promise((resolve9, reject) => {
|
|
10585
10897
|
if (!this.process?.stdin?.writable) {
|
|
10586
10898
|
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
10587
10899
|
}
|
|
@@ -10605,7 +10917,7 @@ var McpClient = class {
|
|
|
10605
10917
|
this.pendingRequests.set(id, {
|
|
10606
10918
|
resolve: (result) => {
|
|
10607
10919
|
cleanup();
|
|
10608
|
-
|
|
10920
|
+
resolve9(result);
|
|
10609
10921
|
},
|
|
10610
10922
|
reject: (error) => {
|
|
10611
10923
|
cleanup();
|
|
@@ -10682,13 +10994,13 @@ var McpClient = class {
|
|
|
10682
10994
|
}
|
|
10683
10995
|
/** Promise 超时包装 */
|
|
10684
10996
|
withTimeout(promise, ms, label) {
|
|
10685
|
-
return new Promise((
|
|
10997
|
+
return new Promise((resolve9, reject) => {
|
|
10686
10998
|
const timer = setTimeout(() => {
|
|
10687
10999
|
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
10688
11000
|
}, ms);
|
|
10689
11001
|
promise.then((val) => {
|
|
10690
11002
|
clearTimeout(timer);
|
|
10691
|
-
|
|
11003
|
+
resolve9(val);
|
|
10692
11004
|
}).catch((err) => {
|
|
10693
11005
|
clearTimeout(timer);
|
|
10694
11006
|
reject(err);
|
|
@@ -11120,6 +11432,7 @@ var SkillManager = class {
|
|
|
11120
11432
|
|
|
11121
11433
|
// src/web/tool-executor-web.ts
|
|
11122
11434
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
11435
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
11123
11436
|
import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
|
|
11124
11437
|
var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
11125
11438
|
constructor(registry, ws) {
|
|
@@ -11131,6 +11444,11 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11131
11444
|
hookConfig;
|
|
11132
11445
|
permissionRules = [];
|
|
11133
11446
|
defaultPermission = "confirm";
|
|
11447
|
+
permissionProfileName = "legacy";
|
|
11448
|
+
permissionProfile;
|
|
11449
|
+
allowedPermissionProfiles = [];
|
|
11450
|
+
workspaceRoot = process.cwd();
|
|
11451
|
+
networkPolicy;
|
|
11134
11452
|
/** Pending confirm promises keyed by requestId */
|
|
11135
11453
|
pendingConfirms = /* @__PURE__ */ new Map();
|
|
11136
11454
|
/** Pending batch confirm promises */
|
|
@@ -11165,6 +11483,11 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11165
11483
|
this.hookConfig = opts.hookConfig;
|
|
11166
11484
|
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
11167
11485
|
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
11486
|
+
if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
|
|
11487
|
+
if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
|
|
11488
|
+
if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
|
|
11489
|
+
if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
|
|
11490
|
+
if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
|
|
11168
11491
|
}
|
|
11169
11492
|
/** Clear M7 timeout timer for a requestId */
|
|
11170
11493
|
clearPendingTimer(requestId) {
|
|
@@ -11176,33 +11499,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11176
11499
|
}
|
|
11177
11500
|
/** Resolve a pending confirm from client response */
|
|
11178
11501
|
resolveConfirm(requestId, approved) {
|
|
11179
|
-
const
|
|
11180
|
-
if (
|
|
11502
|
+
const resolve9 = this.pendingConfirms.get(requestId);
|
|
11503
|
+
if (resolve9) {
|
|
11181
11504
|
this.clearPendingTimer(requestId);
|
|
11182
11505
|
this.pendingConfirms.delete(requestId);
|
|
11183
11506
|
this.confirming = false;
|
|
11184
|
-
|
|
11507
|
+
resolve9(approved);
|
|
11185
11508
|
}
|
|
11186
11509
|
}
|
|
11187
11510
|
/** Resolve a pending batch confirm from client response */
|
|
11188
11511
|
resolveBatchConfirm(requestId, decision) {
|
|
11189
|
-
const
|
|
11190
|
-
if (
|
|
11512
|
+
const resolve9 = this.pendingBatchConfirms.get(requestId);
|
|
11513
|
+
if (resolve9) {
|
|
11191
11514
|
this.clearPendingTimer(requestId);
|
|
11192
11515
|
this.pendingBatchConfirms.delete(requestId);
|
|
11193
11516
|
this.confirming = false;
|
|
11194
11517
|
if (decision === "all" || decision === "none") {
|
|
11195
|
-
|
|
11518
|
+
resolve9(decision);
|
|
11196
11519
|
} else {
|
|
11197
|
-
|
|
11520
|
+
resolve9(new Set(decision));
|
|
11198
11521
|
}
|
|
11199
11522
|
}
|
|
11200
11523
|
}
|
|
11201
11524
|
/** Cancel all pending confirms (e.g., on disconnect) */
|
|
11202
11525
|
cancelAll() {
|
|
11203
|
-
for (const
|
|
11526
|
+
for (const resolve9 of this.pendingConfirms.values()) resolve9(false);
|
|
11204
11527
|
this.pendingConfirms.clear();
|
|
11205
|
-
for (const
|
|
11528
|
+
for (const resolve9 of this.pendingBatchConfirms.values()) resolve9("none");
|
|
11206
11529
|
this.pendingBatchConfirms.clear();
|
|
11207
11530
|
this.confirming = false;
|
|
11208
11531
|
}
|
|
@@ -11213,6 +11536,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11213
11536
|
}
|
|
11214
11537
|
sendToolCallStart(call) {
|
|
11215
11538
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
11539
|
+
let toolCallStarted = false;
|
|
11216
11540
|
const startTime = Date.now();
|
|
11217
11541
|
this.toolStartTimes.set(call.id, startTime);
|
|
11218
11542
|
const msg = {
|
|
@@ -11278,8 +11602,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11278
11602
|
diff: this.getDiffPreview(call)
|
|
11279
11603
|
};
|
|
11280
11604
|
this.send(msg);
|
|
11281
|
-
return new Promise((
|
|
11282
|
-
this.pendingConfirms.set(requestId,
|
|
11605
|
+
return new Promise((resolve9) => {
|
|
11606
|
+
this.pendingConfirms.set(requestId, resolve9);
|
|
11283
11607
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
11284
11608
|
if (this.pendingConfirms.has(requestId)) {
|
|
11285
11609
|
this.resolveConfirm(requestId, false);
|
|
@@ -11303,8 +11627,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11303
11627
|
files
|
|
11304
11628
|
};
|
|
11305
11629
|
this.send(msg);
|
|
11306
|
-
return new Promise((
|
|
11307
|
-
this.pendingBatchConfirms.set(requestId,
|
|
11630
|
+
return new Promise((resolve9) => {
|
|
11631
|
+
this.pendingBatchConfirms.set(requestId, resolve9);
|
|
11308
11632
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
11309
11633
|
if (this.pendingBatchConfirms.has(requestId)) {
|
|
11310
11634
|
this.resolveBatchConfirm(requestId, "none");
|
|
@@ -11321,30 +11645,66 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11321
11645
|
return { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
|
|
11322
11646
|
}
|
|
11323
11647
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
11648
|
+
let toolCallStarted = false;
|
|
11324
11649
|
runHook(this.hookConfig?.preToolExecution, {
|
|
11325
11650
|
tool: call.name,
|
|
11326
11651
|
dangerLevel,
|
|
11327
11652
|
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
11328
11653
|
});
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11654
|
+
const permission = checkPermissionWithProfile(
|
|
11655
|
+
call.name,
|
|
11656
|
+
call.arguments,
|
|
11657
|
+
dangerLevel,
|
|
11658
|
+
this.permissionRules,
|
|
11659
|
+
this.defaultPermission,
|
|
11660
|
+
{
|
|
11661
|
+
profileName: this.permissionProfileName,
|
|
11662
|
+
profile: this.permissionProfile,
|
|
11663
|
+
allowedProfiles: this.allowedPermissionProfiles,
|
|
11664
|
+
workspaceRoot: this.workspaceRoot,
|
|
11665
|
+
tempDirs: [tmpdir2()]
|
|
11333
11666
|
}
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11667
|
+
);
|
|
11668
|
+
const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
|
|
11669
|
+
if (networkPermission?.action === "deny") {
|
|
11670
|
+
const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
|
|
11671
|
+
return {
|
|
11672
|
+
callId: call.id,
|
|
11673
|
+
content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
|
|
11674
|
+
isError: true
|
|
11675
|
+
};
|
|
11676
|
+
}
|
|
11677
|
+
if (permission.action === "deny") {
|
|
11678
|
+
const reason = permission.reason ? ` (${permission.reason})` : "";
|
|
11679
|
+
return {
|
|
11680
|
+
callId: call.id,
|
|
11681
|
+
content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
|
|
11682
|
+
isError: true
|
|
11683
|
+
};
|
|
11684
|
+
}
|
|
11685
|
+
if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
|
|
11686
|
+
this.sendToolCallStart(call);
|
|
11687
|
+
try {
|
|
11688
|
+
const rawContent = await runTool(tool, call.arguments, call.name);
|
|
11689
|
+
const content = truncateOutput(rawContent, call.name);
|
|
11690
|
+
this.sendToolCallResult(call, rawContent, false);
|
|
11691
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
11692
|
+
return { callId: call.id, content, isError: false };
|
|
11693
|
+
} catch (err) {
|
|
11694
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11695
|
+
this.sendToolCallResult(call, message, true);
|
|
11696
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
11697
|
+
return { callId: call.id, content: message, isError: true };
|
|
11698
|
+
}
|
|
11699
|
+
}
|
|
11700
|
+
if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
|
|
11701
|
+
this.sendToolCallStart(call);
|
|
11702
|
+
toolCallStarted = true;
|
|
11703
|
+
const confirmed = await this.confirm(call, "write");
|
|
11704
|
+
if (!confirmed) {
|
|
11705
|
+
const rejectionMsg = `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`;
|
|
11706
|
+
this.sendToolCallResult(call, rejectionMsg, true);
|
|
11707
|
+
return { callId: call.id, content: rejectionMsg, isError: true };
|
|
11348
11708
|
}
|
|
11349
11709
|
}
|
|
11350
11710
|
this.sendToolCallStart(call);
|
|
@@ -11403,6 +11763,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11403
11763
|
return results;
|
|
11404
11764
|
}
|
|
11405
11765
|
async executeBatchFileWrites(items) {
|
|
11766
|
+
if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
|
|
11767
|
+
return Promise.all(items.map(({ call }) => this.execute(call)));
|
|
11768
|
+
}
|
|
11406
11769
|
const calls = items.map((i) => i.call);
|
|
11407
11770
|
const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
|
|
11408
11771
|
const results = new Array(calls.length);
|
|
@@ -11729,14 +12092,171 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
|
|
|
11729
12092
|
return committedSize < originalSize;
|
|
11730
12093
|
}
|
|
11731
12094
|
|
|
12095
|
+
// src/core/context-files.ts
|
|
12096
|
+
import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
|
|
12097
|
+
import { join as join13, relative as relative3, resolve as resolve6 } from "path";
|
|
12098
|
+
function uniqueNonEmpty(values) {
|
|
12099
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12100
|
+
const out = [];
|
|
12101
|
+
for (const value of values) {
|
|
12102
|
+
const trimmed = value.trim();
|
|
12103
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
12104
|
+
seen.add(trimmed);
|
|
12105
|
+
out.push(trimmed);
|
|
12106
|
+
}
|
|
12107
|
+
return out;
|
|
12108
|
+
}
|
|
12109
|
+
function buildContextFileCandidates(fallbackFilenames = []) {
|
|
12110
|
+
return uniqueNonEmpty([...CONTEXT_FILE_CANDIDATES, ...fallbackFilenames]);
|
|
12111
|
+
}
|
|
12112
|
+
function displayPath(filePath, cwd) {
|
|
12113
|
+
const rel = relative3(cwd, filePath);
|
|
12114
|
+
if (rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\")) {
|
|
12115
|
+
return rel;
|
|
12116
|
+
}
|
|
12117
|
+
return filePath;
|
|
12118
|
+
}
|
|
12119
|
+
function isInsideOrEqual(parent, child) {
|
|
12120
|
+
const rel = relative3(resolve6(parent), resolve6(child));
|
|
12121
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
|
|
12122
|
+
}
|
|
12123
|
+
function readContextFile(level, filePath, cwd, maxBytes) {
|
|
12124
|
+
const name = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
12125
|
+
const shown = displayPath(filePath, cwd);
|
|
12126
|
+
try {
|
|
12127
|
+
const raw = readFileSync15(filePath);
|
|
12128
|
+
const byteCount = raw.byteLength;
|
|
12129
|
+
if (byteCount === 0) {
|
|
12130
|
+
return {
|
|
12131
|
+
layer: null,
|
|
12132
|
+
skipped: { level, filePath, displayPath: shown, fileName: name, reason: "empty" }
|
|
12133
|
+
};
|
|
12134
|
+
}
|
|
12135
|
+
const truncated = byteCount > maxBytes;
|
|
12136
|
+
const bytes = truncated ? raw.subarray(0, maxBytes) : raw;
|
|
12137
|
+
const content = bytes.toString("utf-8").trim();
|
|
12138
|
+
if (!content) {
|
|
12139
|
+
return {
|
|
12140
|
+
layer: null,
|
|
12141
|
+
skipped: { level, filePath, displayPath: shown, fileName: name, reason: "empty" }
|
|
12142
|
+
};
|
|
12143
|
+
}
|
|
12144
|
+
return {
|
|
12145
|
+
layer: {
|
|
12146
|
+
level: level === "single" ? "project" : level,
|
|
12147
|
+
filePath,
|
|
12148
|
+
displayPath: shown,
|
|
12149
|
+
fileName: name,
|
|
12150
|
+
content,
|
|
12151
|
+
charCount: content.length,
|
|
12152
|
+
byteCount,
|
|
12153
|
+
truncated
|
|
12154
|
+
},
|
|
12155
|
+
skipped: truncated ? { level, filePath, displayPath: shown, fileName: name, reason: "too-large", message: `truncated to ${maxBytes} bytes` } : null
|
|
12156
|
+
};
|
|
12157
|
+
} catch (err) {
|
|
12158
|
+
return {
|
|
12159
|
+
layer: null,
|
|
12160
|
+
skipped: {
|
|
12161
|
+
level,
|
|
12162
|
+
filePath,
|
|
12163
|
+
displayPath: shown,
|
|
12164
|
+
fileName: name,
|
|
12165
|
+
reason: "read-error",
|
|
12166
|
+
message: err instanceof Error ? err.message : String(err)
|
|
12167
|
+
}
|
|
12168
|
+
};
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
|
|
12172
|
+
const skipped = [];
|
|
12173
|
+
for (const candidate of candidates) {
|
|
12174
|
+
const filePath = join13(dir, candidate);
|
|
12175
|
+
if (!existsSync20(filePath)) continue;
|
|
12176
|
+
const result = readContextFile(level, filePath, cwd, maxBytes);
|
|
12177
|
+
if (result.skipped) skipped.push(result.skipped);
|
|
12178
|
+
if (result.layer) return { layer: result.layer, skipped };
|
|
12179
|
+
}
|
|
12180
|
+
return { layer: null, skipped };
|
|
12181
|
+
}
|
|
12182
|
+
function loadContextFiles(options) {
|
|
12183
|
+
const cwd = resolve6(options.cwd);
|
|
12184
|
+
const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
|
|
12185
|
+
const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
|
|
12186
|
+
const skipped = [];
|
|
12187
|
+
if (options.setting === false) {
|
|
12188
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
12189
|
+
}
|
|
12190
|
+
if (options.setting && options.setting !== "auto") {
|
|
12191
|
+
const filePath = resolve6(cwd, String(options.setting));
|
|
12192
|
+
if (!isInsideOrEqual(cwd, filePath)) {
|
|
12193
|
+
skipped.push({
|
|
12194
|
+
level: "single",
|
|
12195
|
+
filePath,
|
|
12196
|
+
displayPath: filePath,
|
|
12197
|
+
fileName: String(options.setting),
|
|
12198
|
+
reason: "outside-cwd"
|
|
12199
|
+
});
|
|
12200
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
12201
|
+
}
|
|
12202
|
+
if (!existsSync20(filePath)) {
|
|
12203
|
+
skipped.push({
|
|
12204
|
+
level: "single",
|
|
12205
|
+
filePath,
|
|
12206
|
+
displayPath: displayPath(filePath, cwd),
|
|
12207
|
+
fileName: String(options.setting),
|
|
12208
|
+
reason: "missing"
|
|
12209
|
+
});
|
|
12210
|
+
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
12211
|
+
}
|
|
12212
|
+
const result = readContextFile("single", filePath, cwd, maxBytes);
|
|
12213
|
+
if (result.skipped) skipped.push(result.skipped);
|
|
12214
|
+
const layers2 = result.layer ? [result.layer] : [];
|
|
12215
|
+
const mergedContent2 = layers2.map((l) => l.content).join("\n\n---\n\n");
|
|
12216
|
+
return {
|
|
12217
|
+
layers: layers2,
|
|
12218
|
+
mergedContent: mergedContent2,
|
|
12219
|
+
skipped,
|
|
12220
|
+
totalChars: layers2.reduce((sum, l) => sum + l.charCount, 0),
|
|
12221
|
+
totalBytes: layers2.reduce((sum, l) => sum + l.byteCount, 0),
|
|
12222
|
+
maxBytes,
|
|
12223
|
+
candidates
|
|
12224
|
+
};
|
|
12225
|
+
}
|
|
12226
|
+
const layers = [];
|
|
12227
|
+
for (const [level, dir] of [
|
|
12228
|
+
["global", options.configDir],
|
|
12229
|
+
["project", options.projectRoot]
|
|
12230
|
+
]) {
|
|
12231
|
+
const result = findFirstContextFile(level, dir, cwd, candidates, maxBytes);
|
|
12232
|
+
skipped.push(...result.skipped);
|
|
12233
|
+
if (result.layer) layers.push(result.layer);
|
|
12234
|
+
}
|
|
12235
|
+
if (resolve6(options.cwd) !== resolve6(options.projectRoot)) {
|
|
12236
|
+
const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
|
|
12237
|
+
skipped.push(...result.skipped);
|
|
12238
|
+
if (result.layer) layers.push(result.layer);
|
|
12239
|
+
}
|
|
12240
|
+
const mergedContent = layers.map((l) => l.content).join("\n\n---\n\n");
|
|
12241
|
+
return {
|
|
12242
|
+
layers,
|
|
12243
|
+
mergedContent,
|
|
12244
|
+
skipped,
|
|
12245
|
+
totalChars: layers.reduce((sum, l) => sum + l.charCount, 0),
|
|
12246
|
+
totalBytes: layers.reduce((sum, l) => sum + l.byteCount, 0),
|
|
12247
|
+
maxBytes,
|
|
12248
|
+
candidates
|
|
12249
|
+
};
|
|
12250
|
+
}
|
|
12251
|
+
|
|
11732
12252
|
// src/web/session-handler.ts
|
|
11733
|
-
import { existsSync as
|
|
11734
|
-
import { join as
|
|
12253
|
+
import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync2, mkdirSync as mkdirSync10, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
|
|
12254
|
+
import { join as join17, resolve as resolve7, dirname as dirname5 } from "path";
|
|
11735
12255
|
|
|
11736
12256
|
// src/tools/git-context.ts
|
|
11737
12257
|
import { execSync as execSync2 } from "child_process";
|
|
11738
|
-
import { existsSync as
|
|
11739
|
-
import { join as
|
|
12258
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12259
|
+
import { join as join14 } from "path";
|
|
11740
12260
|
function runGit2(cmd, cwd) {
|
|
11741
12261
|
try {
|
|
11742
12262
|
return execSync2(`git ${cmd}`, {
|
|
@@ -11753,7 +12273,7 @@ function getGitRoot(cwd = process.cwd()) {
|
|
|
11753
12273
|
return runGit2("rev-parse --show-toplevel", cwd);
|
|
11754
12274
|
}
|
|
11755
12275
|
function getGitContext(cwd = process.cwd()) {
|
|
11756
|
-
if (!
|
|
12276
|
+
if (!existsSync21(join14(cwd, ".git"))) {
|
|
11757
12277
|
const result = runGit2("rev-parse --git-dir", cwd);
|
|
11758
12278
|
if (!result) return null;
|
|
11759
12279
|
}
|
|
@@ -11823,6 +12343,253 @@ function formatGitContextForPrompt(ctx) {
|
|
|
11823
12343
|
return lines.join("\n");
|
|
11824
12344
|
}
|
|
11825
12345
|
|
|
12346
|
+
// src/cli/review-prompts.ts
|
|
12347
|
+
function buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
12348
|
+
const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
|
|
12349
|
+
return `# Code Review Request
|
|
12350
|
+
|
|
12351
|
+
${level}
|
|
12352
|
+
|
|
12353
|
+
## Git Status
|
|
12354
|
+
${gitContextStr}
|
|
12355
|
+
|
|
12356
|
+
## Code Changes (diff)
|
|
12357
|
+
\`\`\`diff
|
|
12358
|
+
${diff}
|
|
12359
|
+
\`\`\`
|
|
12360
|
+
|
|
12361
|
+
## Output Format
|
|
12362
|
+
Please structure your review as follows:
|
|
12363
|
+
1. **Overall Assessment**: One-sentence summary of the change quality
|
|
12364
|
+
2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
|
|
12365
|
+
3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
|
|
12366
|
+
4. **Highlights** (if any): Good practices worth acknowledging
|
|
12367
|
+
|
|
12368
|
+
Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
12369
|
+
}
|
|
12370
|
+
function buildSecurityReviewPrompt(diff, gitContextStr) {
|
|
12371
|
+
return `# Security Vulnerability Review
|
|
12372
|
+
|
|
12373
|
+
Analyze the following code changes **exclusively for security vulnerabilities**.
|
|
12374
|
+
|
|
12375
|
+
## Categories to check:
|
|
12376
|
+
1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
|
|
12377
|
+
2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
|
|
12378
|
+
3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
|
|
12379
|
+
4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
|
|
12380
|
+
5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
|
|
12381
|
+
6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
|
|
12382
|
+
7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
|
|
12383
|
+
8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
|
|
12384
|
+
|
|
12385
|
+
## Git Status
|
|
12386
|
+
${gitContextStr}
|
|
12387
|
+
|
|
12388
|
+
## Code Changes (diff)
|
|
12389
|
+
\`\`\`diff
|
|
12390
|
+
${diff}
|
|
12391
|
+
\`\`\`
|
|
12392
|
+
|
|
12393
|
+
## Output Format
|
|
12394
|
+
For each finding:
|
|
12395
|
+
- **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
|
|
12396
|
+
- **Category**: (from list above)
|
|
12397
|
+
- **File & location**: file:line
|
|
12398
|
+
- **Description**: what the vulnerability is and how it could be exploited
|
|
12399
|
+
- **Recommended fix**: specific code change to resolve
|
|
12400
|
+
|
|
12401
|
+
If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
|
|
12402
|
+
}
|
|
12403
|
+
|
|
12404
|
+
// src/repl/commands/project-init.ts
|
|
12405
|
+
import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
|
|
12406
|
+
import { join as join15 } from "path";
|
|
12407
|
+
var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12408
|
+
"node_modules",
|
|
12409
|
+
".git",
|
|
12410
|
+
"dist",
|
|
12411
|
+
"build",
|
|
12412
|
+
"out",
|
|
12413
|
+
"target",
|
|
12414
|
+
".next",
|
|
12415
|
+
".nuxt",
|
|
12416
|
+
"__pycache__",
|
|
12417
|
+
".venv",
|
|
12418
|
+
"venv",
|
|
12419
|
+
".tox",
|
|
12420
|
+
".mypy_cache",
|
|
12421
|
+
".pytest_cache",
|
|
12422
|
+
".gradle",
|
|
12423
|
+
".idea",
|
|
12424
|
+
".vscode",
|
|
12425
|
+
".vs",
|
|
12426
|
+
"coverage",
|
|
12427
|
+
".cache",
|
|
12428
|
+
".parcel-cache",
|
|
12429
|
+
"dist-cjs",
|
|
12430
|
+
"release",
|
|
12431
|
+
".output",
|
|
12432
|
+
".turbo",
|
|
12433
|
+
"vendor"
|
|
12434
|
+
]);
|
|
12435
|
+
function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
12436
|
+
const lines = [];
|
|
12437
|
+
let count = 0;
|
|
12438
|
+
const walk = (d, prefix, depth) => {
|
|
12439
|
+
if (depth > maxDepth || count >= maxEntries) return;
|
|
12440
|
+
let entries;
|
|
12441
|
+
try {
|
|
12442
|
+
entries = readdirSync9(d);
|
|
12443
|
+
} catch {
|
|
12444
|
+
return;
|
|
12445
|
+
}
|
|
12446
|
+
const filtered = entries.filter((e) => !e.startsWith(".") && !SCAN_SKIP_DIRS.has(e));
|
|
12447
|
+
const sorted = filtered.sort((a, b) => {
|
|
12448
|
+
let aIsDir = false, bIsDir = false;
|
|
12449
|
+
try {
|
|
12450
|
+
aIsDir = statSync8(join15(d, a)).isDirectory();
|
|
12451
|
+
} catch {
|
|
12452
|
+
}
|
|
12453
|
+
try {
|
|
12454
|
+
bIsDir = statSync8(join15(d, b)).isDirectory();
|
|
12455
|
+
} catch {
|
|
12456
|
+
}
|
|
12457
|
+
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
12458
|
+
return a.localeCompare(b);
|
|
12459
|
+
});
|
|
12460
|
+
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
12461
|
+
const name = sorted[i];
|
|
12462
|
+
const fullPath = join15(d, name);
|
|
12463
|
+
const isLast = i === sorted.length - 1;
|
|
12464
|
+
const connector = isLast ? "+-- " : "|-- ";
|
|
12465
|
+
let isDir;
|
|
12466
|
+
try {
|
|
12467
|
+
isDir = statSync8(fullPath).isDirectory();
|
|
12468
|
+
} catch {
|
|
12469
|
+
continue;
|
|
12470
|
+
}
|
|
12471
|
+
lines.push(prefix + connector + name + (isDir ? "/" : ""));
|
|
12472
|
+
count++;
|
|
12473
|
+
if (isDir) {
|
|
12474
|
+
walk(fullPath, prefix + (isLast ? " " : "| "), depth + 1);
|
|
12475
|
+
}
|
|
12476
|
+
}
|
|
12477
|
+
};
|
|
12478
|
+
walk(dir, "", 0);
|
|
12479
|
+
if (count >= maxEntries) lines.push("... (truncated)");
|
|
12480
|
+
return lines.join("\n");
|
|
12481
|
+
}
|
|
12482
|
+
function scanProject(cwd) {
|
|
12483
|
+
const info = {
|
|
12484
|
+
type: "unknown",
|
|
12485
|
+
language: "unknown",
|
|
12486
|
+
configFiles: [],
|
|
12487
|
+
directoryStructure: ""
|
|
12488
|
+
};
|
|
12489
|
+
const check = (file) => existsSync22(join15(cwd, file));
|
|
12490
|
+
const configCandidates = [
|
|
12491
|
+
"package.json",
|
|
12492
|
+
"tsconfig.json",
|
|
12493
|
+
"Cargo.toml",
|
|
12494
|
+
"pyproject.toml",
|
|
12495
|
+
"setup.py",
|
|
12496
|
+
"requirements.txt",
|
|
12497
|
+
"go.mod",
|
|
12498
|
+
"pom.xml",
|
|
12499
|
+
"build.gradle",
|
|
12500
|
+
"build.gradle.kts",
|
|
12501
|
+
"CMakeLists.txt",
|
|
12502
|
+
"Makefile",
|
|
12503
|
+
".csproj",
|
|
12504
|
+
".sln",
|
|
12505
|
+
"composer.json",
|
|
12506
|
+
"Gemfile",
|
|
12507
|
+
"mix.exs",
|
|
12508
|
+
"deno.json",
|
|
12509
|
+
"bun.lockb"
|
|
12510
|
+
];
|
|
12511
|
+
info.configFiles = configCandidates.filter(check);
|
|
12512
|
+
if (check("package.json")) {
|
|
12513
|
+
info.type = "node";
|
|
12514
|
+
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
12515
|
+
try {
|
|
12516
|
+
const pkg = JSON.parse(readFileSync16(join15(cwd, "package.json"), "utf-8"));
|
|
12517
|
+
const scripts = pkg.scripts ?? {};
|
|
12518
|
+
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
12519
|
+
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
12520
|
+
info.devCommand = scripts.dev ? "npm run dev" : void 0;
|
|
12521
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
12522
|
+
if (allDeps["react"]) info.framework = "React";
|
|
12523
|
+
else if (allDeps["vue"]) info.framework = "Vue";
|
|
12524
|
+
else if (allDeps["@angular/core"]) info.framework = "Angular";
|
|
12525
|
+
else if (allDeps["next"]) info.framework = "Next.js";
|
|
12526
|
+
else if (allDeps["nuxt"]) info.framework = "Nuxt";
|
|
12527
|
+
else if (allDeps["express"]) info.framework = "Express";
|
|
12528
|
+
else if (allDeps["fastify"]) info.framework = "Fastify";
|
|
12529
|
+
else if (allDeps["svelte"]) info.framework = "Svelte";
|
|
12530
|
+
} catch {
|
|
12531
|
+
}
|
|
12532
|
+
} else if (check("Cargo.toml")) {
|
|
12533
|
+
info.type = "rust";
|
|
12534
|
+
info.language = "Rust";
|
|
12535
|
+
info.buildCommand = "cargo build";
|
|
12536
|
+
info.testCommand = "cargo test";
|
|
12537
|
+
} else if (check("pyproject.toml") || check("setup.py") || check("requirements.txt")) {
|
|
12538
|
+
info.type = "python";
|
|
12539
|
+
info.language = "Python";
|
|
12540
|
+
info.testCommand = check("pytest.ini") || check("pyproject.toml") ? "pytest" : "python -m unittest";
|
|
12541
|
+
} else if (check("go.mod")) {
|
|
12542
|
+
info.type = "go";
|
|
12543
|
+
info.language = "Go";
|
|
12544
|
+
info.buildCommand = "go build ./...";
|
|
12545
|
+
info.testCommand = "go test ./...";
|
|
12546
|
+
} else if (check("pom.xml")) {
|
|
12547
|
+
info.type = "java";
|
|
12548
|
+
info.language = "Java";
|
|
12549
|
+
info.buildCommand = "mvn package";
|
|
12550
|
+
info.testCommand = "mvn test";
|
|
12551
|
+
} else if (check("build.gradle") || check("build.gradle.kts")) {
|
|
12552
|
+
info.type = "java";
|
|
12553
|
+
info.language = "Java/Kotlin";
|
|
12554
|
+
info.buildCommand = "./gradlew build";
|
|
12555
|
+
info.testCommand = "./gradlew test";
|
|
12556
|
+
}
|
|
12557
|
+
info.directoryStructure = scanDirTree(cwd);
|
|
12558
|
+
return info;
|
|
12559
|
+
}
|
|
12560
|
+
function buildInitPrompt(info, cwd) {
|
|
12561
|
+
const parts = [
|
|
12562
|
+
"Please generate an AICLI.md context file (Markdown format) for the following project. This file will be injected into the AI conversation system prompt to help the AI understand the project structure and coding conventions.",
|
|
12563
|
+
"\n## Project Info\n",
|
|
12564
|
+
`- Working directory: ${cwd}`,
|
|
12565
|
+
`- Type: ${info.type}`,
|
|
12566
|
+
`- Language: ${info.language}`
|
|
12567
|
+
];
|
|
12568
|
+
if (info.framework) parts.push(`- Framework: ${info.framework}`);
|
|
12569
|
+
if (info.buildCommand) parts.push(`- Build command: ${info.buildCommand}`);
|
|
12570
|
+
if (info.testCommand) parts.push(`- Test command: ${info.testCommand}`);
|
|
12571
|
+
if (info.devCommand) parts.push(`- Dev command: ${info.devCommand}`);
|
|
12572
|
+
parts.push(`
|
|
12573
|
+
## Detected Config Files
|
|
12574
|
+
${info.configFiles.map((f) => `- ${f}`).join("\n")}`);
|
|
12575
|
+
parts.push(`
|
|
12576
|
+
## Directory Structure
|
|
12577
|
+
\`\`\`
|
|
12578
|
+
${info.directoryStructure}
|
|
12579
|
+
\`\`\``);
|
|
12580
|
+
parts.push(`
|
|
12581
|
+
## Requirements
|
|
12582
|
+
Please generate a structured Markdown file containing:
|
|
12583
|
+
1. Project overview (one-sentence summary)
|
|
12584
|
+
2. Tech stack
|
|
12585
|
+
3. Project structure description (based on the directory structure above)
|
|
12586
|
+
4. Common commands (build, test, dev, etc.)
|
|
12587
|
+
5. Code style and conventions (inferred from config files)
|
|
12588
|
+
|
|
12589
|
+
Output the Markdown content directly, do not wrap the entire file in a code block. Keep it concise, within 200 lines.`);
|
|
12590
|
+
return parts.join("\n");
|
|
12591
|
+
}
|
|
12592
|
+
|
|
11826
12593
|
// src/core/git-diff.ts
|
|
11827
12594
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
11828
12595
|
function readGitDiff(options = {}) {
|
|
@@ -12406,7 +13173,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
|
|
|
12406
13173
|
}
|
|
12407
13174
|
|
|
12408
13175
|
// src/hub/persist.ts
|
|
12409
|
-
import { join as
|
|
13176
|
+
import { join as join16 } from "path";
|
|
12410
13177
|
function discussionToMessages(state2) {
|
|
12411
13178
|
const out = [];
|
|
12412
13179
|
const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
|
|
@@ -12444,7 +13211,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
|
|
|
12444
13211
|
session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
|
|
12445
13212
|
session.titleAiGenerated = true;
|
|
12446
13213
|
await sm.save();
|
|
12447
|
-
return { id: session.id, path:
|
|
13214
|
+
return { id: session.id, path: join16(config.getHistoryDir(), `${session.id}.json`) };
|
|
12448
13215
|
}
|
|
12449
13216
|
|
|
12450
13217
|
// src/web/session-handler.ts
|
|
@@ -12462,7 +13229,7 @@ function lastAssistantText(messages) {
|
|
|
12462
13229
|
}
|
|
12463
13230
|
return void 0;
|
|
12464
13231
|
}
|
|
12465
|
-
var SessionHandler = class
|
|
13232
|
+
var SessionHandler = class {
|
|
12466
13233
|
ws;
|
|
12467
13234
|
config;
|
|
12468
13235
|
providers;
|
|
@@ -12499,6 +13266,7 @@ var SessionHandler = class _SessionHandler {
|
|
|
12499
13266
|
pendingAutoPause = /* @__PURE__ */ new Map();
|
|
12500
13267
|
/** Active system prompt from context files */
|
|
12501
13268
|
activeSystemPrompt;
|
|
13269
|
+
contextLoadResult = null;
|
|
12502
13270
|
/** Directories added via /add-dir */
|
|
12503
13271
|
addedDirs = /* @__PURE__ */ new Set();
|
|
12504
13272
|
/** Track MCP tool names used across this session (for MCP budget prioritization) */
|
|
@@ -12542,7 +13310,22 @@ var SessionHandler = class _SessionHandler {
|
|
|
12542
13310
|
const hooks = this.config.get("hooks");
|
|
12543
13311
|
const permissionRules = this.config.get("permissionRules");
|
|
12544
13312
|
const defaultPermission = this.config.get("defaultPermission");
|
|
12545
|
-
this.
|
|
13313
|
+
const permissionProfileName = this.config.get("defaultPermissionProfile") ?? "legacy";
|
|
13314
|
+
const permissionProfiles = this.config.get("permissionProfiles") ?? {};
|
|
13315
|
+
this.toolExecutor.setConfig({
|
|
13316
|
+
hookConfig: hooks,
|
|
13317
|
+
permissionRules,
|
|
13318
|
+
defaultPermission,
|
|
13319
|
+
permissionProfileName,
|
|
13320
|
+
permissionProfile: permissionProfiles[permissionProfileName],
|
|
13321
|
+
allowedPermissionProfiles: this.config.get("allowedPermissionProfiles") ?? [],
|
|
13322
|
+
workspaceRoot: process.cwd(),
|
|
13323
|
+
networkPolicy: this.config.get("networkPolicy")
|
|
13324
|
+
});
|
|
13325
|
+
const permissionProfileWarning = formatPermissionProfileWarning(permissionProfileName);
|
|
13326
|
+
if (permissionProfileWarning) {
|
|
13327
|
+
this.send({ type: "info", message: permissionProfileWarning });
|
|
13328
|
+
}
|
|
12546
13329
|
this.sendStatus();
|
|
12547
13330
|
askUserContext.rl = null;
|
|
12548
13331
|
askUserContext.prompting = false;
|
|
@@ -12577,6 +13360,7 @@ var SessionHandler = class _SessionHandler {
|
|
|
12577
13360
|
messageCount: this.sessions.current?.messages.length ?? 0,
|
|
12578
13361
|
planMode: this.planMode,
|
|
12579
13362
|
thinkingMode: this.runtimeThinking ?? false,
|
|
13363
|
+
permissionProfile: this.config.get("defaultPermissionProfile") ?? "legacy",
|
|
12580
13364
|
tokenUsage: { ...this.sessionTokenUsage },
|
|
12581
13365
|
costUsd,
|
|
12582
13366
|
providers: providerList,
|
|
@@ -12605,10 +13389,10 @@ var SessionHandler = class _SessionHandler {
|
|
|
12605
13389
|
return;
|
|
12606
13390
|
}
|
|
12607
13391
|
case "ask_user_response": {
|
|
12608
|
-
const
|
|
12609
|
-
if (
|
|
13392
|
+
const resolve9 = this.pendingAskUser.get(msg.requestId);
|
|
13393
|
+
if (resolve9) {
|
|
12610
13394
|
this.pendingAskUser.delete(msg.requestId);
|
|
12611
|
-
|
|
13395
|
+
resolve9(msg.answer);
|
|
12612
13396
|
}
|
|
12613
13397
|
return;
|
|
12614
13398
|
}
|
|
@@ -12619,10 +13403,10 @@ var SessionHandler = class _SessionHandler {
|
|
|
12619
13403
|
case "memory_rebuild":
|
|
12620
13404
|
return this.handleMemoryRebuild(Boolean(msg.full));
|
|
12621
13405
|
case "auto_pause_response": {
|
|
12622
|
-
const
|
|
12623
|
-
if (
|
|
13406
|
+
const resolve9 = this.pendingAutoPause.get(msg.requestId);
|
|
13407
|
+
if (resolve9) {
|
|
12624
13408
|
this.pendingAutoPause.delete(msg.requestId);
|
|
12625
|
-
|
|
13409
|
+
resolve9({ action: msg.action, message: msg.message });
|
|
12626
13410
|
}
|
|
12627
13411
|
return;
|
|
12628
13412
|
}
|
|
@@ -12637,10 +13421,10 @@ var SessionHandler = class _SessionHandler {
|
|
|
12637
13421
|
this.hubOrchestrator?.abort();
|
|
12638
13422
|
return;
|
|
12639
13423
|
case "hub_steer": {
|
|
12640
|
-
const
|
|
12641
|
-
if (
|
|
13424
|
+
const resolve9 = this.pendingHubReview.get(msg.requestId);
|
|
13425
|
+
if (resolve9) {
|
|
12642
13426
|
this.pendingHubReview.delete(msg.requestId);
|
|
12643
|
-
|
|
13427
|
+
resolve9({ action: msg.action, message: msg.message });
|
|
12644
13428
|
}
|
|
12645
13429
|
return;
|
|
12646
13430
|
}
|
|
@@ -12652,9 +13436,9 @@ var SessionHandler = class _SessionHandler {
|
|
|
12652
13436
|
onDisconnect() {
|
|
12653
13437
|
this.toolExecutor.cancelAll();
|
|
12654
13438
|
if (this.abortController) this.abortController.abort();
|
|
12655
|
-
for (const
|
|
13439
|
+
for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
|
|
12656
13440
|
this.pendingAskUser.clear();
|
|
12657
|
-
for (const
|
|
13441
|
+
for (const resolve9 of this.pendingAutoPause.values()) resolve9({ action: "stop" });
|
|
12658
13442
|
this.pendingAutoPause.clear();
|
|
12659
13443
|
this.saveIfNeeded();
|
|
12660
13444
|
}
|
|
@@ -12788,9 +13572,9 @@ var SessionHandler = class _SessionHandler {
|
|
|
12788
13572
|
this.hubOrchestrator = orchestrator;
|
|
12789
13573
|
orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
|
|
12790
13574
|
if (config.humanSteer) {
|
|
12791
|
-
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((
|
|
13575
|
+
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve9) => {
|
|
12792
13576
|
const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
12793
|
-
this.pendingHubReview.set(requestId,
|
|
13577
|
+
this.pendingHubReview.set(requestId, resolve9);
|
|
12794
13578
|
this.send({ type: "hub_review", requestId, round, maxRounds });
|
|
12795
13579
|
});
|
|
12796
13580
|
}
|
|
@@ -13127,8 +13911,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
13127
13911
|
onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
|
|
13128
13912
|
requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
|
|
13129
13913
|
const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
13130
|
-
const pauseResp = await new Promise((
|
|
13131
|
-
this.pendingAutoPause.set(requestId,
|
|
13914
|
+
const pauseResp = await new Promise((resolve9) => {
|
|
13915
|
+
this.pendingAutoPause.set(requestId, resolve9);
|
|
13132
13916
|
this.send({
|
|
13133
13917
|
type: "auto_pause_request",
|
|
13134
13918
|
requestId,
|
|
@@ -13307,8 +14091,8 @@ ${summaryContent}`,
|
|
|
13307
14091
|
}
|
|
13308
14092
|
if (chunk.done) break;
|
|
13309
14093
|
}
|
|
13310
|
-
await new Promise((
|
|
13311
|
-
fileStream.end((err) => err ? reject(err) :
|
|
14094
|
+
await new Promise((resolve9, reject) => {
|
|
14095
|
+
fileStream.end((err) => err ? reject(err) : resolve9());
|
|
13312
14096
|
});
|
|
13313
14097
|
const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
|
|
13314
14098
|
if (verdict.kind === "reject") {
|
|
@@ -13334,7 +14118,7 @@ ${summaryContent}`,
|
|
|
13334
14118
|
} catch (err) {
|
|
13335
14119
|
if (fileStream) {
|
|
13336
14120
|
try {
|
|
13337
|
-
await new Promise((
|
|
14121
|
+
await new Promise((resolve9) => fileStream.end(() => resolve9()));
|
|
13338
14122
|
} catch {
|
|
13339
14123
|
}
|
|
13340
14124
|
}
|
|
@@ -13623,7 +14407,7 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
13623
14407
|
" /plan [enter|exit] \u2014 Toggle read-only planning mode",
|
|
13624
14408
|
" /session new|list|load|delete <id> \u2014 Session management",
|
|
13625
14409
|
" /system [prompt|clear] \u2014 Set/view/reset system prompt",
|
|
13626
|
-
" /context [reload]
|
|
14410
|
+
" /context [status|reload] \u2014 Show/reload context layers",
|
|
13627
14411
|
" /status \u2014 Show session info & token usage",
|
|
13628
14412
|
" /cost \u2014 Show cumulative token usage",
|
|
13629
14413
|
" /config [show|get|set] \u2014 View/modify configuration",
|
|
@@ -13864,9 +14648,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
13864
14648
|
let modifiedFiles = 0;
|
|
13865
14649
|
const diffLines2 = [];
|
|
13866
14650
|
for (const [filePath, { earliest }] of fileMap) {
|
|
13867
|
-
const currentContent =
|
|
14651
|
+
const currentContent = existsSync23(filePath) ? (() => {
|
|
13868
14652
|
try {
|
|
13869
|
-
return
|
|
14653
|
+
return readFileSync17(filePath, "utf-8");
|
|
13870
14654
|
} catch {
|
|
13871
14655
|
return null;
|
|
13872
14656
|
}
|
|
@@ -13967,7 +14751,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
13967
14751
|
break;
|
|
13968
14752
|
}
|
|
13969
14753
|
const sub = args[0]?.toLowerCase();
|
|
13970
|
-
const
|
|
14754
|
+
const resolve9 = (ref) => {
|
|
13971
14755
|
const r = session.resolveBranchRef(ref);
|
|
13972
14756
|
if (r.ok) return r.id;
|
|
13973
14757
|
if (r.reason === "ambiguous") {
|
|
@@ -14021,7 +14805,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14021
14805
|
this.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
14022
14806
|
break;
|
|
14023
14807
|
}
|
|
14024
|
-
const id =
|
|
14808
|
+
const id = resolve9(ref);
|
|
14025
14809
|
if (!id) break;
|
|
14026
14810
|
const ok = session.switchBranch(id);
|
|
14027
14811
|
if (ok) {
|
|
@@ -14041,7 +14825,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14041
14825
|
this.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
14042
14826
|
break;
|
|
14043
14827
|
}
|
|
14044
|
-
const id =
|
|
14828
|
+
const id = resolve9(ref);
|
|
14045
14829
|
if (!id) break;
|
|
14046
14830
|
const ok = session.deleteBranch(id);
|
|
14047
14831
|
if (ok) {
|
|
@@ -14060,7 +14844,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14060
14844
|
this.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
14061
14845
|
break;
|
|
14062
14846
|
}
|
|
14063
|
-
const id =
|
|
14847
|
+
const id = resolve9(ref);
|
|
14064
14848
|
if (!id) break;
|
|
14065
14849
|
const ok = session.renameBranch(id, title);
|
|
14066
14850
|
if (ok) {
|
|
@@ -14078,7 +14862,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14078
14862
|
this.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
14079
14863
|
break;
|
|
14080
14864
|
}
|
|
14081
|
-
const id =
|
|
14865
|
+
const id = resolve9(ref);
|
|
14082
14866
|
if (!id) break;
|
|
14083
14867
|
const d = session.diffBranches(id);
|
|
14084
14868
|
if (!d) {
|
|
@@ -14120,7 +14904,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14120
14904
|
this.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
14121
14905
|
break;
|
|
14122
14906
|
}
|
|
14123
|
-
const id =
|
|
14907
|
+
const id = resolve9(ref);
|
|
14124
14908
|
if (!id) break;
|
|
14125
14909
|
const idx = parseInt(idxStr, 10);
|
|
14126
14910
|
if (Number.isNaN(idx)) {
|
|
@@ -14283,7 +15067,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14283
15067
|
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
14284
15068
|
truncated = true;
|
|
14285
15069
|
}
|
|
14286
|
-
const reviewPrompt =
|
|
15070
|
+
const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
14287
15071
|
this.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
|
|
14288
15072
|
try {
|
|
14289
15073
|
const review = await this.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
|
|
@@ -14321,7 +15105,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14321
15105
|
secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
|
|
14322
15106
|
secTruncated = true;
|
|
14323
15107
|
}
|
|
14324
|
-
const secPrompt =
|
|
15108
|
+
const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
|
|
14325
15109
|
this.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
14326
15110
|
try {
|
|
14327
15111
|
const secReview = await this.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
|
|
@@ -14382,7 +15166,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14382
15166
|
case "test": {
|
|
14383
15167
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
14384
15168
|
try {
|
|
14385
|
-
const { executeTests } = await import("./run-tests-
|
|
15169
|
+
const { executeTests } = await import("./run-tests-PACN4UYX.js");
|
|
14386
15170
|
const argStr = args.join(" ").trim();
|
|
14387
15171
|
let testArgs = {};
|
|
14388
15172
|
if (argStr) {
|
|
@@ -14399,17 +15183,17 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14399
15183
|
// ── /init ───────────────────────────────────────────────────────
|
|
14400
15184
|
case "init": {
|
|
14401
15185
|
const cwd = process.cwd();
|
|
14402
|
-
const targetPath =
|
|
15186
|
+
const targetPath = join17(cwd, "AICLI.md");
|
|
14403
15187
|
const force = args.includes("--force");
|
|
14404
|
-
if (
|
|
15188
|
+
if (existsSync23(targetPath) && !force) {
|
|
14405
15189
|
this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
14406
15190
|
Use /init --force to overwrite.` });
|
|
14407
15191
|
break;
|
|
14408
15192
|
}
|
|
14409
15193
|
this.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
|
|
14410
15194
|
try {
|
|
14411
|
-
const projectInfo =
|
|
14412
|
-
const prompt =
|
|
15195
|
+
const projectInfo = scanProject(cwd);
|
|
15196
|
+
const prompt = buildInitPrompt(projectInfo, cwd);
|
|
14413
15197
|
const content = await this.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
|
|
14414
15198
|
writeFileSync2(targetPath, content, "utf-8");
|
|
14415
15199
|
this.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
@@ -14434,19 +15218,19 @@ Use /context reload to load it.` });
|
|
|
14434
15218
|
lines.push("**Config Files:**");
|
|
14435
15219
|
lines.push(` Dir: ${configDir}`);
|
|
14436
15220
|
const checkFile = (label, filePath) => {
|
|
14437
|
-
const exists =
|
|
15221
|
+
const exists = existsSync23(filePath);
|
|
14438
15222
|
let extra = "";
|
|
14439
15223
|
if (exists) {
|
|
14440
15224
|
try {
|
|
14441
|
-
extra = ` (${
|
|
15225
|
+
extra = ` (${statSync9(filePath).size} bytes)`;
|
|
14442
15226
|
} catch {
|
|
14443
15227
|
}
|
|
14444
15228
|
}
|
|
14445
15229
|
lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
|
|
14446
15230
|
};
|
|
14447
|
-
checkFile("config.json",
|
|
14448
|
-
checkFile("memory.md",
|
|
14449
|
-
checkFile("dev-state.md",
|
|
15231
|
+
checkFile("config.json", join17(configDir, "config.json"));
|
|
15232
|
+
checkFile("memory.md", join17(configDir, MEMORY_FILE_NAME));
|
|
15233
|
+
checkFile("dev-state.md", join17(configDir, "dev-state.md"));
|
|
14450
15234
|
lines.push("");
|
|
14451
15235
|
if (this.mcpManager) {
|
|
14452
15236
|
lines.push("**MCP Servers:**");
|
|
@@ -14546,29 +15330,24 @@ ${this.config.toFormattedJSON()}
|
|
|
14546
15330
|
this.send({ type: "info", message: this.activeSystemPrompt ? `\u2713 Context reloaded (${this.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
|
|
14547
15331
|
break;
|
|
14548
15332
|
}
|
|
14549
|
-
const
|
|
14550
|
-
const cwd = process.cwd();
|
|
14551
|
-
const gitRoot = getGitRoot(cwd);
|
|
14552
|
-
const projectRoot = gitRoot ?? cwd;
|
|
15333
|
+
const result = this.contextLoadResult;
|
|
14553
15334
|
const layers = ["\u{1F4DA} **Context Layers:**", ""];
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
|
|
14558
|
-
|
|
14559
|
-
|
|
14560
|
-
|
|
14561
|
-
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
15335
|
+
if (result && result.layers.length > 0) {
|
|
15336
|
+
const labels = { global: "Global ", project: "Project", local: "Local " };
|
|
15337
|
+
for (const layer of result.layers) {
|
|
15338
|
+
layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
|
|
15339
|
+
}
|
|
15340
|
+
layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
|
|
15341
|
+
} else {
|
|
15342
|
+
layers.push(" No context files loaded.");
|
|
15343
|
+
layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
|
|
15344
|
+
}
|
|
15345
|
+
if (result && result.skipped.length > 0) {
|
|
15346
|
+
layers.push("");
|
|
15347
|
+
layers.push("Skipped:");
|
|
15348
|
+
for (const skipped of result.skipped.slice(0, 5)) {
|
|
15349
|
+
layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
|
|
14565
15350
|
}
|
|
14566
|
-
layers.push(` \u2013 ${label}: (none)`);
|
|
14567
|
-
};
|
|
14568
|
-
checkLayer("Global", configDir);
|
|
14569
|
-
checkLayer("Project", projectRoot);
|
|
14570
|
-
if (resolve5(cwd) !== resolve5(projectRoot)) {
|
|
14571
|
-
checkLayer("Subdir", cwd);
|
|
14572
15351
|
}
|
|
14573
15352
|
layers.push("");
|
|
14574
15353
|
layers.push(`Total prompt length: ${this.activeSystemPrompt?.length ?? 0} chars`);
|
|
@@ -14638,12 +15417,12 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
|
|
|
14638
15417
|
this.send({ type: "info", message: "\u2713 All added directories removed from context." });
|
|
14639
15418
|
break;
|
|
14640
15419
|
}
|
|
14641
|
-
const dirPath =
|
|
14642
|
-
if (!
|
|
15420
|
+
const dirPath = resolve7(sub);
|
|
15421
|
+
if (!existsSync23(dirPath)) {
|
|
14643
15422
|
this.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
14644
15423
|
break;
|
|
14645
15424
|
}
|
|
14646
|
-
if (!
|
|
15425
|
+
if (!statSync9(dirPath).isDirectory()) {
|
|
14647
15426
|
this.send({ type: "error", message: `Not a directory: ${dirPath}` });
|
|
14648
15427
|
break;
|
|
14649
15428
|
}
|
|
@@ -14655,14 +15434,14 @@ It will be included in AI context for subsequent messages.` });
|
|
|
14655
15434
|
// ── /commands ───────────────────────────────────────────────────
|
|
14656
15435
|
case "commands": {
|
|
14657
15436
|
const configDir = this.config.getConfigDir();
|
|
14658
|
-
const commandsDir =
|
|
14659
|
-
if (!
|
|
15437
|
+
const commandsDir = join17(configDir, CUSTOM_COMMANDS_DIR_NAME);
|
|
15438
|
+
if (!existsSync23(commandsDir)) {
|
|
14660
15439
|
this.send({ type: "info", message: `No custom commands directory.
|
|
14661
15440
|
Create: ${commandsDir}/ with .md files.` });
|
|
14662
15441
|
break;
|
|
14663
15442
|
}
|
|
14664
15443
|
try {
|
|
14665
|
-
const files =
|
|
15444
|
+
const files = readdirSync10(commandsDir).filter((f) => f.endsWith(".md"));
|
|
14666
15445
|
if (files.length === 0) {
|
|
14667
15446
|
this.send({ type: "info", message: `No custom commands found in ${commandsDir}
|
|
14668
15447
|
Add .md files to create commands.` });
|
|
@@ -14682,7 +15461,7 @@ Add .md files to create commands.` });
|
|
|
14682
15461
|
// ── /plugins ────────────────────────────────────────────────────
|
|
14683
15462
|
case "plugins": {
|
|
14684
15463
|
const configDir = this.config.getConfigDir();
|
|
14685
|
-
const pluginsDir =
|
|
15464
|
+
const pluginsDir = join17(configDir, PLUGINS_DIR_NAME);
|
|
14686
15465
|
const pluginTools = this.toolRegistry.listPluginTools();
|
|
14687
15466
|
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
14688
15467
|
if (pluginTools.length === 0) {
|
|
@@ -14856,11 +15635,11 @@ Add .md files to create commands.` });
|
|
|
14856
15635
|
}
|
|
14857
15636
|
memoryShow() {
|
|
14858
15637
|
const configDir = this.config.getConfigDir();
|
|
14859
|
-
const memPath =
|
|
15638
|
+
const memPath = join17(configDir, MEMORY_FILE_NAME);
|
|
14860
15639
|
let content = "";
|
|
14861
15640
|
try {
|
|
14862
|
-
if (
|
|
14863
|
-
content =
|
|
15641
|
+
if (existsSync23(memPath)) {
|
|
15642
|
+
content = readFileSync17(memPath, "utf-8");
|
|
14864
15643
|
}
|
|
14865
15644
|
} catch (err) {
|
|
14866
15645
|
process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
|
|
@@ -14874,11 +15653,11 @@ Add .md files to create commands.` });
|
|
|
14874
15653
|
}
|
|
14875
15654
|
memoryAdd(text) {
|
|
14876
15655
|
const configDir = this.config.getConfigDir();
|
|
14877
|
-
const memPath =
|
|
15656
|
+
const memPath = join17(configDir, MEMORY_FILE_NAME);
|
|
14878
15657
|
try {
|
|
14879
15658
|
mkdirSync10(configDir, { recursive: true });
|
|
14880
15659
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
14881
|
-
const previous =
|
|
15660
|
+
const previous = existsSync23(memPath) ? readFileSync17(memPath, "utf-8") : "";
|
|
14882
15661
|
const entry = `
|
|
14883
15662
|
- [${timestamp}] ${text}
|
|
14884
15663
|
`;
|
|
@@ -14890,7 +15669,7 @@ Add .md files to create commands.` });
|
|
|
14890
15669
|
}
|
|
14891
15670
|
memoryClear() {
|
|
14892
15671
|
const configDir = this.config.getConfigDir();
|
|
14893
|
-
const memPath =
|
|
15672
|
+
const memPath = join17(configDir, MEMORY_FILE_NAME);
|
|
14894
15673
|
try {
|
|
14895
15674
|
atomicWriteFileSync(memPath, "");
|
|
14896
15675
|
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
@@ -15055,7 +15834,7 @@ Add .md files to create commands.` });
|
|
|
15055
15834
|
dirContext += `
|
|
15056
15835
|
[Directory: ${dir}]
|
|
15057
15836
|
`;
|
|
15058
|
-
dirContext +=
|
|
15837
|
+
dirContext += scanDirTree(dir, 2, 40) + "\n";
|
|
15059
15838
|
totalLen = dirContext.length;
|
|
15060
15839
|
}
|
|
15061
15840
|
stable += dirContext;
|
|
@@ -15094,265 +15873,21 @@ Add .md files to create commands.` });
|
|
|
15094
15873
|
}
|
|
15095
15874
|
return { toolDefs: this.toolRegistry.getDefinitions(), mcpBudgetNote: null };
|
|
15096
15875
|
}
|
|
15097
|
-
/**
|
|
15098
|
-
* Find first matching context file in a directory.
|
|
15099
|
-
*/
|
|
15100
|
-
findContextFile(dir) {
|
|
15101
|
-
for (const name of CONTEXT_FILE_CANDIDATES) {
|
|
15102
|
-
const fullPath = join15(dir, name);
|
|
15103
|
-
try {
|
|
15104
|
-
if (existsSync21(fullPath)) {
|
|
15105
|
-
const content = readFileSync15(fullPath, "utf-8").trim();
|
|
15106
|
-
if (content) return content;
|
|
15107
|
-
}
|
|
15108
|
-
} catch {
|
|
15109
|
-
}
|
|
15110
|
-
}
|
|
15111
|
-
return null;
|
|
15112
|
-
}
|
|
15113
|
-
/**
|
|
15114
|
-
* Load hierarchical context files (same as CLI):
|
|
15115
|
-
* 1. Global: ~/.aicli/AICLI.md or CLAUDE.md
|
|
15116
|
-
* 2. Project: <git-root>/AICLI.md or CLAUDE.md
|
|
15117
|
-
* 3. Subdir: <cwd>/AICLI.md or CLAUDE.md (only if cwd ≠ project root)
|
|
15118
|
-
*/
|
|
15119
|
-
// ── /init helpers ─────────────────────────────────────────────────
|
|
15120
|
-
static SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
15121
|
-
"node_modules",
|
|
15122
|
-
".git",
|
|
15123
|
-
"dist",
|
|
15124
|
-
"build",
|
|
15125
|
-
"out",
|
|
15126
|
-
"target",
|
|
15127
|
-
".next",
|
|
15128
|
-
".nuxt",
|
|
15129
|
-
"__pycache__",
|
|
15130
|
-
".venv",
|
|
15131
|
-
"venv",
|
|
15132
|
-
".tox",
|
|
15133
|
-
".mypy_cache",
|
|
15134
|
-
".pytest_cache",
|
|
15135
|
-
".gradle",
|
|
15136
|
-
".idea",
|
|
15137
|
-
".vscode",
|
|
15138
|
-
".vs",
|
|
15139
|
-
"coverage",
|
|
15140
|
-
".cache",
|
|
15141
|
-
".parcel-cache",
|
|
15142
|
-
"dist-cjs",
|
|
15143
|
-
"release",
|
|
15144
|
-
".output",
|
|
15145
|
-
".turbo",
|
|
15146
|
-
"vendor"
|
|
15147
|
-
]);
|
|
15148
|
-
scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
15149
|
-
const lines = [];
|
|
15150
|
-
let count = 0;
|
|
15151
|
-
const walk = (d, prefix, depth) => {
|
|
15152
|
-
if (depth > maxDepth || count >= maxEntries) return;
|
|
15153
|
-
let entries;
|
|
15154
|
-
try {
|
|
15155
|
-
entries = readdirSync9(d);
|
|
15156
|
-
} catch {
|
|
15157
|
-
return;
|
|
15158
|
-
}
|
|
15159
|
-
const filtered = entries.filter((e) => !e.startsWith(".") && !_SessionHandler.SCAN_SKIP_DIRS.has(e));
|
|
15160
|
-
const sorted = filtered.sort((a, b) => {
|
|
15161
|
-
let aIsDir = false, bIsDir = false;
|
|
15162
|
-
try {
|
|
15163
|
-
aIsDir = statSync8(join15(d, a)).isDirectory();
|
|
15164
|
-
} catch {
|
|
15165
|
-
}
|
|
15166
|
-
try {
|
|
15167
|
-
bIsDir = statSync8(join15(d, b)).isDirectory();
|
|
15168
|
-
} catch {
|
|
15169
|
-
}
|
|
15170
|
-
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
15171
|
-
return a.localeCompare(b);
|
|
15172
|
-
});
|
|
15173
|
-
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
15174
|
-
const name = sorted[i];
|
|
15175
|
-
const fullPath = join15(d, name);
|
|
15176
|
-
const isLast = i === sorted.length - 1;
|
|
15177
|
-
let isDir;
|
|
15178
|
-
try {
|
|
15179
|
-
isDir = statSync8(fullPath).isDirectory();
|
|
15180
|
-
} catch {
|
|
15181
|
-
continue;
|
|
15182
|
-
}
|
|
15183
|
-
lines.push(prefix + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + name + (isDir ? "/" : ""));
|
|
15184
|
-
count++;
|
|
15185
|
-
if (isDir) walk(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
|
|
15186
|
-
}
|
|
15187
|
-
};
|
|
15188
|
-
walk(dir, "", 0);
|
|
15189
|
-
if (count >= maxEntries) lines.push("... (truncated)");
|
|
15190
|
-
return lines.join("\n");
|
|
15191
|
-
}
|
|
15192
|
-
scanProject(cwd) {
|
|
15193
|
-
const info = { type: "unknown", language: "unknown", configFiles: [], directoryStructure: "" };
|
|
15194
|
-
const check = (file) => existsSync21(join15(cwd, file));
|
|
15195
|
-
const configCandidates = [
|
|
15196
|
-
"package.json",
|
|
15197
|
-
"tsconfig.json",
|
|
15198
|
-
"Cargo.toml",
|
|
15199
|
-
"pyproject.toml",
|
|
15200
|
-
"setup.py",
|
|
15201
|
-
"requirements.txt",
|
|
15202
|
-
"go.mod",
|
|
15203
|
-
"pom.xml",
|
|
15204
|
-
"build.gradle",
|
|
15205
|
-
"build.gradle.kts",
|
|
15206
|
-
"CMakeLists.txt",
|
|
15207
|
-
"Makefile",
|
|
15208
|
-
".csproj",
|
|
15209
|
-
".sln",
|
|
15210
|
-
"composer.json",
|
|
15211
|
-
"Gemfile",
|
|
15212
|
-
"mix.exs",
|
|
15213
|
-
"deno.json",
|
|
15214
|
-
"bun.lockb"
|
|
15215
|
-
];
|
|
15216
|
-
info.configFiles = configCandidates.filter(check);
|
|
15217
|
-
if (check("package.json")) {
|
|
15218
|
-
info.type = "node";
|
|
15219
|
-
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
15220
|
-
try {
|
|
15221
|
-
const pkg = JSON.parse(readFileSync15(join15(cwd, "package.json"), "utf-8"));
|
|
15222
|
-
const scripts = pkg.scripts ?? {};
|
|
15223
|
-
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
15224
|
-
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
15225
|
-
info.devCommand = scripts.dev ? "npm run dev" : void 0;
|
|
15226
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
15227
|
-
if (allDeps["react"]) info.framework = "React";
|
|
15228
|
-
else if (allDeps["vue"]) info.framework = "Vue";
|
|
15229
|
-
else if (allDeps["@angular/core"]) info.framework = "Angular";
|
|
15230
|
-
else if (allDeps["next"]) info.framework = "Next.js";
|
|
15231
|
-
else if (allDeps["express"]) info.framework = "Express";
|
|
15232
|
-
} catch {
|
|
15233
|
-
}
|
|
15234
|
-
} else if (check("Cargo.toml")) {
|
|
15235
|
-
info.type = "rust";
|
|
15236
|
-
info.language = "Rust";
|
|
15237
|
-
info.buildCommand = "cargo build";
|
|
15238
|
-
info.testCommand = "cargo test";
|
|
15239
|
-
} else if (check("pyproject.toml") || check("setup.py") || check("requirements.txt")) {
|
|
15240
|
-
info.type = "python";
|
|
15241
|
-
info.language = "Python";
|
|
15242
|
-
info.testCommand = "pytest";
|
|
15243
|
-
} else if (check("go.mod")) {
|
|
15244
|
-
info.type = "go";
|
|
15245
|
-
info.language = "Go";
|
|
15246
|
-
info.buildCommand = "go build ./...";
|
|
15247
|
-
info.testCommand = "go test ./...";
|
|
15248
|
-
} else if (check("pom.xml")) {
|
|
15249
|
-
info.type = "java";
|
|
15250
|
-
info.language = "Java";
|
|
15251
|
-
info.buildCommand = "mvn package";
|
|
15252
|
-
info.testCommand = "mvn test";
|
|
15253
|
-
} else if (check("build.gradle") || check("build.gradle.kts")) {
|
|
15254
|
-
info.type = "java";
|
|
15255
|
-
info.language = "Java/Kotlin";
|
|
15256
|
-
info.buildCommand = "./gradlew build";
|
|
15257
|
-
info.testCommand = "./gradlew test";
|
|
15258
|
-
}
|
|
15259
|
-
info.directoryStructure = this.scanDirTree(cwd);
|
|
15260
|
-
return info;
|
|
15261
|
-
}
|
|
15262
|
-
buildInitPrompt(info, cwd) {
|
|
15263
|
-
const parts = [
|
|
15264
|
-
"Please generate an AICLI.md context file (Markdown format) for the following project.",
|
|
15265
|
-
"\n## Project Info\n",
|
|
15266
|
-
`- Working directory: ${cwd}`,
|
|
15267
|
-
`- Type: ${info.type}`,
|
|
15268
|
-
`- Language: ${info.language}`
|
|
15269
|
-
];
|
|
15270
|
-
if (info.framework) parts.push(`- Framework: ${info.framework}`);
|
|
15271
|
-
if (info.buildCommand) parts.push(`- Build command: ${info.buildCommand}`);
|
|
15272
|
-
if (info.testCommand) parts.push(`- Test command: ${info.testCommand}`);
|
|
15273
|
-
if (info.devCommand) parts.push(`- Dev command: ${info.devCommand}`);
|
|
15274
|
-
parts.push(`
|
|
15275
|
-
## Detected Config Files
|
|
15276
|
-
${info.configFiles.map((f) => `- ${f}`).join("\n")}`);
|
|
15277
|
-
parts.push(`
|
|
15278
|
-
## Directory Structure
|
|
15279
|
-
\`\`\`
|
|
15280
|
-
${info.directoryStructure}
|
|
15281
|
-
\`\`\``);
|
|
15282
|
-
parts.push(`
|
|
15283
|
-
## Requirements
|
|
15284
|
-
Generate a structured Markdown file with: project overview, tech stack, project structure, common commands, code style. Keep it concise, within 200 lines.`);
|
|
15285
|
-
return parts.join("\n");
|
|
15286
|
-
}
|
|
15287
|
-
// ── /review helper ──────────────────────────────────────────────────
|
|
15288
|
-
buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
15289
|
-
const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
|
|
15290
|
-
return `# Code Review Request
|
|
15291
|
-
|
|
15292
|
-
${level}
|
|
15293
|
-
|
|
15294
|
-
## Git Status
|
|
15295
|
-
${gitContextStr}
|
|
15296
|
-
|
|
15297
|
-
## Code Changes (diff)
|
|
15298
|
-
\`\`\`diff
|
|
15299
|
-
${diff}
|
|
15300
|
-
\`\`\`
|
|
15301
|
-
|
|
15302
|
-
## Output Format
|
|
15303
|
-
1. **Overall Assessment**: One-sentence summary
|
|
15304
|
-
2. **Issues** (if any): [Severity] file:line \u2014 description + fix
|
|
15305
|
-
3. **Improvement Suggestions** (if any)
|
|
15306
|
-
4. **Highlights** (if any)
|
|
15307
|
-
|
|
15308
|
-
Severity: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
15309
|
-
}
|
|
15310
|
-
buildSecurityReviewPrompt(diff, gitContextStr) {
|
|
15311
|
-
return `# Security Vulnerability Review
|
|
15312
|
-
|
|
15313
|
-
Analyze the following code changes **exclusively for security vulnerabilities**.
|
|
15314
|
-
|
|
15315
|
-
## Categories
|
|
15316
|
-
1. Injection (SQL, command, path traversal, XSS)
|
|
15317
|
-
2. Auth & Authorization (hardcoded creds, missing checks)
|
|
15318
|
-
3. Secrets & Sensitive Data (API keys, tokens in code)
|
|
15319
|
-
4. Input Validation (missing validation, unsafe deserialization)
|
|
15320
|
-
5. Cryptography (weak algorithms, hardcoded IVs)
|
|
15321
|
-
6. File System (path traversal, symlink attacks)
|
|
15322
|
-
7. Network (SSRF, insecure protocols)
|
|
15323
|
-
|
|
15324
|
-
## Git Status
|
|
15325
|
-
${gitContextStr}
|
|
15326
|
-
|
|
15327
|
-
## Code Changes
|
|
15328
|
-
\`\`\`diff
|
|
15329
|
-
${diff}
|
|
15330
|
-
\`\`\`
|
|
15331
|
-
|
|
15332
|
-
## Output Format
|
|
15333
|
-
For each finding:
|
|
15334
|
-
- **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW
|
|
15335
|
-
- **Category** + **File:line**
|
|
15336
|
-
- **Description** + exploitation scenario
|
|
15337
|
-
- **Recommended fix**
|
|
15338
|
-
|
|
15339
|
-
If none found: "\u2705 No security vulnerabilities detected"`;
|
|
15340
|
-
}
|
|
15341
15876
|
loadContextFiles() {
|
|
15342
|
-
const
|
|
15877
|
+
const contextConfig = this.config.get("context");
|
|
15343
15878
|
const cwd = process.cwd();
|
|
15344
|
-
const configDir = this.config.getConfigDir();
|
|
15345
|
-
const globalCtx = this.findContextFile(configDir);
|
|
15346
|
-
if (globalCtx) parts.push(globalCtx);
|
|
15347
15879
|
const gitRoot = getGitRoot(cwd);
|
|
15348
15880
|
const projectRoot = gitRoot ?? cwd;
|
|
15349
|
-
const
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15881
|
+
const result = loadContextFiles({
|
|
15882
|
+
cwd,
|
|
15883
|
+
configDir: this.config.getConfigDir(),
|
|
15884
|
+
projectRoot,
|
|
15885
|
+
setting: this.config.get("contextFile"),
|
|
15886
|
+
fallbackFilenames: contextConfig.projectDocFallbackFilenames,
|
|
15887
|
+
maxBytes: contextConfig.projectDocMaxBytes
|
|
15888
|
+
});
|
|
15889
|
+
this.contextLoadResult = result;
|
|
15890
|
+
return result.mergedContent || void 0;
|
|
15356
15891
|
}
|
|
15357
15892
|
};
|
|
15358
15893
|
|
|
@@ -15368,8 +15903,8 @@ async function setupProxy(configProxy) {
|
|
|
15368
15903
|
}
|
|
15369
15904
|
|
|
15370
15905
|
// src/web/auth.ts
|
|
15371
|
-
import { existsSync as
|
|
15372
|
-
import { join as
|
|
15906
|
+
import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync3, mkdirSync as mkdirSync11, readdirSync as readdirSync11, copyFileSync } from "fs";
|
|
15907
|
+
import { join as join18 } from "path";
|
|
15373
15908
|
import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
|
|
15374
15909
|
var USERS_FILE = "users.json";
|
|
15375
15910
|
var TOKEN_EXPIRY_HOURS = 24;
|
|
@@ -15384,7 +15919,7 @@ var AuthManager = class {
|
|
|
15384
15919
|
db;
|
|
15385
15920
|
constructor(baseDir) {
|
|
15386
15921
|
this.baseDir = baseDir;
|
|
15387
|
-
this.usersFile =
|
|
15922
|
+
this.usersFile = join18(baseDir, USERS_FILE);
|
|
15388
15923
|
this.db = this.loadOrCreate();
|
|
15389
15924
|
}
|
|
15390
15925
|
// ── Public API ─────────────────────────────────────────────────
|
|
@@ -15413,9 +15948,9 @@ var AuthManager = class {
|
|
|
15413
15948
|
}
|
|
15414
15949
|
const salt = randomBytes(16).toString("hex");
|
|
15415
15950
|
const passwordHash = this.hashPassword(password, salt);
|
|
15416
|
-
const dataDir =
|
|
15417
|
-
const fullDataDir =
|
|
15418
|
-
mkdirSync11(
|
|
15951
|
+
const dataDir = join18(USERS_DIR, username);
|
|
15952
|
+
const fullDataDir = join18(this.baseDir, dataDir);
|
|
15953
|
+
mkdirSync11(join18(fullDataDir, "history"), { recursive: true });
|
|
15419
15954
|
const user = {
|
|
15420
15955
|
username,
|
|
15421
15956
|
passwordHash,
|
|
@@ -15530,7 +16065,7 @@ var AuthManager = class {
|
|
|
15530
16065
|
getUserDataDir(username) {
|
|
15531
16066
|
const user = this.db.users.find((u) => u.username === username);
|
|
15532
16067
|
if (!user) throw new Error(`User not found: ${username}`);
|
|
15533
|
-
return
|
|
16068
|
+
return join18(this.baseDir, user.dataDir);
|
|
15534
16069
|
}
|
|
15535
16070
|
/** List all usernames */
|
|
15536
16071
|
listUsers() {
|
|
@@ -15566,30 +16101,30 @@ var AuthManager = class {
|
|
|
15566
16101
|
const err = this.register(username, password);
|
|
15567
16102
|
if (err) return err;
|
|
15568
16103
|
const userDir = this.getUserDataDir(username);
|
|
15569
|
-
const globalConfig =
|
|
15570
|
-
if (
|
|
16104
|
+
const globalConfig = join18(this.baseDir, "config.json");
|
|
16105
|
+
if (existsSync24(globalConfig)) {
|
|
15571
16106
|
try {
|
|
15572
|
-
const content =
|
|
15573
|
-
writeFileSync3(
|
|
16107
|
+
const content = readFileSync18(globalConfig, "utf-8");
|
|
16108
|
+
writeFileSync3(join18(userDir, "config.json"), content, "utf-8");
|
|
15574
16109
|
} catch {
|
|
15575
16110
|
}
|
|
15576
16111
|
}
|
|
15577
|
-
const globalMemory =
|
|
15578
|
-
if (
|
|
16112
|
+
const globalMemory = join18(this.baseDir, "memory.md");
|
|
16113
|
+
if (existsSync24(globalMemory)) {
|
|
15579
16114
|
try {
|
|
15580
|
-
const content =
|
|
15581
|
-
writeFileSync3(
|
|
16115
|
+
const content = readFileSync18(globalMemory, "utf-8");
|
|
16116
|
+
writeFileSync3(join18(userDir, "memory.md"), content, "utf-8");
|
|
15582
16117
|
} catch {
|
|
15583
16118
|
}
|
|
15584
16119
|
}
|
|
15585
|
-
const globalHistory =
|
|
15586
|
-
if (
|
|
16120
|
+
const globalHistory = join18(this.baseDir, "history");
|
|
16121
|
+
if (existsSync24(globalHistory)) {
|
|
15587
16122
|
try {
|
|
15588
|
-
const files =
|
|
15589
|
-
const userHistory =
|
|
16123
|
+
const files = readdirSync11(globalHistory).filter((f) => f.endsWith(".json"));
|
|
16124
|
+
const userHistory = join18(userDir, "history");
|
|
15590
16125
|
for (const f of files) {
|
|
15591
16126
|
try {
|
|
15592
|
-
copyFileSync(
|
|
16127
|
+
copyFileSync(join18(globalHistory, f), join18(userHistory, f));
|
|
15593
16128
|
} catch {
|
|
15594
16129
|
}
|
|
15595
16130
|
}
|
|
@@ -15600,9 +16135,9 @@ var AuthManager = class {
|
|
|
15600
16135
|
}
|
|
15601
16136
|
// ── Private methods ────────────────────────────────────────────
|
|
15602
16137
|
loadOrCreate() {
|
|
15603
|
-
if (
|
|
16138
|
+
if (existsSync24(this.usersFile)) {
|
|
15604
16139
|
try {
|
|
15605
|
-
return JSON.parse(
|
|
16140
|
+
return JSON.parse(readFileSync18(this.usersFile, "utf-8"));
|
|
15606
16141
|
} catch {
|
|
15607
16142
|
}
|
|
15608
16143
|
}
|
|
@@ -15654,7 +16189,7 @@ function getModuleDir() {
|
|
|
15654
16189
|
}
|
|
15655
16190
|
} catch {
|
|
15656
16191
|
}
|
|
15657
|
-
return
|
|
16192
|
+
return resolve8(".");
|
|
15658
16193
|
}
|
|
15659
16194
|
async function startWebServer(options = {}) {
|
|
15660
16195
|
const port = options.port ?? 3e3;
|
|
@@ -15719,8 +16254,8 @@ async function startWebServer(options = {}) {
|
|
|
15719
16254
|
}
|
|
15720
16255
|
}
|
|
15721
16256
|
let skillManager = null;
|
|
15722
|
-
const skillsDir =
|
|
15723
|
-
if (
|
|
16257
|
+
const skillsDir = join19(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
16258
|
+
if (existsSync25(skillsDir)) {
|
|
15724
16259
|
skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
|
|
15725
16260
|
skillManager.loadSkills();
|
|
15726
16261
|
const count = skillManager.listSkills().length;
|
|
@@ -15791,18 +16326,18 @@ async function startWebServer(options = {}) {
|
|
|
15791
16326
|
next();
|
|
15792
16327
|
};
|
|
15793
16328
|
const moduleDir = getModuleDir();
|
|
15794
|
-
let clientDir =
|
|
15795
|
-
if (!
|
|
15796
|
-
clientDir =
|
|
16329
|
+
let clientDir = join19(moduleDir, "web", "client");
|
|
16330
|
+
if (!existsSync25(clientDir)) {
|
|
16331
|
+
clientDir = join19(moduleDir, "client");
|
|
15797
16332
|
}
|
|
15798
|
-
if (!
|
|
15799
|
-
clientDir =
|
|
16333
|
+
if (!existsSync25(clientDir)) {
|
|
16334
|
+
clientDir = join19(moduleDir, "..", "..", "src", "web", "client");
|
|
15800
16335
|
}
|
|
15801
|
-
if (!
|
|
15802
|
-
clientDir =
|
|
16336
|
+
if (!existsSync25(clientDir)) {
|
|
16337
|
+
clientDir = join19(process.cwd(), "src", "web", "client");
|
|
15803
16338
|
}
|
|
15804
16339
|
console.log(` Static files: ${clientDir}`);
|
|
15805
|
-
app.use("/vendor", express.static(
|
|
16340
|
+
app.use("/vendor", express.static(join19(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
|
|
15806
16341
|
app.use(express.static(clientDir));
|
|
15807
16342
|
app.get("/api/status", (_req, res) => {
|
|
15808
16343
|
res.json({
|
|
@@ -15869,10 +16404,10 @@ async function startWebServer(options = {}) {
|
|
|
15869
16404
|
app.get("/api/files", requireAuth, (req, res) => {
|
|
15870
16405
|
const cwd = process.cwd();
|
|
15871
16406
|
const prefix = req.query.prefix || "";
|
|
15872
|
-
const targetDir =
|
|
16407
|
+
const targetDir = join19(cwd, prefix);
|
|
15873
16408
|
try {
|
|
15874
|
-
const canonicalTarget = realpathSync(
|
|
15875
|
-
const canonicalCwd = realpathSync(
|
|
16409
|
+
const canonicalTarget = realpathSync(resolve8(targetDir));
|
|
16410
|
+
const canonicalCwd = realpathSync(resolve8(cwd));
|
|
15876
16411
|
if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
|
|
15877
16412
|
res.json({ files: [] });
|
|
15878
16413
|
return;
|
|
@@ -15883,10 +16418,10 @@ async function startWebServer(options = {}) {
|
|
|
15883
16418
|
}
|
|
15884
16419
|
try {
|
|
15885
16420
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
15886
|
-
const entries =
|
|
16421
|
+
const entries = readdirSync12(targetDir, { withFileTypes: true });
|
|
15887
16422
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
15888
16423
|
name: e.name,
|
|
15889
|
-
path:
|
|
16424
|
+
path: relative4(cwd, join19(targetDir, e.name)).replace(/\\/g, "/"),
|
|
15890
16425
|
isDir: e.isDirectory()
|
|
15891
16426
|
}));
|
|
15892
16427
|
res.json({ files });
|
|
@@ -15922,8 +16457,8 @@ async function startWebServer(options = {}) {
|
|
|
15922
16457
|
try {
|
|
15923
16458
|
const authUser = req._authUser;
|
|
15924
16459
|
const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
|
|
15925
|
-
const filePath =
|
|
15926
|
-
if (!
|
|
16460
|
+
const filePath = join19(histDir, `${id}.json`);
|
|
16461
|
+
if (!existsSync25(filePath)) {
|
|
15927
16462
|
res.status(404).json({ error: "Session not found" });
|
|
15928
16463
|
return;
|
|
15929
16464
|
}
|
|
@@ -15938,7 +16473,7 @@ async function startWebServer(options = {}) {
|
|
|
15938
16473
|
res.status(404).json({ error: "Session not found" });
|
|
15939
16474
|
return;
|
|
15940
16475
|
}
|
|
15941
|
-
const data = JSON.parse(
|
|
16476
|
+
const data = JSON.parse(readFileSync19(filePath, "utf-8"));
|
|
15942
16477
|
res.json({ session: data });
|
|
15943
16478
|
} catch (err) {
|
|
15944
16479
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
@@ -15951,10 +16486,10 @@ async function startWebServer(options = {}) {
|
|
|
15951
16486
|
return;
|
|
15952
16487
|
}
|
|
15953
16488
|
const cwd = process.cwd();
|
|
15954
|
-
const fullPath =
|
|
16489
|
+
const fullPath = resolve8(join19(cwd, filePath));
|
|
15955
16490
|
try {
|
|
15956
16491
|
const canonicalFull = realpathSync(fullPath);
|
|
15957
|
-
const canonicalCwd = realpathSync(
|
|
16492
|
+
const canonicalCwd = realpathSync(resolve8(cwd));
|
|
15958
16493
|
if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
|
|
15959
16494
|
res.json({ error: "Access denied" });
|
|
15960
16495
|
return;
|
|
@@ -15964,12 +16499,12 @@ async function startWebServer(options = {}) {
|
|
|
15964
16499
|
return;
|
|
15965
16500
|
}
|
|
15966
16501
|
try {
|
|
15967
|
-
const stat =
|
|
16502
|
+
const stat = statSync10(fullPath);
|
|
15968
16503
|
if (stat.size > 512 * 1024) {
|
|
15969
16504
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
15970
16505
|
return;
|
|
15971
16506
|
}
|
|
15972
|
-
const content =
|
|
16507
|
+
const content = readFileSync19(fullPath, "utf-8");
|
|
15973
16508
|
res.json({ content, size: stat.size });
|
|
15974
16509
|
} catch {
|
|
15975
16510
|
res.json({ error: "Cannot read file" });
|
|
@@ -16173,7 +16708,7 @@ async function startWebServer(options = {}) {
|
|
|
16173
16708
|
});
|
|
16174
16709
|
const MAX_PORT_ATTEMPTS = 10;
|
|
16175
16710
|
let actualPort = port;
|
|
16176
|
-
const result = await new Promise((
|
|
16711
|
+
const result = await new Promise((resolve9, reject) => {
|
|
16177
16712
|
const tryListen = (attempt) => {
|
|
16178
16713
|
server.once("error", (err) => {
|
|
16179
16714
|
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
@@ -16204,7 +16739,7 @@ async function startWebServer(options = {}) {
|
|
|
16204
16739
|
}
|
|
16205
16740
|
console.log(` Press Ctrl+C to stop
|
|
16206
16741
|
`);
|
|
16207
|
-
|
|
16742
|
+
resolve9({ port: actualPort, host, url });
|
|
16208
16743
|
});
|
|
16209
16744
|
};
|
|
16210
16745
|
tryListen(1);
|
|
@@ -16224,17 +16759,17 @@ function resolveProjectMcpPath() {
|
|
|
16224
16759
|
const cwd = process.cwd();
|
|
16225
16760
|
const gitRoot = getGitRoot(cwd);
|
|
16226
16761
|
const projectRoot = gitRoot ?? cwd;
|
|
16227
|
-
const configPath =
|
|
16228
|
-
return
|
|
16762
|
+
const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
16763
|
+
return existsSync25(configPath) ? configPath : null;
|
|
16229
16764
|
}
|
|
16230
16765
|
function loadProjectMcpConfig() {
|
|
16231
16766
|
const cwd = process.cwd();
|
|
16232
16767
|
const gitRoot = getGitRoot(cwd);
|
|
16233
16768
|
const projectRoot = gitRoot ?? cwd;
|
|
16234
|
-
const configPath =
|
|
16235
|
-
if (!
|
|
16769
|
+
const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
16770
|
+
if (!existsSync25(configPath)) return null;
|
|
16236
16771
|
try {
|
|
16237
|
-
const raw = JSON.parse(
|
|
16772
|
+
const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
|
|
16238
16773
|
return raw.mcpServers ?? raw;
|
|
16239
16774
|
} catch {
|
|
16240
16775
|
return null;
|