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.
@@ -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-EYID2AIR.js";
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 join17, dirname as dirname6, resolve as resolve6, relative as relative3, sep as sep3 } from "path";
63
- import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync11, statSync as statSync9, realpathSync } from "fs";
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 → CLAUDE.md → .aicli/context.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
- if (this.permissionRules.length > 0) {
6311
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
6312
- if (action === "deny") {
6313
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
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
- if (action === "auto-approve") {
6316
- this.printToolCall(call);
6317
- try {
6318
- const rawContent = await runTool(tool, call.arguments, call.name);
6319
- const content = truncateOutput(rawContent, call.name);
6320
- const wasTruncated = content !== rawContent;
6321
- this.printToolResult(call.name, rawContent, false, wasTruncated);
6322
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6323
- return { callId: call.id, content, isError: false };
6324
- } catch (err) {
6325
- const message = err instanceof Error ? err.message : String(err);
6326
- this.printToolResult(call.name, message, true, false);
6327
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6328
- return { callId: call.id, content: message, isError: true };
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((resolve7) => {
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
- resolve7(result);
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((resolve7) => {
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
- resolve7(answer === "y");
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 resolve3, sep as sep2, basename as basename2 } from "path";
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 = resolve3(p);
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, displayPath, regex, contextLines, maxResults) {
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: displayPath.replace(/\\/g, "/"),
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, displayPath, regex, contextLines, maxResults, results) {
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: displayPath,
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((resolve7) => {
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
- resolve7(`${prefixWarnings}[Timeout after ${timeout}ms]
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
- resolve7(`${prefixWarnings}Exit code ${code}:
8287
+ resolve9(`${prefixWarnings}Exit code ${code}:
7976
8288
  ${output}`);
7977
8289
  } else {
7978
- resolve7(`${prefixWarnings}${output || "(no output)"}`);
8290
+ resolve9(`${prefixWarnings}${output || "(no output)"}`);
7979
8291
  }
7980
8292
  });
7981
8293
  child.on("error", (err) => {
7982
8294
  clearTimeout(timer);
7983
- resolve7(
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((resolve7) => {
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
- resolve7(answer);
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 resolve4, extname as extname2 } from "path";
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 = resolve4(filePath);
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((resolve7, reject) => {
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
- resolve7(result);
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((resolve7, reject) => {
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
- resolve7(val);
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 resolve7 = this.pendingConfirms.get(requestId);
11180
- if (resolve7) {
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
- resolve7(approved);
11507
+ resolve9(approved);
11185
11508
  }
11186
11509
  }
11187
11510
  /** Resolve a pending batch confirm from client response */
11188
11511
  resolveBatchConfirm(requestId, decision) {
11189
- const resolve7 = this.pendingBatchConfirms.get(requestId);
11190
- if (resolve7) {
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
- resolve7(decision);
11518
+ resolve9(decision);
11196
11519
  } else {
11197
- resolve7(new Set(decision));
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 resolve7 of this.pendingConfirms.values()) resolve7(false);
11526
+ for (const resolve9 of this.pendingConfirms.values()) resolve9(false);
11204
11527
  this.pendingConfirms.clear();
11205
- for (const resolve7 of this.pendingBatchConfirms.values()) resolve7("none");
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((resolve7) => {
11282
- this.pendingConfirms.set(requestId, resolve7);
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((resolve7) => {
11307
- this.pendingBatchConfirms.set(requestId, resolve7);
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
- if (this.permissionRules.length > 0) {
11330
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
11331
- if (action === "deny") {
11332
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
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
- if (action === "auto-approve") {
11335
- this.sendToolCallStart(call);
11336
- try {
11337
- const rawContent = await runTool(tool, call.arguments, call.name);
11338
- const content = truncateOutput(rawContent, call.name);
11339
- this.sendToolCallResult(call, rawContent, false);
11340
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11341
- return { callId: call.id, content, isError: false };
11342
- } catch (err) {
11343
- const message = err instanceof Error ? err.message : String(err);
11344
- this.sendToolCallResult(call, message, true);
11345
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
11346
- return { callId: call.id, content: message, isError: true };
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 existsSync21, readFileSync as readFileSync15, writeFileSync as writeFileSync2, mkdirSync as mkdirSync10, readdirSync as readdirSync9, statSync as statSync8, createWriteStream } from "fs";
11734
- import { join as join15, resolve as resolve5, dirname as dirname5 } from "path";
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 existsSync20 } from "fs";
11739
- import { join as join13 } from "path";
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 (!existsSync20(join13(cwd, ".git"))) {
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 join14 } from "path";
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: join14(config.getHistoryDir(), `${session.id}.json`) };
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 _SessionHandler {
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.toolExecutor.setConfig({ hookConfig: hooks, permissionRules, defaultPermission });
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 resolve7 = this.pendingAskUser.get(msg.requestId);
12609
- if (resolve7) {
13392
+ const resolve9 = this.pendingAskUser.get(msg.requestId);
13393
+ if (resolve9) {
12610
13394
  this.pendingAskUser.delete(msg.requestId);
12611
- resolve7(msg.answer);
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 resolve7 = this.pendingAutoPause.get(msg.requestId);
12623
- if (resolve7) {
13406
+ const resolve9 = this.pendingAutoPause.get(msg.requestId);
13407
+ if (resolve9) {
12624
13408
  this.pendingAutoPause.delete(msg.requestId);
12625
- resolve7({ action: msg.action, message: msg.message });
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 resolve7 = this.pendingHubReview.get(msg.requestId);
12641
- if (resolve7) {
13424
+ const resolve9 = this.pendingHubReview.get(msg.requestId);
13425
+ if (resolve9) {
12642
13426
  this.pendingHubReview.delete(msg.requestId);
12643
- resolve7({ action: msg.action, message: msg.message });
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 resolve7 of this.pendingAskUser.values()) resolve7(null);
13439
+ for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
12656
13440
  this.pendingAskUser.clear();
12657
- for (const resolve7 of this.pendingAutoPause.values()) resolve7({ action: "stop" });
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((resolve7) => {
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, resolve7);
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((resolve7) => {
13131
- this.pendingAutoPause.set(requestId, resolve7);
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((resolve7, reject) => {
13311
- fileStream.end((err) => err ? reject(err) : resolve7());
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((resolve7) => fileStream.end(() => resolve7()));
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] \u2014 Show/reload context layers",
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 = existsSync21(filePath) ? (() => {
14651
+ const currentContent = existsSync23(filePath) ? (() => {
13868
14652
  try {
13869
- return readFileSync15(filePath, "utf-8");
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 resolve7 = (ref) => {
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 = resolve7(ref);
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 = resolve7(ref);
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 = resolve7(ref);
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 = resolve7(ref);
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 = resolve7(ref);
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 = this.buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
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 = this.buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
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-UAS5NJBM.js");
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 = join15(cwd, "AICLI.md");
15186
+ const targetPath = join17(cwd, "AICLI.md");
14403
15187
  const force = args.includes("--force");
14404
- if (existsSync21(targetPath) && !force) {
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 = this.scanProject(cwd);
14412
- const prompt = this.buildInitPrompt(projectInfo, cwd);
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 = existsSync21(filePath);
15221
+ const exists = existsSync23(filePath);
14438
15222
  let extra = "";
14439
15223
  if (exists) {
14440
15224
  try {
14441
- extra = ` (${statSync8(filePath).size} bytes)`;
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", join15(configDir, "config.json"));
14448
- checkFile("memory.md", join15(configDir, MEMORY_FILE_NAME));
14449
- checkFile("dev-state.md", join15(configDir, "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 configDir = this.config.getConfigDir();
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
- const checkLayer = (label, dir) => {
14555
- for (const name2 of CONTEXT_FILE_CANDIDATES) {
14556
- const fullPath = join15(dir, name2);
14557
- if (existsSync21(fullPath)) {
14558
- try {
14559
- const size = statSync8(fullPath).size;
14560
- layers.push(` \u2713 ${label}: ${fullPath} (${size} bytes)`);
14561
- return;
14562
- } catch {
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 = resolve5(sub);
14642
- if (!existsSync21(dirPath)) {
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 (!statSync8(dirPath).isDirectory()) {
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 = join15(configDir, CUSTOM_COMMANDS_DIR_NAME);
14659
- if (!existsSync21(commandsDir)) {
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 = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
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 = join15(configDir, PLUGINS_DIR_NAME);
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 = join15(configDir, MEMORY_FILE_NAME);
15638
+ const memPath = join17(configDir, MEMORY_FILE_NAME);
14860
15639
  let content = "";
14861
15640
  try {
14862
- if (existsSync21(memPath)) {
14863
- content = readFileSync15(memPath, "utf-8");
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 = join15(configDir, MEMORY_FILE_NAME);
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 = existsSync21(memPath) ? readFileSync15(memPath, "utf-8") : "";
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 = join15(configDir, MEMORY_FILE_NAME);
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 += this.scanDirTree(dir, 2, 40) + "\n";
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 parts = [];
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 projectCtx = this.findContextFile(projectRoot);
15350
- if (projectCtx) parts.push(projectCtx);
15351
- if (resolve5(cwd) !== resolve5(projectRoot)) {
15352
- const localCtx = this.findContextFile(cwd);
15353
- if (localCtx) parts.push(localCtx);
15354
- }
15355
- return parts.length > 0 ? parts.join("\n\n---\n\n") : void 0;
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 existsSync22, readFileSync as readFileSync16, writeFileSync as writeFileSync3, mkdirSync as mkdirSync11, readdirSync as readdirSync10, copyFileSync } from "fs";
15372
- import { join as join16 } from "path";
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 = join16(baseDir, USERS_FILE);
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 = join16(USERS_DIR, username);
15417
- const fullDataDir = join16(this.baseDir, dataDir);
15418
- mkdirSync11(join16(fullDataDir, "history"), { recursive: true });
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 join16(this.baseDir, user.dataDir);
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 = join16(this.baseDir, "config.json");
15570
- if (existsSync22(globalConfig)) {
16104
+ const globalConfig = join18(this.baseDir, "config.json");
16105
+ if (existsSync24(globalConfig)) {
15571
16106
  try {
15572
- const content = readFileSync16(globalConfig, "utf-8");
15573
- writeFileSync3(join16(userDir, "config.json"), content, "utf-8");
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 = join16(this.baseDir, "memory.md");
15578
- if (existsSync22(globalMemory)) {
16112
+ const globalMemory = join18(this.baseDir, "memory.md");
16113
+ if (existsSync24(globalMemory)) {
15579
16114
  try {
15580
- const content = readFileSync16(globalMemory, "utf-8");
15581
- writeFileSync3(join16(userDir, "memory.md"), content, "utf-8");
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 = join16(this.baseDir, "history");
15586
- if (existsSync22(globalHistory)) {
16120
+ const globalHistory = join18(this.baseDir, "history");
16121
+ if (existsSync24(globalHistory)) {
15587
16122
  try {
15588
- const files = readdirSync10(globalHistory).filter((f) => f.endsWith(".json"));
15589
- const userHistory = join16(userDir, "history");
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(join16(globalHistory, f), join16(userHistory, f));
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 (existsSync22(this.usersFile)) {
16138
+ if (existsSync24(this.usersFile)) {
15604
16139
  try {
15605
- return JSON.parse(readFileSync16(this.usersFile, "utf-8"));
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 resolve6(".");
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 = join17(config.getConfigDir(), SKILLS_DIR_NAME);
15723
- if (existsSync23(skillsDir)) {
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 = join17(moduleDir, "web", "client");
15795
- if (!existsSync23(clientDir)) {
15796
- clientDir = join17(moduleDir, "client");
16329
+ let clientDir = join19(moduleDir, "web", "client");
16330
+ if (!existsSync25(clientDir)) {
16331
+ clientDir = join19(moduleDir, "client");
15797
16332
  }
15798
- if (!existsSync23(clientDir)) {
15799
- clientDir = join17(moduleDir, "..", "..", "src", "web", "client");
16333
+ if (!existsSync25(clientDir)) {
16334
+ clientDir = join19(moduleDir, "..", "..", "src", "web", "client");
15800
16335
  }
15801
- if (!existsSync23(clientDir)) {
15802
- clientDir = join17(process.cwd(), "src", "web", "client");
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(join17(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
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 = join17(cwd, prefix);
16407
+ const targetDir = join19(cwd, prefix);
15873
16408
  try {
15874
- const canonicalTarget = realpathSync(resolve6(targetDir));
15875
- const canonicalCwd = realpathSync(resolve6(cwd));
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 = readdirSync11(targetDir, { withFileTypes: true });
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: relative3(cwd, join17(targetDir, e.name)).replace(/\\/g, "/"),
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 = join17(histDir, `${id}.json`);
15926
- if (!existsSync23(filePath)) {
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(readFileSync17(filePath, "utf-8"));
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 = resolve6(join17(cwd, filePath));
16489
+ const fullPath = resolve8(join19(cwd, filePath));
15955
16490
  try {
15956
16491
  const canonicalFull = realpathSync(fullPath);
15957
- const canonicalCwd = realpathSync(resolve6(cwd));
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 = statSync9(fullPath);
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 = readFileSync17(fullPath, "utf-8");
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((resolve7, reject) => {
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
- resolve7({ port: actualPort, host, url });
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 = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
16228
- return existsSync23(configPath) ? configPath : null;
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 = join17(projectRoot, MCP_PROJECT_CONFIG_NAME);
16235
- if (!existsSync23(configPath)) return null;
16769
+ const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16770
+ if (!existsSync25(configPath)) return null;
16236
16771
  try {
16237
- const raw = JSON.parse(readFileSync17(configPath, "utf-8"));
16772
+ const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
16238
16773
  return raw.mcpServers ?? raw;
16239
16774
  } catch {
16240
16775
  return null;