jinzd-ai-cli 0.4.210 → 0.4.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -1
- package/README.zh-CN.md +63 -0
- package/dist/{batch-MFPYHOIL.js → batch-WIKFEOAZ.js} +2 -2
- package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
- package/dist/{chunk-GRJNQJA5.js → chunk-GBBVCZ4W.js} +1 -1
- package/dist/{chunk-7JWT3KXO.js → chunk-HW2ALWQJ.js} +2 -2
- package/dist/{chunk-E2ZWWE6Q.js → chunk-HX6N6QEY.js} +1 -1
- package/dist/{chunk-DJ35G5VO.js → chunk-IFZX26K7.js} +1 -1
- package/dist/{chunk-HZQVX7VF.js → chunk-KE4B3NOQ.js} +1 -1
- package/dist/{chunk-NWP7C6CC.js → chunk-LU6FBJQ5.js} +1 -1
- package/dist/{chunk-NEPFADHX.js → chunk-N5LB3PPL.js} +567 -87
- package/dist/{chunk-P6MTFCXB.js → chunk-NYCBOVNF.js} +62 -5
- package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
- package/dist/{ci-QLJUDLMY.js → ci-X3LNUFZV.js} +2 -2
- package/dist/{constants-47OR72MV.js → constants-KDWG4KR4.js} +1 -1
- package/dist/{doctor-cli-32COMA5K.js → doctor-cli-TJTWIW7U.js} +4 -4
- package/dist/electron-server.js +947 -280
- package/dist/{hub-3NWJUCLK.js → hub-JWSV4MKC.js} +1 -1
- package/dist/index.js +160 -18
- package/dist/{run-tests-NT2UIUVB.js → run-tests-D7YBY4XB.js} +1 -1
- package/dist/{run-tests-UT7RZ7Y3.js → run-tests-H4Q2PRX6.js} +2 -2
- package/dist/{server-AIHVMLNU.js → server-OFKGDRYI.js} +4 -4
- package/dist/{server-E5D54DIZ.js → server-VNCN7AY5.js} +187 -29
- package/dist/{task-orchestrator-BFDSTSOH.js → task-orchestrator-AL2E642M.js} +4 -4
- package/dist/{usage-P4B2RZKL.js → usage-LB6REWPO.js} +2 -2
- package/dist/web/client/app.js +1 -1
- package/package.json +1 -1
package/dist/electron-server.js
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
VERSION,
|
|
38
38
|
buildUserIdentityPrompt,
|
|
39
39
|
runTestsTool
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-KE4B3NOQ.js";
|
|
41
41
|
import {
|
|
42
42
|
hasSemanticIndex,
|
|
43
43
|
semanticSearch
|
|
@@ -49,8 +49,9 @@ import "./chunk-IEQAE3QG.js";
|
|
|
49
49
|
import {
|
|
50
50
|
loadChatIndex,
|
|
51
51
|
redactJson,
|
|
52
|
+
scanString,
|
|
52
53
|
searchChatMemory
|
|
53
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-OQGVGPEK.js";
|
|
54
55
|
import "./chunk-JV5N65KN.js";
|
|
55
56
|
import {
|
|
56
57
|
atomicWriteFileSync
|
|
@@ -60,8 +61,8 @@ import {
|
|
|
60
61
|
import express from "express";
|
|
61
62
|
import { createServer } from "http";
|
|
62
63
|
import { WebSocketServer } from "ws";
|
|
63
|
-
import { join as
|
|
64
|
-
import { existsSync as
|
|
64
|
+
import { join as join20, dirname as dirname6, resolve as resolve8, relative as relative4, sep as sep3 } from "path";
|
|
65
|
+
import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
|
|
65
66
|
import { networkInterfaces } from "os";
|
|
66
67
|
|
|
67
68
|
// src/config/config-manager.ts
|
|
@@ -208,12 +209,69 @@ var ConfigSchema = z.object({
|
|
|
208
209
|
env: z.record(z.string()).optional(),
|
|
209
210
|
timeout: z.number().default(3e4)
|
|
210
211
|
})).default({}),
|
|
211
|
-
//
|
|
212
|
+
// Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
|
|
212
213
|
hooks: z.object({
|
|
214
|
+
enabled: z.boolean().default(true),
|
|
213
215
|
preToolExecution: z.string().optional(),
|
|
214
|
-
postToolExecution: z.string().optional()
|
|
216
|
+
postToolExecution: z.string().optional(),
|
|
217
|
+
events: z.record(z.union([
|
|
218
|
+
z.string(),
|
|
219
|
+
z.object({
|
|
220
|
+
command: z.string(),
|
|
221
|
+
source: z.enum(["user", "project", "managed"]).default("user"),
|
|
222
|
+
description: z.string().optional(),
|
|
223
|
+
required: z.boolean().default(false),
|
|
224
|
+
timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
|
|
225
|
+
disabled: z.boolean().default(false)
|
|
226
|
+
}),
|
|
227
|
+
z.array(z.union([
|
|
228
|
+
z.string(),
|
|
229
|
+
z.object({
|
|
230
|
+
command: z.string(),
|
|
231
|
+
source: z.enum(["user", "project", "managed"]).default("user"),
|
|
232
|
+
description: z.string().optional(),
|
|
233
|
+
required: z.boolean().default(false),
|
|
234
|
+
timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
|
|
235
|
+
disabled: z.boolean().default(false)
|
|
236
|
+
})
|
|
237
|
+
]))
|
|
238
|
+
])).default({})
|
|
215
239
|
}).optional(),
|
|
216
|
-
//
|
|
240
|
+
// 网络访问治理(v0.4.211+):默认关闭以保持兼容;开启后统一治理 web/search/MCP/shell 网络出口。
|
|
241
|
+
networkPolicy: z.object({
|
|
242
|
+
enabled: z.boolean().default(false),
|
|
243
|
+
defaultAction: z.enum(["allow", "confirm", "deny"]).default("confirm"),
|
|
244
|
+
allowDomains: z.array(z.string()).default([]),
|
|
245
|
+
denyDomains: z.array(z.string()).default([]),
|
|
246
|
+
allowPorts: z.array(z.number().int().min(1).max(65535)).default([]),
|
|
247
|
+
denyPorts: z.array(z.number().int().min(1).max(65535)).default([]),
|
|
248
|
+
allowPrivateNetwork: z.boolean().default(false),
|
|
249
|
+
tools: z.object({
|
|
250
|
+
web_fetch: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
251
|
+
web_search: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
252
|
+
google_search: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
253
|
+
shell: z.enum(["allow", "confirm", "deny"]).optional(),
|
|
254
|
+
mcp: z.enum(["allow", "confirm", "deny"]).optional()
|
|
255
|
+
}).default({})
|
|
256
|
+
}).default({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
|
|
257
|
+
// 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
|
|
258
|
+
defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
|
|
259
|
+
allowedPermissionProfiles: z.array(z.string()).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
|
|
260
|
+
permissionProfiles: z.record(z.object({
|
|
261
|
+
extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
262
|
+
defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
|
|
263
|
+
workspaceRoots: z.array(z.string()).optional(),
|
|
264
|
+
allowTemp: z.boolean().optional(),
|
|
265
|
+
rules: z.array(z.object({
|
|
266
|
+
tool: z.string(),
|
|
267
|
+
action: z.enum(["auto-approve", "deny", "confirm"]),
|
|
268
|
+
when: z.object({
|
|
269
|
+
dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
|
|
270
|
+
pathPattern: z.string().optional()
|
|
271
|
+
}).optional()
|
|
272
|
+
})).optional()
|
|
273
|
+
})).default({}),
|
|
274
|
+
// 工具权限规则(按顺序匹配第一个生效;作为当前权限 Profile 内的细粒度规则)
|
|
217
275
|
permissionRules: z.array(z.object({
|
|
218
276
|
tool: z.string(),
|
|
219
277
|
action: z.enum(["auto-approve", "deny", "confirm"]),
|
|
@@ -222,7 +280,7 @@ var ConfigSchema = z.object({
|
|
|
222
280
|
pathPattern: z.string().optional()
|
|
223
281
|
}).optional()
|
|
224
282
|
})).default([]),
|
|
225
|
-
//
|
|
283
|
+
// 无规则匹配时的默认权限动作(legacy profile 兼容旧行为)
|
|
226
284
|
defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
|
|
227
285
|
// 自动上下文压缩开关
|
|
228
286
|
// 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
|
|
@@ -5727,12 +5785,13 @@ ${content}`;
|
|
|
5727
5785
|
};
|
|
5728
5786
|
|
|
5729
5787
|
// src/tools/builtin/write-file.ts
|
|
5730
|
-
import { appendFileSync, mkdirSync as
|
|
5788
|
+
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
5731
5789
|
import { dirname as dirname3 } from "path";
|
|
5732
5790
|
|
|
5733
5791
|
// src/tools/executor.ts
|
|
5734
5792
|
import chalk3 from "chalk";
|
|
5735
|
-
import { existsSync as
|
|
5793
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
5794
|
+
import { tmpdir } from "os";
|
|
5736
5795
|
|
|
5737
5796
|
// src/core/readline-internal.ts
|
|
5738
5797
|
function rlInternal(rl) {
|
|
@@ -5939,6 +5998,139 @@ function simpleDiff(oldLines, newLines) {
|
|
|
5939
5998
|
|
|
5940
5999
|
// src/tools/hooks.ts
|
|
5941
6000
|
import { execSync } from "child_process";
|
|
6001
|
+
import { createHash } from "crypto";
|
|
6002
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
6003
|
+
import { join as join3 } from "path";
|
|
6004
|
+
var TRUST_FILE = "hooks-trust.json";
|
|
6005
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
6006
|
+
function asArray(entry) {
|
|
6007
|
+
if (!entry) return [];
|
|
6008
|
+
const raw = Array.isArray(entry) ? entry : [entry];
|
|
6009
|
+
return raw.map((item) => typeof item === "string" ? { command: item } : item);
|
|
6010
|
+
}
|
|
6011
|
+
function hookTrustPath(configDir) {
|
|
6012
|
+
return join3(configDir, TRUST_FILE);
|
|
6013
|
+
}
|
|
6014
|
+
function loadHookTrustStore(configDir) {
|
|
6015
|
+
const file = hookTrustPath(configDir);
|
|
6016
|
+
if (!existsSync6(file)) return { records: [] };
|
|
6017
|
+
try {
|
|
6018
|
+
const parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
6019
|
+
return { records: Array.isArray(parsed.records) ? parsed.records : [] };
|
|
6020
|
+
} catch {
|
|
6021
|
+
return { records: [] };
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
function hashHook(event, command) {
|
|
6025
|
+
return createHash("sha256").update(`${event}
|
|
6026
|
+
${command}`, "utf-8").digest("hex");
|
|
6027
|
+
}
|
|
6028
|
+
function isTrusted(source, event, hash, store) {
|
|
6029
|
+
if (source === "user" || source === "managed") return true;
|
|
6030
|
+
return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
|
|
6031
|
+
}
|
|
6032
|
+
function listHooks(config, configDir) {
|
|
6033
|
+
if (!config || config.enabled === false) return [];
|
|
6034
|
+
const store = loadHookTrustStore(configDir);
|
|
6035
|
+
const out = [];
|
|
6036
|
+
const push = (event, hook, index) => {
|
|
6037
|
+
if (!hook.command) return;
|
|
6038
|
+
const source = hook.source ?? "user";
|
|
6039
|
+
const hash = hashHook(event, hook.command);
|
|
6040
|
+
out.push({
|
|
6041
|
+
id: `${event}:${index}`,
|
|
6042
|
+
event,
|
|
6043
|
+
command: hook.command,
|
|
6044
|
+
source,
|
|
6045
|
+
hash,
|
|
6046
|
+
trusted: isTrusted(source, event, hash, store),
|
|
6047
|
+
disabled: hook.disabled === true,
|
|
6048
|
+
description: hook.description
|
|
6049
|
+
});
|
|
6050
|
+
};
|
|
6051
|
+
if (config.preToolExecution) push("PreToolUse", { command: config.preToolExecution }, 0);
|
|
6052
|
+
if (config.postToolExecution) push("PostToolUse", { command: config.postToolExecution }, 0);
|
|
6053
|
+
for (const [event, entry] of Object.entries(config.events ?? {})) {
|
|
6054
|
+
asArray(entry).forEach((hook, i) => push(event, hook, i));
|
|
6055
|
+
}
|
|
6056
|
+
return out;
|
|
6057
|
+
}
|
|
6058
|
+
function getPendingHookTrust(config, configDir) {
|
|
6059
|
+
return listHooks(config, configDir).filter((h) => h.source === "project" && !h.disabled && !h.trusted);
|
|
6060
|
+
}
|
|
6061
|
+
function normalizeDecision(value, hook) {
|
|
6062
|
+
if (!value || typeof value !== "object") return null;
|
|
6063
|
+
const obj = value;
|
|
6064
|
+
const action = obj.action;
|
|
6065
|
+
if (action !== "allow" && action !== "deny" && action !== "ask") return null;
|
|
6066
|
+
return {
|
|
6067
|
+
action,
|
|
6068
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
6069
|
+
prompt: typeof obj.prompt === "string" ? obj.prompt : void 0,
|
|
6070
|
+
contextAppend: typeof obj.contextAppend === "string" ? obj.contextAppend : void 0,
|
|
6071
|
+
warning: typeof obj.warning === "string" ? obj.warning : void 0,
|
|
6072
|
+
warnings: Array.isArray(obj.warnings) ? obj.warnings.filter((x) => typeof x === "string") : void 0,
|
|
6073
|
+
hook
|
|
6074
|
+
};
|
|
6075
|
+
}
|
|
6076
|
+
function runStructuredHook(hook, descriptor, payload) {
|
|
6077
|
+
if (hook.disabled === true) return null;
|
|
6078
|
+
const timeout = Math.max(100, Math.min(hook.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
|
|
6079
|
+
try {
|
|
6080
|
+
const stdout = execSync(hook.command, {
|
|
6081
|
+
timeout,
|
|
6082
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6083
|
+
encoding: "utf-8",
|
|
6084
|
+
env: {
|
|
6085
|
+
...process.env,
|
|
6086
|
+
AICLI_HOOK_EVENT: payload.event,
|
|
6087
|
+
AICLI_HOOK_EVENT_JSON: JSON.stringify(payload)
|
|
6088
|
+
}
|
|
6089
|
+
}).trim();
|
|
6090
|
+
if (!stdout) return null;
|
|
6091
|
+
return normalizeDecision(JSON.parse(stdout), descriptor);
|
|
6092
|
+
} catch (err) {
|
|
6093
|
+
process.stderr.write(`\u26A0 Hook failed: ${hook.command.slice(0, 100)}
|
|
6094
|
+
`);
|
|
6095
|
+
if (hook.required) {
|
|
6096
|
+
return { action: "deny", reason: err instanceof Error ? err.message : String(err), hook: descriptor };
|
|
6097
|
+
}
|
|
6098
|
+
return null;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
function runLifecycleHooks(config, event, payload, opts) {
|
|
6102
|
+
if (!config || config.enabled === false) return [];
|
|
6103
|
+
const options = typeof opts === "string" ? { configDir: opts } : opts;
|
|
6104
|
+
const store = loadHookTrustStore(options.configDir);
|
|
6105
|
+
const hooks = asArray(config.events?.[event]);
|
|
6106
|
+
const decisions = [];
|
|
6107
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
6108
|
+
const hook = hooks[i];
|
|
6109
|
+
if (!hook.command || hook.disabled) continue;
|
|
6110
|
+
const source = hook.source ?? "user";
|
|
6111
|
+
const hash = hashHook(event, hook.command);
|
|
6112
|
+
const descriptor = {
|
|
6113
|
+
id: `${event}:${i}`,
|
|
6114
|
+
event,
|
|
6115
|
+
command: hook.command,
|
|
6116
|
+
source,
|
|
6117
|
+
hash,
|
|
6118
|
+
trusted: isTrusted(source, event, hash, store),
|
|
6119
|
+
disabled: false,
|
|
6120
|
+
description: hook.description
|
|
6121
|
+
};
|
|
6122
|
+
if (!descriptor.trusted) {
|
|
6123
|
+
options.onSummary?.(`hook skipped: ${event} project hook requires trust (${hash.slice(0, 12)})`);
|
|
6124
|
+
continue;
|
|
6125
|
+
}
|
|
6126
|
+
const decision = runStructuredHook(hook, descriptor, { event, ...payload });
|
|
6127
|
+
if (decision) {
|
|
6128
|
+
decisions.push(decision);
|
|
6129
|
+
options.onSummary?.(`hook ${event}: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`);
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
return decisions;
|
|
6133
|
+
}
|
|
5942
6134
|
function rewriteTemplate(template, isWindows) {
|
|
5943
6135
|
const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
|
|
5944
6136
|
return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
|
|
@@ -5949,7 +6141,7 @@ function runHook(template, vars) {
|
|
|
5949
6141
|
const cmd = rewriteTemplate(template, isWindows);
|
|
5950
6142
|
try {
|
|
5951
6143
|
execSync(cmd, {
|
|
5952
|
-
timeout:
|
|
6144
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
5953
6145
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5954
6146
|
encoding: "utf-8",
|
|
5955
6147
|
env: {
|
|
@@ -5967,6 +6159,7 @@ function runHook(template, vars) {
|
|
|
5967
6159
|
}
|
|
5968
6160
|
|
|
5969
6161
|
// src/tools/permissions.ts
|
|
6162
|
+
import { isAbsolute, resolve as resolve3 } from "path";
|
|
5970
6163
|
function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
|
|
5971
6164
|
for (const rule of rules) {
|
|
5972
6165
|
if (rule.tool !== "*" && rule.tool !== toolName) continue;
|
|
@@ -5987,6 +6180,225 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
|
|
|
5987
6180
|
}
|
|
5988
6181
|
return defaultAction;
|
|
5989
6182
|
}
|
|
6183
|
+
var BUILTIN_PROFILE_NAMES = /* @__PURE__ */ new Set(["legacy", "read-only", "workspace-write", "danger-full-access"]);
|
|
6184
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
6185
|
+
"read_file",
|
|
6186
|
+
"list_dir",
|
|
6187
|
+
"grep_files",
|
|
6188
|
+
"glob_files",
|
|
6189
|
+
"find_symbol",
|
|
6190
|
+
"get_outline",
|
|
6191
|
+
"find_references",
|
|
6192
|
+
"search_code",
|
|
6193
|
+
"git_status",
|
|
6194
|
+
"git_diff",
|
|
6195
|
+
"git_log",
|
|
6196
|
+
"task_list",
|
|
6197
|
+
"web_fetch",
|
|
6198
|
+
"web_search",
|
|
6199
|
+
"google_search",
|
|
6200
|
+
"ask_user",
|
|
6201
|
+
"write_todos",
|
|
6202
|
+
"recall_memory"
|
|
6203
|
+
]);
|
|
6204
|
+
function normalizePathForPermission(value) {
|
|
6205
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
6206
|
+
}
|
|
6207
|
+
function isInsidePath(root, target) {
|
|
6208
|
+
const absoluteTarget = isAbsolute(target) ? target : resolve3(root, target);
|
|
6209
|
+
const r = normalizePathForPermission(resolve3(root));
|
|
6210
|
+
const t = normalizePathForPermission(absoluteTarget);
|
|
6211
|
+
return t === r || t.startsWith(`${r}/`);
|
|
6212
|
+
}
|
|
6213
|
+
function extractPathArg(toolName, args) {
|
|
6214
|
+
const raw = args["path"] ?? args["filePath"] ?? args["notebook_path"];
|
|
6215
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim();
|
|
6216
|
+
if (toolName === "save_last_response") {
|
|
6217
|
+
const output = args["outputPath"] ?? args["file"] ?? args["path"];
|
|
6218
|
+
if (typeof output === "string" && output.trim()) return output.trim();
|
|
6219
|
+
}
|
|
6220
|
+
return null;
|
|
6221
|
+
}
|
|
6222
|
+
function isExplicitFileWriteTool(toolName) {
|
|
6223
|
+
return toolName === "write_file" || toolName === "edit_file" || toolName === "notebook_edit" || toolName === "save_last_response";
|
|
6224
|
+
}
|
|
6225
|
+
function resolveProfileName(ctx) {
|
|
6226
|
+
const requested = ctx?.profileName ?? "legacy";
|
|
6227
|
+
if (ctx?.allowedProfiles && ctx.allowedProfiles.length > 0 && !ctx.allowedProfiles.includes(requested)) {
|
|
6228
|
+
return "legacy";
|
|
6229
|
+
}
|
|
6230
|
+
if (BUILTIN_PROFILE_NAMES.has(requested)) return requested;
|
|
6231
|
+
const ext = ctx?.profile?.extends;
|
|
6232
|
+
return ext && BUILTIN_PROFILE_NAMES.has(ext) ? ext : "legacy";
|
|
6233
|
+
}
|
|
6234
|
+
function getProfileRules(ctx) {
|
|
6235
|
+
return ctx?.profile?.rules ?? [];
|
|
6236
|
+
}
|
|
6237
|
+
function getProfileDefaultAction(name, ctx) {
|
|
6238
|
+
if (ctx?.profile?.defaultAction) return ctx.profile.defaultAction;
|
|
6239
|
+
if (name === "danger-full-access") return "auto-approve";
|
|
6240
|
+
return "confirm";
|
|
6241
|
+
}
|
|
6242
|
+
function getWorkspaceRoots(ctx) {
|
|
6243
|
+
const roots = [ctx?.workspaceRoot, ...ctx?.profile?.workspaceRoots ?? []].filter((v) => typeof v === "string" && v.trim().length > 0);
|
|
6244
|
+
return roots;
|
|
6245
|
+
}
|
|
6246
|
+
function getAllowedWriteRoots(ctx) {
|
|
6247
|
+
const roots = [...getWorkspaceRoots(ctx)];
|
|
6248
|
+
const allowTemp = ctx?.profile?.allowTemp !== false;
|
|
6249
|
+
if (allowTemp) roots.push(...ctx?.tempDirs ?? []);
|
|
6250
|
+
return roots.filter((v) => v.trim().length > 0);
|
|
6251
|
+
}
|
|
6252
|
+
function profileHardDecision(profileName, toolName, args, dangerLevel, ctx) {
|
|
6253
|
+
if (profileName === "legacy") return null;
|
|
6254
|
+
if (profileName === "read-only") {
|
|
6255
|
+
const mcpReadOnly = toolName.startsWith("mcp__") && dangerLevel === "safe";
|
|
6256
|
+
if (dangerLevel !== "safe" || !READ_ONLY_TOOLS.has(toolName) && !mcpReadOnly) {
|
|
6257
|
+
return { action: "deny", reason: "blocked by read-only permission profile", profileName };
|
|
6258
|
+
}
|
|
6259
|
+
return null;
|
|
6260
|
+
}
|
|
6261
|
+
if (profileName === "workspace-write") {
|
|
6262
|
+
if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools always require confirmation", profileName };
|
|
6263
|
+
if (dangerLevel === "write" && isExplicitFileWriteTool(toolName)) {
|
|
6264
|
+
const target = extractPathArg(toolName, args);
|
|
6265
|
+
if (!target) return { action: "confirm", reason: "file write target is not explicit", profileName };
|
|
6266
|
+
const allowedRoots = getAllowedWriteRoots(ctx);
|
|
6267
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsidePath(root, target))) {
|
|
6268
|
+
return { action: "deny", reason: "file write target is outside workspace/temp roots", profileName };
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
return null;
|
|
6272
|
+
}
|
|
6273
|
+
if (profileName === "danger-full-access") {
|
|
6274
|
+
if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools still require confirmation", profileName };
|
|
6275
|
+
return null;
|
|
6276
|
+
}
|
|
6277
|
+
return null;
|
|
6278
|
+
}
|
|
6279
|
+
function checkPermissionWithProfile(toolName, args, dangerLevel, rules, defaultAction = "confirm", profileContext) {
|
|
6280
|
+
const profileName = resolveProfileName(profileContext);
|
|
6281
|
+
const hard = profileHardDecision(profileName, toolName, args, dangerLevel, profileContext);
|
|
6282
|
+
if (hard?.action === "deny") return hard;
|
|
6283
|
+
const combinedRules = [...getProfileRules(profileContext), ...rules];
|
|
6284
|
+
const profileDefault = profileName === "legacy" ? defaultAction : getProfileDefaultAction(profileName, profileContext);
|
|
6285
|
+
const action = checkPermission(toolName, args, dangerLevel, combinedRules, profileDefault);
|
|
6286
|
+
if (hard?.action === "confirm" && action === "auto-approve") return hard;
|
|
6287
|
+
return { action, profileName };
|
|
6288
|
+
}
|
|
6289
|
+
function formatPermissionProfileWarning(profileName) {
|
|
6290
|
+
if (profileName === "danger-full-access") {
|
|
6291
|
+
return "\u26A0 danger-full-access permission profile is active. Write tools may auto-run; destructive tools still require confirmation.";
|
|
6292
|
+
}
|
|
6293
|
+
if (profileName === "read-only") return "Read-only permission profile is active. Write and destructive tools are blocked.";
|
|
6294
|
+
if (profileName === "workspace-write") return "Workspace-write permission profile is active. Writes outside workspace/temp roots are blocked.";
|
|
6295
|
+
return null;
|
|
6296
|
+
}
|
|
6297
|
+
var NETWORK_TOOL_HOSTS = {
|
|
6298
|
+
web_search: ["cn.bing.com", "www.google.com"],
|
|
6299
|
+
google_search: ["www.googleapis.com"]
|
|
6300
|
+
};
|
|
6301
|
+
var NETWORK_TOOL_PORTS = {
|
|
6302
|
+
web_search: [443],
|
|
6303
|
+
google_search: [443]
|
|
6304
|
+
};
|
|
6305
|
+
function networkActionToPermission(action) {
|
|
6306
|
+
if (action === "allow") return null;
|
|
6307
|
+
return action;
|
|
6308
|
+
}
|
|
6309
|
+
function domainMatches(pattern, host) {
|
|
6310
|
+
const p = pattern.trim().toLowerCase().replace(/^\*\./, "");
|
|
6311
|
+
const h = host.trim().toLowerCase().replace(/:\d+$/, "");
|
|
6312
|
+
return h === p || h.endsWith(`.${p}`);
|
|
6313
|
+
}
|
|
6314
|
+
function isPrivateNetworkHost(host) {
|
|
6315
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
6316
|
+
if (h === "" || h === "localhost") return true;
|
|
6317
|
+
if (/^(127|10)\./.test(h)) return true;
|
|
6318
|
+
if (/^192\.168\./.test(h)) return true;
|
|
6319
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
|
|
6320
|
+
if (/^169\.254\./.test(h)) return true;
|
|
6321
|
+
if (h === "::1" || h === "::") return true;
|
|
6322
|
+
if (/^f[cd][0-9a-f]*:/i.test(h) || /^fe[89ab][0-9a-f]*:/i.test(h)) return true;
|
|
6323
|
+
return false;
|
|
6324
|
+
}
|
|
6325
|
+
function extractNetworkHosts(toolName, args) {
|
|
6326
|
+
if (toolName === "web_fetch") {
|
|
6327
|
+
const raw = String(args["url"] ?? "").trim();
|
|
6328
|
+
try {
|
|
6329
|
+
return raw ? [new URL(raw).hostname] : [];
|
|
6330
|
+
} catch {
|
|
6331
|
+
return [];
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
return NETWORK_TOOL_HOSTS[toolName] ?? [];
|
|
6335
|
+
}
|
|
6336
|
+
function extractNetworkPorts(toolName, args) {
|
|
6337
|
+
if (toolName === "web_fetch") {
|
|
6338
|
+
const raw = String(args["url"] ?? "").trim();
|
|
6339
|
+
try {
|
|
6340
|
+
if (!raw) return [];
|
|
6341
|
+
const url = new URL(raw);
|
|
6342
|
+
if (url.port) return [Number(url.port)];
|
|
6343
|
+
return [url.protocol === "http:" ? 80 : 443];
|
|
6344
|
+
} catch {
|
|
6345
|
+
return [];
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
return NETWORK_TOOL_PORTS[toolName] ?? [];
|
|
6349
|
+
}
|
|
6350
|
+
function isShellNetworkCommand(command) {
|
|
6351
|
+
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);
|
|
6352
|
+
}
|
|
6353
|
+
function networkToolAction(toolName, policy) {
|
|
6354
|
+
if (toolName === "web_fetch") return policy.tools?.web_fetch ?? policy.defaultAction ?? "confirm";
|
|
6355
|
+
if (toolName === "web_search") return policy.tools?.web_search ?? policy.defaultAction ?? "confirm";
|
|
6356
|
+
if (toolName === "google_search") return policy.tools?.google_search ?? policy.defaultAction ?? "confirm";
|
|
6357
|
+
if (toolName.startsWith("mcp__")) return policy.tools?.mcp ?? policy.defaultAction ?? "confirm";
|
|
6358
|
+
return null;
|
|
6359
|
+
}
|
|
6360
|
+
function checkNetworkPolicy(toolName, args, policy) {
|
|
6361
|
+
if (!policy?.enabled) return null;
|
|
6362
|
+
let action = networkToolAction(toolName, policy);
|
|
6363
|
+
let reason = "network access requires confirmation";
|
|
6364
|
+
if (toolName === "bash") {
|
|
6365
|
+
const command = String(args["command"] ?? "");
|
|
6366
|
+
if (!isShellNetworkCommand(command)) return null;
|
|
6367
|
+
action = policy.tools?.shell ?? policy.defaultAction ?? "confirm";
|
|
6368
|
+
reason = "shell command may access the network";
|
|
6369
|
+
}
|
|
6370
|
+
if (!action) return null;
|
|
6371
|
+
const hosts = extractNetworkHosts(toolName, args);
|
|
6372
|
+
for (const host of hosts) {
|
|
6373
|
+
if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) {
|
|
6374
|
+
return { action: "deny", reason: `network host "${host}" is denied by networkPolicy`, profileName: "networkPolicy" };
|
|
6375
|
+
}
|
|
6376
|
+
if (!policy.allowPrivateNetwork && isPrivateNetworkHost(host)) {
|
|
6377
|
+
return { action: "deny", reason: `network host "${host}" is private/internal`, profileName: "networkPolicy" };
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
if (hosts.length > 0 && policy.allowDomains && policy.allowDomains.length > 0) {
|
|
6381
|
+
const allAllowed = hosts.every((host) => policy.allowDomains.some((pattern) => domainMatches(pattern, host)));
|
|
6382
|
+
if (!allAllowed) {
|
|
6383
|
+
return { action: "deny", reason: `network host is not in networkPolicy.allowDomains`, profileName: "networkPolicy" };
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
const ports = extractNetworkPorts(toolName, args);
|
|
6387
|
+
for (const port of ports) {
|
|
6388
|
+
if (policy.denyPorts?.includes(port)) {
|
|
6389
|
+
return { action: "deny", reason: `network port ${port} is denied by networkPolicy`, profileName: "networkPolicy" };
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
if (ports.length > 0 && policy.allowPorts && policy.allowPorts.length > 0) {
|
|
6393
|
+
const allPortsAllowed = ports.every((port) => policy.allowPorts.includes(port));
|
|
6394
|
+
if (!allPortsAllowed) {
|
|
6395
|
+
return { action: "deny", reason: `network port is not in networkPolicy.allowPorts`, profileName: "networkPolicy" };
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
const permissionAction = networkActionToPermission(action);
|
|
6399
|
+
if (!permissionAction) return null;
|
|
6400
|
+
return { action: permissionAction, reason, profileName: "networkPolicy" };
|
|
6401
|
+
}
|
|
5990
6402
|
|
|
5991
6403
|
// src/tools/truncate.ts
|
|
5992
6404
|
var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
|
|
@@ -6129,8 +6541,8 @@ var theme = new Proxy(DARK_THEME, {
|
|
|
6129
6541
|
});
|
|
6130
6542
|
|
|
6131
6543
|
// src/diagnostics/tool-stats.ts
|
|
6132
|
-
import { existsSync as
|
|
6133
|
-
import { join as
|
|
6544
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
6545
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
6134
6546
|
import { homedir as homedir3 } from "os";
|
|
6135
6547
|
var STATS_FILE_NAME = "tool-stats.json";
|
|
6136
6548
|
var STATS_VERSION = 1;
|
|
@@ -6140,16 +6552,16 @@ var dirty = false;
|
|
|
6140
6552
|
var pendingWrites = 0;
|
|
6141
6553
|
var configDirOverride;
|
|
6142
6554
|
function statsFilePath() {
|
|
6143
|
-
const base = configDirOverride ??
|
|
6144
|
-
return
|
|
6555
|
+
const base = configDirOverride ?? join4(homedir3(), CONFIG_DIR_NAME);
|
|
6556
|
+
return join4(base, STATS_FILE_NAME);
|
|
6145
6557
|
}
|
|
6146
6558
|
function load() {
|
|
6147
6559
|
const path3 = statsFilePath();
|
|
6148
|
-
if (!
|
|
6560
|
+
if (!existsSync7(path3)) {
|
|
6149
6561
|
return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
6150
6562
|
}
|
|
6151
6563
|
try {
|
|
6152
|
-
const raw = JSON.parse(
|
|
6564
|
+
const raw = JSON.parse(readFileSync6(path3, "utf-8"));
|
|
6153
6565
|
if (raw.version !== STATS_VERSION || !raw.entries) {
|
|
6154
6566
|
return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
6155
6567
|
}
|
|
@@ -6207,7 +6619,7 @@ function flush() {
|
|
|
6207
6619
|
if (!dirty || state === null) return;
|
|
6208
6620
|
const path3 = statsFilePath();
|
|
6209
6621
|
try {
|
|
6210
|
-
|
|
6622
|
+
mkdirSync4(dirname2(path3), { recursive: true });
|
|
6211
6623
|
atomicWriteFileSync(path3, JSON.stringify(state, null, 2));
|
|
6212
6624
|
dirty = false;
|
|
6213
6625
|
pendingWrites = 0;
|
|
@@ -6289,11 +6701,23 @@ var ToolExecutor = class {
|
|
|
6289
6701
|
/** 权限规则(可选) */
|
|
6290
6702
|
permissionRules = [];
|
|
6291
6703
|
defaultPermission = "confirm";
|
|
6704
|
+
permissionProfileName = "legacy";
|
|
6705
|
+
permissionProfile;
|
|
6706
|
+
allowedPermissionProfiles = [];
|
|
6707
|
+
workspaceRoot = process.cwd();
|
|
6708
|
+
networkPolicy;
|
|
6709
|
+
hookConfigDir = process.cwd();
|
|
6292
6710
|
/** 注入 hooks 和 permission rules 配置 */
|
|
6293
6711
|
setConfig(opts) {
|
|
6294
6712
|
this.hookConfig = opts.hookConfig;
|
|
6295
6713
|
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
6296
6714
|
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
6715
|
+
if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
|
|
6716
|
+
if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
|
|
6717
|
+
if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
|
|
6718
|
+
if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
|
|
6719
|
+
if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
|
|
6720
|
+
if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
|
|
6297
6721
|
}
|
|
6298
6722
|
async execute(call) {
|
|
6299
6723
|
return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
|
|
@@ -6308,31 +6732,97 @@ var ToolExecutor = class {
|
|
|
6308
6732
|
};
|
|
6309
6733
|
}
|
|
6310
6734
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
6735
|
+
let toolCallAlreadyPrinted = false;
|
|
6311
6736
|
runHook(this.hookConfig?.preToolExecution, {
|
|
6312
6737
|
tool: call.name,
|
|
6313
6738
|
dangerLevel,
|
|
6314
6739
|
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
6315
6740
|
});
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6741
|
+
const preToolDecisions = runLifecycleHooks(
|
|
6742
|
+
this.hookConfig,
|
|
6743
|
+
"PreToolUse",
|
|
6744
|
+
{ tool: call.name, dangerLevel, args: call.arguments },
|
|
6745
|
+
{ configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(` [hook] ${msg}`)) }
|
|
6746
|
+
);
|
|
6747
|
+
const preDeny = preToolDecisions.find((d) => d.action === "deny");
|
|
6748
|
+
if (preDeny) {
|
|
6749
|
+
return {
|
|
6750
|
+
callId: call.id,
|
|
6751
|
+
content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
|
|
6752
|
+
isError: true
|
|
6753
|
+
};
|
|
6754
|
+
}
|
|
6755
|
+
for (const d of preToolDecisions) {
|
|
6756
|
+
const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
|
|
6757
|
+
for (const w of warnings) console.log(theme.warning(` \u26A0 Hook warning: ${w}`));
|
|
6758
|
+
}
|
|
6759
|
+
const permission = checkPermissionWithProfile(
|
|
6760
|
+
call.name,
|
|
6761
|
+
call.arguments,
|
|
6762
|
+
dangerLevel,
|
|
6763
|
+
this.permissionRules,
|
|
6764
|
+
this.defaultPermission,
|
|
6765
|
+
{
|
|
6766
|
+
profileName: this.permissionProfileName,
|
|
6767
|
+
profile: this.permissionProfile,
|
|
6768
|
+
allowedProfiles: this.allowedPermissionProfiles,
|
|
6769
|
+
workspaceRoot: this.workspaceRoot,
|
|
6770
|
+
tempDirs: [tmpdir()]
|
|
6320
6771
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
}
|
|
6772
|
+
);
|
|
6773
|
+
const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
|
|
6774
|
+
if (networkPermission?.action === "confirm" || permission.action === "confirm") {
|
|
6775
|
+
runLifecycleHooks(
|
|
6776
|
+
this.hookConfig,
|
|
6777
|
+
"PermissionRequest",
|
|
6778
|
+
{ tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
|
|
6779
|
+
{ configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(" [hook] " + msg)) }
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
if (networkPermission?.action === "deny") {
|
|
6783
|
+
const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
|
|
6784
|
+
return {
|
|
6785
|
+
callId: call.id,
|
|
6786
|
+
content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
|
|
6787
|
+
isError: true
|
|
6788
|
+
};
|
|
6789
|
+
}
|
|
6790
|
+
if (permission.action === "deny") {
|
|
6791
|
+
const reason = permission.reason ? ` (${permission.reason})` : "";
|
|
6792
|
+
return {
|
|
6793
|
+
callId: call.id,
|
|
6794
|
+
content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
|
|
6795
|
+
isError: true
|
|
6796
|
+
};
|
|
6797
|
+
}
|
|
6798
|
+
if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
|
|
6799
|
+
this.printToolCall(call);
|
|
6800
|
+
try {
|
|
6801
|
+
const rawContent = await runTool(tool, call.arguments, call.name);
|
|
6802
|
+
const content = truncateOutput(rawContent, call.name);
|
|
6803
|
+
const wasTruncated = content !== rawContent;
|
|
6804
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
6805
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
6806
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
6807
|
+
return { callId: call.id, content, isError: false };
|
|
6808
|
+
} catch (err) {
|
|
6809
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6810
|
+
this.printToolResult(call.name, message, true, false);
|
|
6811
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
6812
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
6813
|
+
return { callId: call.id, content: message, isError: true };
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
|
|
6817
|
+
this.printToolCall(call);
|
|
6818
|
+
toolCallAlreadyPrinted = true;
|
|
6819
|
+
const confirmed = await this.confirm(call, "write");
|
|
6820
|
+
if (!confirmed) {
|
|
6821
|
+
return {
|
|
6822
|
+
callId: call.id,
|
|
6823
|
+
content: `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`,
|
|
6824
|
+
isError: true
|
|
6825
|
+
};
|
|
6336
6826
|
}
|
|
6337
6827
|
}
|
|
6338
6828
|
if (this.sessionAutoApprove && dangerLevel === "write") {
|
|
@@ -6345,11 +6835,13 @@ var ToolExecutor = class {
|
|
|
6345
6835
|
const wasTruncated = content !== rawContent;
|
|
6346
6836
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
6347
6837
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
6838
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
6348
6839
|
return { callId: call.id, content, isError: false };
|
|
6349
6840
|
} catch (err) {
|
|
6350
6841
|
const message = err instanceof Error ? err.message : String(err);
|
|
6351
6842
|
this.printToolResult(call.name, message, true, false);
|
|
6352
6843
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
6844
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
6353
6845
|
return { callId: call.id, content: message, isError: true };
|
|
6354
6846
|
}
|
|
6355
6847
|
}
|
|
@@ -6383,11 +6875,13 @@ var ToolExecutor = class {
|
|
|
6383
6875
|
const wasTruncated = content !== rawContent;
|
|
6384
6876
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
6385
6877
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
6878
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
6386
6879
|
return { callId: call.id, content, isError: false };
|
|
6387
6880
|
} catch (err) {
|
|
6388
6881
|
const message = err instanceof Error ? err.message : String(err);
|
|
6389
6882
|
this.printToolResult(call.name, message, true, false);
|
|
6390
6883
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
6884
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
6391
6885
|
return { callId: call.id, content: message, isError: true };
|
|
6392
6886
|
}
|
|
6393
6887
|
}
|
|
@@ -6422,6 +6916,9 @@ var ToolExecutor = class {
|
|
|
6422
6916
|
* 然后让用户 approve all / reject all / 选择性 approve。
|
|
6423
6917
|
*/
|
|
6424
6918
|
async executeBatchFileWrites(calls) {
|
|
6919
|
+
if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
|
|
6920
|
+
return Promise.all(calls.map((call) => this.execute(call)));
|
|
6921
|
+
}
|
|
6425
6922
|
console.log();
|
|
6426
6923
|
console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
|
|
6427
6924
|
console.log(theme.dim("\u2500".repeat(50)));
|
|
@@ -6495,7 +6992,7 @@ var ToolExecutor = class {
|
|
|
6495
6992
|
rl.resume();
|
|
6496
6993
|
process.stdout.write(prompt);
|
|
6497
6994
|
this.confirming = true;
|
|
6498
|
-
return new Promise((
|
|
6995
|
+
return new Promise((resolve9) => {
|
|
6499
6996
|
let completed = false;
|
|
6500
6997
|
const cleanup = (result) => {
|
|
6501
6998
|
if (completed) return;
|
|
@@ -6505,7 +7002,7 @@ var ToolExecutor = class {
|
|
|
6505
7002
|
rl.pause();
|
|
6506
7003
|
rlAny.output = savedOutput;
|
|
6507
7004
|
this.confirming = false;
|
|
6508
|
-
|
|
7005
|
+
resolve9(result);
|
|
6509
7006
|
};
|
|
6510
7007
|
const onLine = (line) => {
|
|
6511
7008
|
const trimmed = line.trim();
|
|
@@ -6572,10 +7069,10 @@ var ToolExecutor = class {
|
|
|
6572
7069
|
const filePath = String(call.arguments["path"] ?? "");
|
|
6573
7070
|
const newContent = String(call.arguments["content"] ?? "");
|
|
6574
7071
|
if (!filePath) return;
|
|
6575
|
-
if (
|
|
7072
|
+
if (existsSync8(filePath)) {
|
|
6576
7073
|
let oldContent;
|
|
6577
7074
|
try {
|
|
6578
|
-
oldContent =
|
|
7075
|
+
oldContent = readFileSync7(filePath, "utf-8");
|
|
6579
7076
|
} catch {
|
|
6580
7077
|
return;
|
|
6581
7078
|
}
|
|
@@ -6598,7 +7095,7 @@ var ToolExecutor = class {
|
|
|
6598
7095
|
}
|
|
6599
7096
|
} else if (call.name === "edit_file") {
|
|
6600
7097
|
const filePath = String(call.arguments["path"] ?? "");
|
|
6601
|
-
if (!filePath || !
|
|
7098
|
+
if (!filePath || !existsSync8(filePath)) return;
|
|
6602
7099
|
const oldStr = call.arguments["old_str"];
|
|
6603
7100
|
const newStr = call.arguments["new_str"];
|
|
6604
7101
|
if (oldStr !== void 0) {
|
|
@@ -6625,7 +7122,7 @@ var ToolExecutor = class {
|
|
|
6625
7122
|
const to = Number(call.arguments["delete_to_line"] ?? from);
|
|
6626
7123
|
let fileContent;
|
|
6627
7124
|
try {
|
|
6628
|
-
fileContent =
|
|
7125
|
+
fileContent = readFileSync7(filePath, "utf-8");
|
|
6629
7126
|
} catch {
|
|
6630
7127
|
return;
|
|
6631
7128
|
}
|
|
@@ -6679,7 +7176,7 @@ var ToolExecutor = class {
|
|
|
6679
7176
|
rl.resume();
|
|
6680
7177
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
6681
7178
|
this.confirming = true;
|
|
6682
|
-
return new Promise((
|
|
7179
|
+
return new Promise((resolve9) => {
|
|
6683
7180
|
let completed = false;
|
|
6684
7181
|
const cleanup = (answer) => {
|
|
6685
7182
|
if (completed) return;
|
|
@@ -6689,7 +7186,7 @@ var ToolExecutor = class {
|
|
|
6689
7186
|
rl.pause();
|
|
6690
7187
|
rlAny.output = savedOutput;
|
|
6691
7188
|
this.confirming = false;
|
|
6692
|
-
|
|
7189
|
+
resolve9(answer === "y");
|
|
6693
7190
|
};
|
|
6694
7191
|
const onLine = (line) => {
|
|
6695
7192
|
const trimmed = line.trim();
|
|
@@ -6717,11 +7214,11 @@ var ToolExecutor = class {
|
|
|
6717
7214
|
};
|
|
6718
7215
|
|
|
6719
7216
|
// src/tools/sensitive-paths.ts
|
|
6720
|
-
import { resolve as
|
|
7217
|
+
import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
|
|
6721
7218
|
import { homedir as homedir4 } from "os";
|
|
6722
7219
|
var home = homedir4();
|
|
6723
7220
|
function norm(p) {
|
|
6724
|
-
const abs =
|
|
7221
|
+
const abs = resolve4(p);
|
|
6725
7222
|
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
6726
7223
|
}
|
|
6727
7224
|
function homeRel(p) {
|
|
@@ -6860,7 +7357,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
|
|
|
6860
7357
|
}
|
|
6861
7358
|
undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
|
|
6862
7359
|
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
6863
|
-
|
|
7360
|
+
mkdirSync5(dirname3(filePath), { recursive: true });
|
|
6864
7361
|
if (appendMode) {
|
|
6865
7362
|
appendFileSync(filePath, content, encoding);
|
|
6866
7363
|
} else {
|
|
@@ -6880,7 +7377,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
|
|
|
6880
7377
|
};
|
|
6881
7378
|
|
|
6882
7379
|
// src/tools/builtin/edit-file.ts
|
|
6883
|
-
import { readFileSync as
|
|
7380
|
+
import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
|
|
6884
7381
|
|
|
6885
7382
|
// src/tools/builtin/patch-apply.ts
|
|
6886
7383
|
function parseUnifiedDiff(patch) {
|
|
@@ -7241,7 +7738,7 @@ Note: Path can be absolute or relative to cwd.`,
|
|
|
7241
7738
|
const filePath = String(args["path"] ?? "");
|
|
7242
7739
|
const encoding = args["encoding"] ?? "utf-8";
|
|
7243
7740
|
if (!filePath) throw new ToolError("edit_file", "path is required");
|
|
7244
|
-
if (!
|
|
7741
|
+
if (!existsSync9(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
|
|
7245
7742
|
const verdict = classifyWritePath(filePath);
|
|
7246
7743
|
if (verdict.sensitive && subAgentGuard.active) {
|
|
7247
7744
|
throw new ToolError(
|
|
@@ -7249,7 +7746,7 @@ Note: Path can be absolute or relative to cwd.`,
|
|
|
7249
7746
|
`Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
|
|
7250
7747
|
);
|
|
7251
7748
|
}
|
|
7252
|
-
const original =
|
|
7749
|
+
const original = readFileSync8(filePath, encoding);
|
|
7253
7750
|
if (args["patch"] !== void 0) {
|
|
7254
7751
|
const patchText = String(args["patch"] ?? "");
|
|
7255
7752
|
const stopOnError = args["stop_on_error"] !== false;
|
|
@@ -7407,8 +7904,8 @@ function truncatePreview(str, maxLen = 80) {
|
|
|
7407
7904
|
}
|
|
7408
7905
|
|
|
7409
7906
|
// src/tools/builtin/list-dir.ts
|
|
7410
|
-
import { readdirSync as readdirSync4, statSync as statSync3, existsSync as
|
|
7411
|
-
import { join as
|
|
7907
|
+
import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync10 } from "fs";
|
|
7908
|
+
import { join as join5, basename as basename3 } from "path";
|
|
7412
7909
|
var listDirTool = {
|
|
7413
7910
|
definition: {
|
|
7414
7911
|
name: "list_dir",
|
|
@@ -7430,7 +7927,7 @@ var listDirTool = {
|
|
|
7430
7927
|
async execute(args) {
|
|
7431
7928
|
const dirPath = String(args["path"] ?? process.cwd());
|
|
7432
7929
|
const recursive = Boolean(args["recursive"] ?? false);
|
|
7433
|
-
if (!
|
|
7930
|
+
if (!existsSync10(dirPath)) {
|
|
7434
7931
|
const targetName = basename3(dirPath).toLowerCase();
|
|
7435
7932
|
const cwd = process.cwd();
|
|
7436
7933
|
const suggestions = [];
|
|
@@ -7488,11 +7985,11 @@ function listRecursive(basePath, indent, recursive, lines) {
|
|
|
7488
7985
|
if (entry.isDirectory()) {
|
|
7489
7986
|
lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
|
|
7490
7987
|
if (recursive) {
|
|
7491
|
-
listRecursive(
|
|
7988
|
+
listRecursive(join5(basePath, entry.name), indent + " ", true, lines);
|
|
7492
7989
|
}
|
|
7493
7990
|
} else {
|
|
7494
7991
|
try {
|
|
7495
|
-
const stat = statSync3(
|
|
7992
|
+
const stat = statSync3(join5(basePath, entry.name));
|
|
7496
7993
|
const size = formatSize(stat.size);
|
|
7497
7994
|
lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
|
|
7498
7995
|
} catch {
|
|
@@ -7508,9 +8005,9 @@ function formatSize(bytes) {
|
|
|
7508
8005
|
}
|
|
7509
8006
|
|
|
7510
8007
|
// src/tools/builtin/grep-files.ts
|
|
7511
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
8008
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, existsSync as existsSync11 } from "fs";
|
|
7512
8009
|
import { readFile } from "fs/promises";
|
|
7513
|
-
import { join as
|
|
8010
|
+
import { join as join6, relative } from "path";
|
|
7514
8011
|
var grepFilesTool = {
|
|
7515
8012
|
definition: {
|
|
7516
8013
|
name: "grep_files",
|
|
@@ -7561,7 +8058,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
|
|
|
7561
8058
|
const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
|
|
7562
8059
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
|
|
7563
8060
|
if (!pattern) throw new ToolError("grep_files", "pattern is required");
|
|
7564
|
-
if (!
|
|
8061
|
+
if (!existsSync11(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
|
|
7565
8062
|
const MAX_PATTERN_LENGTH = 1e3;
|
|
7566
8063
|
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
7567
8064
|
throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
|
|
@@ -7655,11 +8152,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
|
|
|
7655
8152
|
if (paths.length >= maxFiles) return;
|
|
7656
8153
|
if (entry.isDirectory()) {
|
|
7657
8154
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
7658
|
-
walk(
|
|
8155
|
+
walk(join6(dirPath, entry.name));
|
|
7659
8156
|
} else if (entry.isFile()) {
|
|
7660
8157
|
if (isBinary(entry.name)) continue;
|
|
7661
8158
|
if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
|
|
7662
|
-
const fullPath =
|
|
8159
|
+
const fullPath = join6(dirPath, entry.name);
|
|
7663
8160
|
try {
|
|
7664
8161
|
if (statSync4(fullPath).size > 1e6) continue;
|
|
7665
8162
|
} catch {
|
|
@@ -7714,7 +8211,7 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
|
|
|
7714
8211
|
}
|
|
7715
8212
|
let content;
|
|
7716
8213
|
try {
|
|
7717
|
-
content =
|
|
8214
|
+
content = readFileSync9(fullPath, "utf-8");
|
|
7718
8215
|
} catch {
|
|
7719
8216
|
return;
|
|
7720
8217
|
}
|
|
@@ -7745,8 +8242,8 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
|
|
|
7745
8242
|
}
|
|
7746
8243
|
|
|
7747
8244
|
// src/tools/builtin/glob-files.ts
|
|
7748
|
-
import { readdirSync as readdirSync6, statSync as statSync5, existsSync as
|
|
7749
|
-
import { join as
|
|
8245
|
+
import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync12 } from "fs";
|
|
8246
|
+
import { join as join7, relative as relative2, basename as basename4 } from "path";
|
|
7750
8247
|
var globFilesTool = {
|
|
7751
8248
|
definition: {
|
|
7752
8249
|
name: "glob_files",
|
|
@@ -7779,7 +8276,7 @@ Results sorted by most recent modification time. Automatically skips node_module
|
|
|
7779
8276
|
const rootPath = String(args["path"] ?? process.cwd());
|
|
7780
8277
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
|
|
7781
8278
|
if (!pattern) throw new ToolError("glob_files", "pattern is required");
|
|
7782
|
-
if (!
|
|
8279
|
+
if (!existsSync12(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
|
|
7783
8280
|
const regex = globToRegex(pattern);
|
|
7784
8281
|
const matches = [];
|
|
7785
8282
|
collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
|
|
@@ -7856,7 +8353,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
7856
8353
|
}
|
|
7857
8354
|
for (const entry of entries) {
|
|
7858
8355
|
if (results.length >= maxResults) break;
|
|
7859
|
-
const fullPath =
|
|
8356
|
+
const fullPath = join7(dirPath, entry.name);
|
|
7860
8357
|
if (entry.isDirectory()) {
|
|
7861
8358
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
7862
8359
|
collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
|
|
@@ -7938,7 +8435,7 @@ var runInteractiveTool = {
|
|
|
7938
8435
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
7939
8436
|
};
|
|
7940
8437
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
7941
|
-
return new Promise((
|
|
8438
|
+
return new Promise((resolve9) => {
|
|
7942
8439
|
const child = spawn2(executable, cmdArgs.map(String), {
|
|
7943
8440
|
cwd: process.cwd(),
|
|
7944
8441
|
env,
|
|
@@ -7971,22 +8468,22 @@ var runInteractiveTool = {
|
|
|
7971
8468
|
setTimeout(writeNextLine, 400);
|
|
7972
8469
|
const timer = setTimeout(() => {
|
|
7973
8470
|
child.kill();
|
|
7974
|
-
|
|
8471
|
+
resolve9(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
7975
8472
|
${buildOutput(stdout, stderr)}`);
|
|
7976
8473
|
}, timeout);
|
|
7977
8474
|
child.on("close", (code) => {
|
|
7978
8475
|
clearTimeout(timer);
|
|
7979
8476
|
const output = buildOutput(stdout, stderr);
|
|
7980
8477
|
if (code !== 0 && code !== null) {
|
|
7981
|
-
|
|
8478
|
+
resolve9(`${prefixWarnings}Exit code ${code}:
|
|
7982
8479
|
${output}`);
|
|
7983
8480
|
} else {
|
|
7984
|
-
|
|
8481
|
+
resolve9(`${prefixWarnings}${output || "(no output)"}`);
|
|
7985
8482
|
}
|
|
7986
8483
|
});
|
|
7987
8484
|
child.on("error", (err) => {
|
|
7988
8485
|
clearTimeout(timer);
|
|
7989
|
-
|
|
8486
|
+
resolve9(
|
|
7990
8487
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
7991
8488
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
7992
8489
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -8596,7 +9093,7 @@ ${preamble}`;
|
|
|
8596
9093
|
}
|
|
8597
9094
|
|
|
8598
9095
|
// src/tools/builtin/save-last-response.ts
|
|
8599
|
-
import { mkdirSync as
|
|
9096
|
+
import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync3, rmdirSync as rmdirSync2 } from "fs";
|
|
8600
9097
|
import { dirname as dirname4 } from "path";
|
|
8601
9098
|
var lastResponseStore = { content: "" };
|
|
8602
9099
|
function cleanupRejectedTeeFile(filePath) {
|
|
@@ -8646,7 +9143,7 @@ Any of these triggers means use save_last_response, NOT write_file:
|
|
|
8646
9143
|
throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
|
|
8647
9144
|
}
|
|
8648
9145
|
undoStack.push(filePath, `save_last_response: ${filePath}`);
|
|
8649
|
-
|
|
9146
|
+
mkdirSync6(dirname4(filePath), { recursive: true });
|
|
8650
9147
|
atomicWriteFileSync(filePath, content);
|
|
8651
9148
|
const lines = content.split("\n").length;
|
|
8652
9149
|
return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
|
|
@@ -8654,11 +9151,11 @@ Any of these triggers means use save_last_response, NOT write_file:
|
|
|
8654
9151
|
};
|
|
8655
9152
|
|
|
8656
9153
|
// src/tools/builtin/save-memory.ts
|
|
8657
|
-
import { existsSync as
|
|
8658
|
-
import { join as
|
|
9154
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, statSync as statSync6, mkdirSync as mkdirSync7 } from "fs";
|
|
9155
|
+
import { join as join8 } from "path";
|
|
8659
9156
|
import { homedir as homedir5 } from "os";
|
|
8660
9157
|
function getMemoryFilePath() {
|
|
8661
|
-
return
|
|
9158
|
+
return join8(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
|
|
8662
9159
|
}
|
|
8663
9160
|
function formatTimestamp() {
|
|
8664
9161
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -8682,16 +9179,16 @@ var saveMemoryTool = {
|
|
|
8682
9179
|
const content = String(args["content"] ?? "").trim();
|
|
8683
9180
|
if (!content) throw new ToolError("save_memory", "content is required");
|
|
8684
9181
|
const memoryPath = getMemoryFilePath();
|
|
8685
|
-
const configDir =
|
|
8686
|
-
if (!
|
|
8687
|
-
|
|
9182
|
+
const configDir = join8(homedir5(), CONFIG_DIR_NAME);
|
|
9183
|
+
if (!existsSync13(configDir)) {
|
|
9184
|
+
mkdirSync7(configDir, { recursive: true });
|
|
8688
9185
|
}
|
|
8689
9186
|
const timestamp = formatTimestamp();
|
|
8690
9187
|
const entry = `
|
|
8691
9188
|
## ${timestamp}
|
|
8692
9189
|
${content}
|
|
8693
9190
|
`;
|
|
8694
|
-
const previous =
|
|
9191
|
+
const previous = existsSync13(memoryPath) ? readFileSync10(memoryPath, "utf-8") : "";
|
|
8695
9192
|
atomicWriteFileSync(memoryPath, previous + entry);
|
|
8696
9193
|
const byteSize = statSync6(memoryPath).size;
|
|
8697
9194
|
return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
|
|
@@ -8738,7 +9235,7 @@ function promptUser(rl, question) {
|
|
|
8738
9235
|
console.log();
|
|
8739
9236
|
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
8740
9237
|
process.stdout.write(chalk4.cyan("> "));
|
|
8741
|
-
return new Promise((
|
|
9238
|
+
return new Promise((resolve9) => {
|
|
8742
9239
|
let completed = false;
|
|
8743
9240
|
const cleanup = (answer) => {
|
|
8744
9241
|
if (completed) return;
|
|
@@ -8748,7 +9245,7 @@ function promptUser(rl, question) {
|
|
|
8748
9245
|
rl.pause();
|
|
8749
9246
|
rlAny.output = savedOutput;
|
|
8750
9247
|
askUserContext.prompting = false;
|
|
8751
|
-
|
|
9248
|
+
resolve9(answer);
|
|
8752
9249
|
};
|
|
8753
9250
|
const onLine = (line) => {
|
|
8754
9251
|
cleanup(line);
|
|
@@ -9228,11 +9725,27 @@ var spawnAgentTool = {
|
|
|
9228
9725
|
if (!ctx.provider) {
|
|
9229
9726
|
throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
|
|
9230
9727
|
}
|
|
9728
|
+
const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
|
|
9729
|
+
const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
|
|
9730
|
+
const taskCount = singleTask ? 1 : tasksArr.length;
|
|
9731
|
+
const hookPayload = {
|
|
9732
|
+
task: singleTask || void 0,
|
|
9733
|
+
tasks: tasksArr.length > 0 ? tasksArr : void 0,
|
|
9734
|
+
taskCount,
|
|
9735
|
+
maxRounds
|
|
9736
|
+
};
|
|
9737
|
+
const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
|
|
9738
|
+
const startDeny = startDecisions.find((d) => d.action === "deny");
|
|
9739
|
+
if (startDeny) {
|
|
9740
|
+
throw new ToolError("spawn_agent", "SubagentStart hook denied: " + (startDeny.reason ?? "denied"));
|
|
9741
|
+
}
|
|
9742
|
+
let subagentStatus = "error";
|
|
9231
9743
|
const guardWasActive = subAgentGuard.active;
|
|
9232
9744
|
subAgentGuard.active = true;
|
|
9233
9745
|
try {
|
|
9234
9746
|
if (singleTask) {
|
|
9235
9747
|
const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
|
|
9748
|
+
subagentStatus = "ok";
|
|
9236
9749
|
return [
|
|
9237
9750
|
"## Sub-Agent Result",
|
|
9238
9751
|
"",
|
|
@@ -9261,8 +9774,10 @@ var spawnAgentTool = {
|
|
|
9261
9774
|
});
|
|
9262
9775
|
lines.push("---");
|
|
9263
9776
|
lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
|
|
9777
|
+
subagentStatus = "ok";
|
|
9264
9778
|
return lines.join("\n");
|
|
9265
9779
|
} finally {
|
|
9780
|
+
runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
|
|
9266
9781
|
subAgentGuard.active = guardWasActive;
|
|
9267
9782
|
}
|
|
9268
9783
|
}
|
|
@@ -9485,14 +10000,14 @@ var taskStopTool = {
|
|
|
9485
10000
|
|
|
9486
10001
|
// src/tools/builtin/git-tools.ts
|
|
9487
10002
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
9488
|
-
import { existsSync as
|
|
9489
|
-
import { join as
|
|
10003
|
+
import { existsSync as existsSync14 } from "fs";
|
|
10004
|
+
import { join as join9 } from "path";
|
|
9490
10005
|
function assertGitRepo(cwd) {
|
|
9491
10006
|
let dir = cwd;
|
|
9492
10007
|
const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
|
|
9493
10008
|
while (dir && dir !== root) {
|
|
9494
|
-
if (
|
|
9495
|
-
const parent =
|
|
10009
|
+
if (existsSync14(join9(dir, ".git"))) return;
|
|
10010
|
+
const parent = join9(dir, "..");
|
|
9496
10011
|
if (parent === dir) break;
|
|
9497
10012
|
dir = parent;
|
|
9498
10013
|
}
|
|
@@ -9757,9 +10272,9 @@ ${commitOutput.trim()}`;
|
|
|
9757
10272
|
};
|
|
9758
10273
|
|
|
9759
10274
|
// src/tools/builtin/notebook-edit.ts
|
|
9760
|
-
import { readFileSync as
|
|
10275
|
+
import { readFileSync as readFileSync11, existsSync as existsSync15 } from "fs";
|
|
9761
10276
|
import { writeFile } from "fs/promises";
|
|
9762
|
-
import { resolve as
|
|
10277
|
+
import { resolve as resolve5, extname as extname2 } from "path";
|
|
9763
10278
|
var notebookEditTool = {
|
|
9764
10279
|
definition: {
|
|
9765
10280
|
name: "notebook_edit",
|
|
@@ -9808,14 +10323,14 @@ var notebookEditTool = {
|
|
|
9808
10323
|
if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
|
|
9809
10324
|
throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
|
|
9810
10325
|
}
|
|
9811
|
-
const absPath =
|
|
10326
|
+
const absPath = resolve5(filePath);
|
|
9812
10327
|
if (extname2(absPath).toLowerCase() !== ".ipynb") {
|
|
9813
10328
|
throw new ToolError("notebook_edit", "path must point to a .ipynb file");
|
|
9814
10329
|
}
|
|
9815
|
-
if (!
|
|
10330
|
+
if (!existsSync15(absPath)) {
|
|
9816
10331
|
throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
|
|
9817
10332
|
}
|
|
9818
|
-
const raw =
|
|
10333
|
+
const raw = readFileSync11(absPath, "utf-8");
|
|
9819
10334
|
const nb = parseNotebook(raw);
|
|
9820
10335
|
const cellIdx0 = cellIndexRaw - 1;
|
|
9821
10336
|
undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
|
|
@@ -10232,8 +10747,8 @@ function estimateToolDefinitionTokens(def) {
|
|
|
10232
10747
|
|
|
10233
10748
|
// src/tools/registry.ts
|
|
10234
10749
|
import { pathToFileURL } from "url";
|
|
10235
|
-
import { existsSync as
|
|
10236
|
-
import { join as
|
|
10750
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync7 } from "fs";
|
|
10751
|
+
import { join as join10 } from "path";
|
|
10237
10752
|
var ToolRegistry = class {
|
|
10238
10753
|
tools = /* @__PURE__ */ new Map();
|
|
10239
10754
|
pluginToolNames = /* @__PURE__ */ new Set();
|
|
@@ -10394,9 +10909,9 @@ var ToolRegistry = class {
|
|
|
10394
10909
|
* Returns the number of successfully loaded plugins.
|
|
10395
10910
|
*/
|
|
10396
10911
|
async loadPlugins(pluginsDir, allowPlugins = false) {
|
|
10397
|
-
if (!
|
|
10912
|
+
if (!existsSync16(pluginsDir)) {
|
|
10398
10913
|
try {
|
|
10399
|
-
|
|
10914
|
+
mkdirSync8(pluginsDir, { recursive: true });
|
|
10400
10915
|
} catch {
|
|
10401
10916
|
}
|
|
10402
10917
|
return 0;
|
|
@@ -10420,12 +10935,12 @@ var ToolRegistry = class {
|
|
|
10420
10935
|
process.stderr.write(
|
|
10421
10936
|
`
|
|
10422
10937
|
[plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
|
|
10423
|
-
` + files.map((f) => ` + ${
|
|
10938
|
+
` + files.map((f) => ` + ${join10(pluginsDir, f)}`).join("\n") + "\n\n"
|
|
10424
10939
|
);
|
|
10425
10940
|
let loaded = 0;
|
|
10426
10941
|
for (const file of files) {
|
|
10427
10942
|
try {
|
|
10428
|
-
const fileUrl = pathToFileURL(
|
|
10943
|
+
const fileUrl = pathToFileURL(join10(pluginsDir, file)).href;
|
|
10429
10944
|
const mod = await import(fileUrl);
|
|
10430
10945
|
const tool = mod.tool ?? mod.default?.tool ?? mod.default;
|
|
10431
10946
|
if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
|
|
@@ -10587,7 +11102,7 @@ var McpClient = class {
|
|
|
10587
11102
|
// 内部方法:JSON-RPC 通信
|
|
10588
11103
|
// ══════════════════════════════════════════════════════════════════
|
|
10589
11104
|
sendRequest(method, params) {
|
|
10590
|
-
return new Promise((
|
|
11105
|
+
return new Promise((resolve9, reject) => {
|
|
10591
11106
|
if (!this.process?.stdin?.writable) {
|
|
10592
11107
|
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
10593
11108
|
}
|
|
@@ -10611,7 +11126,7 @@ var McpClient = class {
|
|
|
10611
11126
|
this.pendingRequests.set(id, {
|
|
10612
11127
|
resolve: (result) => {
|
|
10613
11128
|
cleanup();
|
|
10614
|
-
|
|
11129
|
+
resolve9(result);
|
|
10615
11130
|
},
|
|
10616
11131
|
reject: (error) => {
|
|
10617
11132
|
cleanup();
|
|
@@ -10688,13 +11203,13 @@ var McpClient = class {
|
|
|
10688
11203
|
}
|
|
10689
11204
|
/** Promise 超时包装 */
|
|
10690
11205
|
withTimeout(promise, ms, label) {
|
|
10691
|
-
return new Promise((
|
|
11206
|
+
return new Promise((resolve9, reject) => {
|
|
10692
11207
|
const timer = setTimeout(() => {
|
|
10693
11208
|
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
10694
11209
|
}, ms);
|
|
10695
11210
|
promise.then((val) => {
|
|
10696
11211
|
clearTimeout(timer);
|
|
10697
|
-
|
|
11212
|
+
resolve9(val);
|
|
10698
11213
|
}).catch((err) => {
|
|
10699
11214
|
clearTimeout(timer);
|
|
10700
11215
|
reject(err);
|
|
@@ -10971,11 +11486,11 @@ var McpManager = class {
|
|
|
10971
11486
|
};
|
|
10972
11487
|
|
|
10973
11488
|
// src/skills/manager.ts
|
|
10974
|
-
import { existsSync as
|
|
10975
|
-
import { join as
|
|
11489
|
+
import { existsSync as existsSync17, readdirSync as readdirSync8, mkdirSync as mkdirSync9, statSync as statSync7 } from "fs";
|
|
11490
|
+
import { join as join11 } from "path";
|
|
10976
11491
|
|
|
10977
11492
|
// src/skills/types.ts
|
|
10978
|
-
import { readFileSync as
|
|
11493
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
10979
11494
|
import { basename as basename5 } from "path";
|
|
10980
11495
|
function parseSimpleYaml(yaml) {
|
|
10981
11496
|
const result = {};
|
|
@@ -10997,7 +11512,7 @@ function parseYamlArray(value) {
|
|
|
10997
11512
|
function parseSkillFile(filePath) {
|
|
10998
11513
|
let raw;
|
|
10999
11514
|
try {
|
|
11000
|
-
raw =
|
|
11515
|
+
raw = readFileSync12(filePath, "utf-8");
|
|
11001
11516
|
} catch {
|
|
11002
11517
|
return null;
|
|
11003
11518
|
}
|
|
@@ -11040,9 +11555,9 @@ var SkillManager = class {
|
|
|
11040
11555
|
/** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
|
|
11041
11556
|
loadSkills() {
|
|
11042
11557
|
this.skills.clear();
|
|
11043
|
-
if (!
|
|
11558
|
+
if (!existsSync17(this.skillsDir)) {
|
|
11044
11559
|
try {
|
|
11045
|
-
|
|
11560
|
+
mkdirSync9(this.skillsDir, { recursive: true });
|
|
11046
11561
|
} catch {
|
|
11047
11562
|
}
|
|
11048
11563
|
return 0;
|
|
@@ -11055,14 +11570,14 @@ var SkillManager = class {
|
|
|
11055
11570
|
}
|
|
11056
11571
|
for (const entry of entries) {
|
|
11057
11572
|
let filePath;
|
|
11058
|
-
const fullPath =
|
|
11573
|
+
const fullPath = join11(this.skillsDir, entry);
|
|
11059
11574
|
if (entry.endsWith(".md")) {
|
|
11060
11575
|
filePath = fullPath;
|
|
11061
11576
|
} else {
|
|
11062
11577
|
try {
|
|
11063
11578
|
if (statSync7(fullPath).isDirectory()) {
|
|
11064
|
-
const skillMd =
|
|
11065
|
-
if (
|
|
11579
|
+
const skillMd = join11(fullPath, "SKILL.md");
|
|
11580
|
+
if (existsSync17(skillMd)) {
|
|
11066
11581
|
filePath = skillMd;
|
|
11067
11582
|
} else {
|
|
11068
11583
|
continue;
|
|
@@ -11126,7 +11641,8 @@ var SkillManager = class {
|
|
|
11126
11641
|
|
|
11127
11642
|
// src/web/tool-executor-web.ts
|
|
11128
11643
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
11129
|
-
import {
|
|
11644
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
11645
|
+
import { existsSync as existsSync18, readFileSync as readFileSync13 } from "fs";
|
|
11130
11646
|
var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
11131
11647
|
constructor(registry, ws) {
|
|
11132
11648
|
this.registry = registry;
|
|
@@ -11137,6 +11653,12 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11137
11653
|
hookConfig;
|
|
11138
11654
|
permissionRules = [];
|
|
11139
11655
|
defaultPermission = "confirm";
|
|
11656
|
+
permissionProfileName = "legacy";
|
|
11657
|
+
permissionProfile;
|
|
11658
|
+
allowedPermissionProfiles = [];
|
|
11659
|
+
workspaceRoot = process.cwd();
|
|
11660
|
+
networkPolicy;
|
|
11661
|
+
hookConfigDir = process.cwd();
|
|
11140
11662
|
/** Pending confirm promises keyed by requestId */
|
|
11141
11663
|
pendingConfirms = /* @__PURE__ */ new Map();
|
|
11142
11664
|
/** Pending batch confirm promises */
|
|
@@ -11171,6 +11693,12 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11171
11693
|
this.hookConfig = opts.hookConfig;
|
|
11172
11694
|
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
11173
11695
|
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
11696
|
+
if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
|
|
11697
|
+
if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
|
|
11698
|
+
if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
|
|
11699
|
+
if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
|
|
11700
|
+
if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
|
|
11701
|
+
if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
|
|
11174
11702
|
}
|
|
11175
11703
|
/** Clear M7 timeout timer for a requestId */
|
|
11176
11704
|
clearPendingTimer(requestId) {
|
|
@@ -11182,33 +11710,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11182
11710
|
}
|
|
11183
11711
|
/** Resolve a pending confirm from client response */
|
|
11184
11712
|
resolveConfirm(requestId, approved) {
|
|
11185
|
-
const
|
|
11186
|
-
if (
|
|
11713
|
+
const resolve9 = this.pendingConfirms.get(requestId);
|
|
11714
|
+
if (resolve9) {
|
|
11187
11715
|
this.clearPendingTimer(requestId);
|
|
11188
11716
|
this.pendingConfirms.delete(requestId);
|
|
11189
11717
|
this.confirming = false;
|
|
11190
|
-
|
|
11718
|
+
resolve9(approved);
|
|
11191
11719
|
}
|
|
11192
11720
|
}
|
|
11193
11721
|
/** Resolve a pending batch confirm from client response */
|
|
11194
11722
|
resolveBatchConfirm(requestId, decision) {
|
|
11195
|
-
const
|
|
11196
|
-
if (
|
|
11723
|
+
const resolve9 = this.pendingBatchConfirms.get(requestId);
|
|
11724
|
+
if (resolve9) {
|
|
11197
11725
|
this.clearPendingTimer(requestId);
|
|
11198
11726
|
this.pendingBatchConfirms.delete(requestId);
|
|
11199
11727
|
this.confirming = false;
|
|
11200
11728
|
if (decision === "all" || decision === "none") {
|
|
11201
|
-
|
|
11729
|
+
resolve9(decision);
|
|
11202
11730
|
} else {
|
|
11203
|
-
|
|
11731
|
+
resolve9(new Set(decision));
|
|
11204
11732
|
}
|
|
11205
11733
|
}
|
|
11206
11734
|
}
|
|
11207
11735
|
/** Cancel all pending confirms (e.g., on disconnect) */
|
|
11208
11736
|
cancelAll() {
|
|
11209
|
-
for (const
|
|
11737
|
+
for (const resolve9 of this.pendingConfirms.values()) resolve9(false);
|
|
11210
11738
|
this.pendingConfirms.clear();
|
|
11211
|
-
for (const
|
|
11739
|
+
for (const resolve9 of this.pendingBatchConfirms.values()) resolve9("none");
|
|
11212
11740
|
this.pendingBatchConfirms.clear();
|
|
11213
11741
|
this.confirming = false;
|
|
11214
11742
|
}
|
|
@@ -11251,9 +11779,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11251
11779
|
if (call.name === "write_file") {
|
|
11252
11780
|
const filePath = String(call.arguments["path"] ?? "");
|
|
11253
11781
|
const newContent = String(call.arguments["content"] ?? "");
|
|
11254
|
-
if (filePath &&
|
|
11782
|
+
if (filePath && existsSync18(filePath)) {
|
|
11255
11783
|
try {
|
|
11256
|
-
const old =
|
|
11784
|
+
const old = readFileSync13(filePath, "utf-8");
|
|
11257
11785
|
return renderDiff(old, newContent, { filePath });
|
|
11258
11786
|
} catch {
|
|
11259
11787
|
}
|
|
@@ -11284,8 +11812,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11284
11812
|
diff: this.getDiffPreview(call)
|
|
11285
11813
|
};
|
|
11286
11814
|
this.send(msg);
|
|
11287
|
-
return new Promise((
|
|
11288
|
-
this.pendingConfirms.set(requestId,
|
|
11815
|
+
return new Promise((resolve9) => {
|
|
11816
|
+
this.pendingConfirms.set(requestId, resolve9);
|
|
11289
11817
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
11290
11818
|
if (this.pendingConfirms.has(requestId)) {
|
|
11291
11819
|
this.resolveConfirm(requestId, false);
|
|
@@ -11309,8 +11837,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11309
11837
|
files
|
|
11310
11838
|
};
|
|
11311
11839
|
this.send(msg);
|
|
11312
|
-
return new Promise((
|
|
11313
|
-
this.pendingBatchConfirms.set(requestId,
|
|
11840
|
+
return new Promise((resolve9) => {
|
|
11841
|
+
this.pendingBatchConfirms.set(requestId, resolve9);
|
|
11314
11842
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
11315
11843
|
if (this.pendingBatchConfirms.has(requestId)) {
|
|
11316
11844
|
this.resolveBatchConfirm(requestId, "none");
|
|
@@ -11327,30 +11855,96 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11327
11855
|
return { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
|
|
11328
11856
|
}
|
|
11329
11857
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
11858
|
+
let toolCallStarted = false;
|
|
11330
11859
|
runHook(this.hookConfig?.preToolExecution, {
|
|
11331
11860
|
tool: call.name,
|
|
11332
11861
|
dangerLevel,
|
|
11333
11862
|
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
11334
11863
|
});
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11864
|
+
const preToolDecisions = runLifecycleHooks(
|
|
11865
|
+
this.hookConfig,
|
|
11866
|
+
"PreToolUse",
|
|
11867
|
+
{ tool: call.name, dangerLevel, args: call.arguments },
|
|
11868
|
+
{ configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: `[hook] ${message}` }) }
|
|
11869
|
+
);
|
|
11870
|
+
const preDeny = preToolDecisions.find((d) => d.action === "deny");
|
|
11871
|
+
if (preDeny) {
|
|
11872
|
+
const result = {
|
|
11873
|
+
callId: call.id,
|
|
11874
|
+
content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
|
|
11875
|
+
isError: true
|
|
11876
|
+
};
|
|
11877
|
+
this.sendToolCallResult(call, result.content, true);
|
|
11878
|
+
return result;
|
|
11879
|
+
}
|
|
11880
|
+
for (const d of preToolDecisions) {
|
|
11881
|
+
const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
|
|
11882
|
+
for (const warning of warnings) this.send({ type: "info", message: `[hook] ${warning}` });
|
|
11883
|
+
}
|
|
11884
|
+
const permission = checkPermissionWithProfile(
|
|
11885
|
+
call.name,
|
|
11886
|
+
call.arguments,
|
|
11887
|
+
dangerLevel,
|
|
11888
|
+
this.permissionRules,
|
|
11889
|
+
this.defaultPermission,
|
|
11890
|
+
{
|
|
11891
|
+
profileName: this.permissionProfileName,
|
|
11892
|
+
profile: this.permissionProfile,
|
|
11893
|
+
allowedProfiles: this.allowedPermissionProfiles,
|
|
11894
|
+
workspaceRoot: this.workspaceRoot,
|
|
11895
|
+
tempDirs: [tmpdir2()]
|
|
11339
11896
|
}
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11897
|
+
);
|
|
11898
|
+
const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
|
|
11899
|
+
if (networkPermission?.action === "confirm" || permission.action === "confirm") {
|
|
11900
|
+
runLifecycleHooks(
|
|
11901
|
+
this.hookConfig,
|
|
11902
|
+
"PermissionRequest",
|
|
11903
|
+
{ tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
|
|
11904
|
+
{ configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
|
|
11905
|
+
);
|
|
11906
|
+
}
|
|
11907
|
+
if (networkPermission?.action === "deny") {
|
|
11908
|
+
const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
|
|
11909
|
+
return {
|
|
11910
|
+
callId: call.id,
|
|
11911
|
+
content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
|
|
11912
|
+
isError: true
|
|
11913
|
+
};
|
|
11914
|
+
}
|
|
11915
|
+
if (permission.action === "deny") {
|
|
11916
|
+
const reason = permission.reason ? ` (${permission.reason})` : "";
|
|
11917
|
+
return {
|
|
11918
|
+
callId: call.id,
|
|
11919
|
+
content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
|
|
11920
|
+
isError: true
|
|
11921
|
+
};
|
|
11922
|
+
}
|
|
11923
|
+
if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
|
|
11924
|
+
this.sendToolCallStart(call);
|
|
11925
|
+
try {
|
|
11926
|
+
const rawContent = await runTool(tool, call.arguments, call.name);
|
|
11927
|
+
const content = truncateOutput(rawContent, call.name);
|
|
11928
|
+
this.sendToolCallResult(call, rawContent, false);
|
|
11929
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
11930
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
11931
|
+
return { callId: call.id, content, isError: false };
|
|
11932
|
+
} catch (err) {
|
|
11933
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11934
|
+
this.sendToolCallResult(call, message, true);
|
|
11935
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
11936
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
11937
|
+
return { callId: call.id, content: message, isError: true };
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
|
|
11941
|
+
this.sendToolCallStart(call);
|
|
11942
|
+
toolCallStarted = true;
|
|
11943
|
+
const confirmed = await this.confirm(call, "write");
|
|
11944
|
+
if (!confirmed) {
|
|
11945
|
+
const rejectionMsg = `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`;
|
|
11946
|
+
this.sendToolCallResult(call, rejectionMsg, true);
|
|
11947
|
+
return { callId: call.id, content: rejectionMsg, isError: true };
|
|
11354
11948
|
}
|
|
11355
11949
|
}
|
|
11356
11950
|
this.sendToolCallStart(call);
|
|
@@ -11360,11 +11954,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11360
11954
|
const content = truncateOutput(rawContent, call.name);
|
|
11361
11955
|
this.sendToolCallResult(call, rawContent, false);
|
|
11362
11956
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
11957
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
11363
11958
|
return { callId: call.id, content, isError: false };
|
|
11364
11959
|
} catch (err) {
|
|
11365
11960
|
const message = err instanceof Error ? err.message : String(err);
|
|
11366
11961
|
this.sendToolCallResult(call, message, true);
|
|
11367
11962
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
11963
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
11368
11964
|
return { callId: call.id, content: message, isError: true };
|
|
11369
11965
|
}
|
|
11370
11966
|
}
|
|
@@ -11382,11 +11978,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11382
11978
|
const content = truncateOutput(rawContent, call.name);
|
|
11383
11979
|
this.sendToolCallResult(call, rawContent, false);
|
|
11384
11980
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
11981
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
11385
11982
|
return { callId: call.id, content, isError: false };
|
|
11386
11983
|
} catch (err) {
|
|
11387
11984
|
const message = err instanceof Error ? err.message : String(err);
|
|
11388
11985
|
this.sendToolCallResult(call, message, true);
|
|
11389
11986
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
11987
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
11390
11988
|
return { callId: call.id, content: message, isError: true };
|
|
11391
11989
|
}
|
|
11392
11990
|
}
|
|
@@ -11409,6 +12007,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11409
12007
|
return results;
|
|
11410
12008
|
}
|
|
11411
12009
|
async executeBatchFileWrites(items) {
|
|
12010
|
+
if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
|
|
12011
|
+
return Promise.all(items.map(({ call }) => this.execute(call)));
|
|
12012
|
+
}
|
|
11412
12013
|
const calls = items.map((i) => i.call);
|
|
11413
12014
|
const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
|
|
11414
12015
|
const results = new Array(calls.length);
|
|
@@ -11454,20 +12055,20 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
11454
12055
|
};
|
|
11455
12056
|
|
|
11456
12057
|
// src/core/system-prompt-builder.ts
|
|
11457
|
-
import { existsSync as
|
|
11458
|
-
import { join as
|
|
12058
|
+
import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
|
|
12059
|
+
import { join as join13 } from "path";
|
|
11459
12060
|
|
|
11460
12061
|
// src/repl/dev-state.ts
|
|
11461
|
-
import { existsSync as
|
|
11462
|
-
import { join as
|
|
12062
|
+
import { existsSync as existsSync19, readFileSync as readFileSync14, unlinkSync as unlinkSync4, mkdirSync as mkdirSync10 } from "fs";
|
|
12063
|
+
import { join as join12 } from "path";
|
|
11463
12064
|
import { homedir as homedir6 } from "os";
|
|
11464
12065
|
function getDevStatePath() {
|
|
11465
|
-
return
|
|
12066
|
+
return join12(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
|
|
11466
12067
|
}
|
|
11467
12068
|
function loadDevState() {
|
|
11468
12069
|
const path3 = getDevStatePath();
|
|
11469
|
-
if (!
|
|
11470
|
-
const content =
|
|
12070
|
+
if (!existsSync19(path3)) return null;
|
|
12071
|
+
const content = readFileSync14(path3, "utf-8").trim();
|
|
11471
12072
|
return content || null;
|
|
11472
12073
|
}
|
|
11473
12074
|
|
|
@@ -11525,9 +12126,9 @@ ${ctx.activeSkill.content}`);
|
|
|
11525
12126
|
return { stable: stableParts.join("\n\n---\n\n"), volatile };
|
|
11526
12127
|
}
|
|
11527
12128
|
function loadMemoryContent(configDir) {
|
|
11528
|
-
const memoryPath =
|
|
11529
|
-
if (!
|
|
11530
|
-
let content =
|
|
12129
|
+
const memoryPath = join13(configDir, MEMORY_FILE_NAME);
|
|
12130
|
+
if (!existsSync20(memoryPath)) return null;
|
|
12131
|
+
let content = readFileSync15(memoryPath, "utf-8").trim();
|
|
11531
12132
|
if (!content) return null;
|
|
11532
12133
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
11533
12134
|
content = content.slice(-MEMORY_MAX_CHARS);
|
|
@@ -11736,8 +12337,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
|
|
|
11736
12337
|
}
|
|
11737
12338
|
|
|
11738
12339
|
// src/core/context-files.ts
|
|
11739
|
-
import { existsSync as
|
|
11740
|
-
import { join as
|
|
12340
|
+
import { existsSync as existsSync21, readFileSync as readFileSync16 } from "fs";
|
|
12341
|
+
import { join as join14, relative as relative3, resolve as resolve6 } from "path";
|
|
11741
12342
|
function uniqueNonEmpty(values) {
|
|
11742
12343
|
const seen = /* @__PURE__ */ new Set();
|
|
11743
12344
|
const out = [];
|
|
@@ -11760,14 +12361,14 @@ function displayPath(filePath, cwd) {
|
|
|
11760
12361
|
return filePath;
|
|
11761
12362
|
}
|
|
11762
12363
|
function isInsideOrEqual(parent, child) {
|
|
11763
|
-
const rel = relative3(
|
|
12364
|
+
const rel = relative3(resolve6(parent), resolve6(child));
|
|
11764
12365
|
return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
|
|
11765
12366
|
}
|
|
11766
12367
|
function readContextFile(level, filePath, cwd, maxBytes) {
|
|
11767
12368
|
const name = filePath.split(/[\\/]/).pop() ?? filePath;
|
|
11768
12369
|
const shown = displayPath(filePath, cwd);
|
|
11769
12370
|
try {
|
|
11770
|
-
const raw =
|
|
12371
|
+
const raw = readFileSync16(filePath);
|
|
11771
12372
|
const byteCount = raw.byteLength;
|
|
11772
12373
|
if (byteCount === 0) {
|
|
11773
12374
|
return {
|
|
@@ -11814,8 +12415,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
|
|
|
11814
12415
|
function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
|
|
11815
12416
|
const skipped = [];
|
|
11816
12417
|
for (const candidate of candidates) {
|
|
11817
|
-
const filePath =
|
|
11818
|
-
if (!
|
|
12418
|
+
const filePath = join14(dir, candidate);
|
|
12419
|
+
if (!existsSync21(filePath)) continue;
|
|
11819
12420
|
const result = readContextFile(level, filePath, cwd, maxBytes);
|
|
11820
12421
|
if (result.skipped) skipped.push(result.skipped);
|
|
11821
12422
|
if (result.layer) return { layer: result.layer, skipped };
|
|
@@ -11823,7 +12424,7 @@ function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
|
|
|
11823
12424
|
return { layer: null, skipped };
|
|
11824
12425
|
}
|
|
11825
12426
|
function loadContextFiles(options) {
|
|
11826
|
-
const cwd =
|
|
12427
|
+
const cwd = resolve6(options.cwd);
|
|
11827
12428
|
const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
|
|
11828
12429
|
const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
|
|
11829
12430
|
const skipped = [];
|
|
@@ -11831,7 +12432,7 @@ function loadContextFiles(options) {
|
|
|
11831
12432
|
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
11832
12433
|
}
|
|
11833
12434
|
if (options.setting && options.setting !== "auto") {
|
|
11834
|
-
const filePath =
|
|
12435
|
+
const filePath = resolve6(cwd, String(options.setting));
|
|
11835
12436
|
if (!isInsideOrEqual(cwd, filePath)) {
|
|
11836
12437
|
skipped.push({
|
|
11837
12438
|
level: "single",
|
|
@@ -11842,7 +12443,7 @@ function loadContextFiles(options) {
|
|
|
11842
12443
|
});
|
|
11843
12444
|
return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
|
|
11844
12445
|
}
|
|
11845
|
-
if (!
|
|
12446
|
+
if (!existsSync21(filePath)) {
|
|
11846
12447
|
skipped.push({
|
|
11847
12448
|
level: "single",
|
|
11848
12449
|
filePath,
|
|
@@ -11875,7 +12476,7 @@ function loadContextFiles(options) {
|
|
|
11875
12476
|
skipped.push(...result.skipped);
|
|
11876
12477
|
if (result.layer) layers.push(result.layer);
|
|
11877
12478
|
}
|
|
11878
|
-
if (
|
|
12479
|
+
if (resolve6(options.cwd) !== resolve6(options.projectRoot)) {
|
|
11879
12480
|
const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
|
|
11880
12481
|
skipped.push(...result.skipped);
|
|
11881
12482
|
if (result.layer) layers.push(result.layer);
|
|
@@ -11893,13 +12494,13 @@ function loadContextFiles(options) {
|
|
|
11893
12494
|
}
|
|
11894
12495
|
|
|
11895
12496
|
// src/web/session-handler.ts
|
|
11896
|
-
import { existsSync as
|
|
11897
|
-
import { join as
|
|
12497
|
+
import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync2, mkdirSync as mkdirSync11, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
|
|
12498
|
+
import { join as join18, resolve as resolve7, dirname as dirname5 } from "path";
|
|
11898
12499
|
|
|
11899
12500
|
// src/tools/git-context.ts
|
|
11900
12501
|
import { execSync as execSync2 } from "child_process";
|
|
11901
|
-
import { existsSync as
|
|
11902
|
-
import { join as
|
|
12502
|
+
import { existsSync as existsSync22 } from "fs";
|
|
12503
|
+
import { join as join15 } from "path";
|
|
11903
12504
|
function runGit2(cmd, cwd) {
|
|
11904
12505
|
try {
|
|
11905
12506
|
return execSync2(`git ${cmd}`, {
|
|
@@ -11916,7 +12517,7 @@ function getGitRoot(cwd = process.cwd()) {
|
|
|
11916
12517
|
return runGit2("rev-parse --show-toplevel", cwd);
|
|
11917
12518
|
}
|
|
11918
12519
|
function getGitContext(cwd = process.cwd()) {
|
|
11919
|
-
if (!
|
|
12520
|
+
if (!existsSync22(join15(cwd, ".git"))) {
|
|
11920
12521
|
const result = runGit2("rev-parse --git-dir", cwd);
|
|
11921
12522
|
if (!result) return null;
|
|
11922
12523
|
}
|
|
@@ -12045,8 +12646,8 @@ If no security issues found, state "\u2705 No security vulnerabilities detected"
|
|
|
12045
12646
|
}
|
|
12046
12647
|
|
|
12047
12648
|
// src/repl/commands/project-init.ts
|
|
12048
|
-
import { existsSync as
|
|
12049
|
-
import { join as
|
|
12649
|
+
import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
|
|
12650
|
+
import { join as join16 } from "path";
|
|
12050
12651
|
var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
12051
12652
|
"node_modules",
|
|
12052
12653
|
".git",
|
|
@@ -12090,11 +12691,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
|
12090
12691
|
const sorted = filtered.sort((a, b) => {
|
|
12091
12692
|
let aIsDir = false, bIsDir = false;
|
|
12092
12693
|
try {
|
|
12093
|
-
aIsDir = statSync8(
|
|
12694
|
+
aIsDir = statSync8(join16(d, a)).isDirectory();
|
|
12094
12695
|
} catch {
|
|
12095
12696
|
}
|
|
12096
12697
|
try {
|
|
12097
|
-
bIsDir = statSync8(
|
|
12698
|
+
bIsDir = statSync8(join16(d, b)).isDirectory();
|
|
12098
12699
|
} catch {
|
|
12099
12700
|
}
|
|
12100
12701
|
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
@@ -12102,7 +12703,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
|
12102
12703
|
});
|
|
12103
12704
|
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
12104
12705
|
const name = sorted[i];
|
|
12105
|
-
const fullPath =
|
|
12706
|
+
const fullPath = join16(d, name);
|
|
12106
12707
|
const isLast = i === sorted.length - 1;
|
|
12107
12708
|
const connector = isLast ? "+-- " : "|-- ";
|
|
12108
12709
|
let isDir;
|
|
@@ -12129,7 +12730,7 @@ function scanProject(cwd) {
|
|
|
12129
12730
|
configFiles: [],
|
|
12130
12731
|
directoryStructure: ""
|
|
12131
12732
|
};
|
|
12132
|
-
const check = (file) =>
|
|
12733
|
+
const check = (file) => existsSync23(join16(cwd, file));
|
|
12133
12734
|
const configCandidates = [
|
|
12134
12735
|
"package.json",
|
|
12135
12736
|
"tsconfig.json",
|
|
@@ -12156,7 +12757,7 @@ function scanProject(cwd) {
|
|
|
12156
12757
|
info.type = "node";
|
|
12157
12758
|
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
12158
12759
|
try {
|
|
12159
|
-
const pkg = JSON.parse(
|
|
12760
|
+
const pkg = JSON.parse(readFileSync17(join16(cwd, "package.json"), "utf-8"));
|
|
12160
12761
|
const scripts = pkg.scripts ?? {};
|
|
12161
12762
|
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
12162
12763
|
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
@@ -12816,7 +13417,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
|
|
|
12816
13417
|
}
|
|
12817
13418
|
|
|
12818
13419
|
// src/hub/persist.ts
|
|
12819
|
-
import { join as
|
|
13420
|
+
import { join as join17 } from "path";
|
|
12820
13421
|
function discussionToMessages(state2) {
|
|
12821
13422
|
const out = [];
|
|
12822
13423
|
const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
|
|
@@ -12854,7 +13455,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
|
|
|
12854
13455
|
session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
|
|
12855
13456
|
session.titleAiGenerated = true;
|
|
12856
13457
|
await sm.save();
|
|
12857
|
-
return { id: session.id, path:
|
|
13458
|
+
return { id: session.id, path: join17(config.getHistoryDir(), `${session.id}.json`) };
|
|
12858
13459
|
}
|
|
12859
13460
|
|
|
12860
13461
|
// src/web/session-handler.ts
|
|
@@ -12953,7 +13554,28 @@ var SessionHandler = class {
|
|
|
12953
13554
|
const hooks = this.config.get("hooks");
|
|
12954
13555
|
const permissionRules = this.config.get("permissionRules");
|
|
12955
13556
|
const defaultPermission = this.config.get("defaultPermission");
|
|
12956
|
-
this.
|
|
13557
|
+
const permissionProfileName = this.config.get("defaultPermissionProfile") ?? "legacy";
|
|
13558
|
+
const permissionProfiles = this.config.get("permissionProfiles") ?? {};
|
|
13559
|
+
this.toolExecutor.setConfig({
|
|
13560
|
+
hookConfig: hooks,
|
|
13561
|
+
hookConfigDir: this.config.getConfigDir(),
|
|
13562
|
+
permissionRules,
|
|
13563
|
+
defaultPermission,
|
|
13564
|
+
permissionProfileName,
|
|
13565
|
+
permissionProfile: permissionProfiles[permissionProfileName],
|
|
13566
|
+
allowedPermissionProfiles: this.config.get("allowedPermissionProfiles") ?? [],
|
|
13567
|
+
workspaceRoot: process.cwd(),
|
|
13568
|
+
networkPolicy: this.config.get("networkPolicy")
|
|
13569
|
+
});
|
|
13570
|
+
const permissionProfileWarning = formatPermissionProfileWarning(permissionProfileName);
|
|
13571
|
+
if (permissionProfileWarning) {
|
|
13572
|
+
this.send({ type: "info", message: permissionProfileWarning });
|
|
13573
|
+
}
|
|
13574
|
+
const pendingHooks = getPendingHookTrust(hooks, this.config.getConfigDir());
|
|
13575
|
+
if (pendingHooks.length > 0) {
|
|
13576
|
+
this.send({ type: "info", message: `${pendingHooks.length} project hook(s) require trust. Use /hooks list and /hooks trust <id>.` });
|
|
13577
|
+
}
|
|
13578
|
+
runLifecycleHooks(hooks, "SessionStart", { source: "web" }, { configDir: this.config.getConfigDir() });
|
|
12957
13579
|
this.sendStatus();
|
|
12958
13580
|
askUserContext.rl = null;
|
|
12959
13581
|
askUserContext.prompting = false;
|
|
@@ -12988,6 +13610,7 @@ var SessionHandler = class {
|
|
|
12988
13610
|
messageCount: this.sessions.current?.messages.length ?? 0,
|
|
12989
13611
|
planMode: this.planMode,
|
|
12990
13612
|
thinkingMode: this.runtimeThinking ?? false,
|
|
13613
|
+
permissionProfile: this.config.get("defaultPermissionProfile") ?? "legacy",
|
|
12991
13614
|
tokenUsage: { ...this.sessionTokenUsage },
|
|
12992
13615
|
costUsd,
|
|
12993
13616
|
providers: providerList,
|
|
@@ -13016,10 +13639,10 @@ var SessionHandler = class {
|
|
|
13016
13639
|
return;
|
|
13017
13640
|
}
|
|
13018
13641
|
case "ask_user_response": {
|
|
13019
|
-
const
|
|
13020
|
-
if (
|
|
13642
|
+
const resolve9 = this.pendingAskUser.get(msg.requestId);
|
|
13643
|
+
if (resolve9) {
|
|
13021
13644
|
this.pendingAskUser.delete(msg.requestId);
|
|
13022
|
-
|
|
13645
|
+
resolve9(msg.answer);
|
|
13023
13646
|
}
|
|
13024
13647
|
return;
|
|
13025
13648
|
}
|
|
@@ -13030,10 +13653,10 @@ var SessionHandler = class {
|
|
|
13030
13653
|
case "memory_rebuild":
|
|
13031
13654
|
return this.handleMemoryRebuild(Boolean(msg.full));
|
|
13032
13655
|
case "auto_pause_response": {
|
|
13033
|
-
const
|
|
13034
|
-
if (
|
|
13656
|
+
const resolve9 = this.pendingAutoPause.get(msg.requestId);
|
|
13657
|
+
if (resolve9) {
|
|
13035
13658
|
this.pendingAutoPause.delete(msg.requestId);
|
|
13036
|
-
|
|
13659
|
+
resolve9({ action: msg.action, message: msg.message });
|
|
13037
13660
|
}
|
|
13038
13661
|
return;
|
|
13039
13662
|
}
|
|
@@ -13048,10 +13671,10 @@ var SessionHandler = class {
|
|
|
13048
13671
|
this.hubOrchestrator?.abort();
|
|
13049
13672
|
return;
|
|
13050
13673
|
case "hub_steer": {
|
|
13051
|
-
const
|
|
13052
|
-
if (
|
|
13674
|
+
const resolve9 = this.pendingHubReview.get(msg.requestId);
|
|
13675
|
+
if (resolve9) {
|
|
13053
13676
|
this.pendingHubReview.delete(msg.requestId);
|
|
13054
|
-
|
|
13677
|
+
resolve9({ action: msg.action, message: msg.message });
|
|
13055
13678
|
}
|
|
13056
13679
|
return;
|
|
13057
13680
|
}
|
|
@@ -13061,11 +13684,12 @@ var SessionHandler = class {
|
|
|
13061
13684
|
}
|
|
13062
13685
|
}
|
|
13063
13686
|
onDisconnect() {
|
|
13687
|
+
runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
|
|
13064
13688
|
this.toolExecutor.cancelAll();
|
|
13065
13689
|
if (this.abortController) this.abortController.abort();
|
|
13066
|
-
for (const
|
|
13690
|
+
for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
|
|
13067
13691
|
this.pendingAskUser.clear();
|
|
13068
|
-
for (const
|
|
13692
|
+
for (const resolve9 of this.pendingAutoPause.values()) resolve9({ action: "stop" });
|
|
13069
13693
|
this.pendingAutoPause.clear();
|
|
13070
13694
|
this.saveIfNeeded();
|
|
13071
13695
|
}
|
|
@@ -13199,9 +13823,9 @@ var SessionHandler = class {
|
|
|
13199
13823
|
this.hubOrchestrator = orchestrator;
|
|
13200
13824
|
orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
|
|
13201
13825
|
if (config.humanSteer) {
|
|
13202
|
-
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((
|
|
13826
|
+
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve9) => {
|
|
13203
13827
|
const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
13204
|
-
this.pendingHubReview.set(requestId,
|
|
13828
|
+
this.pendingHubReview.set(requestId, resolve9);
|
|
13205
13829
|
this.send({ type: "hub_review", requestId, round, maxRounds });
|
|
13206
13830
|
});
|
|
13207
13831
|
}
|
|
@@ -13238,6 +13862,26 @@ var SessionHandler = class {
|
|
|
13238
13862
|
try {
|
|
13239
13863
|
this.ensureSession();
|
|
13240
13864
|
const session = this.sessions.current;
|
|
13865
|
+
const promptDecisions = runLifecycleHooks(
|
|
13866
|
+
this.config.get("hooks") ?? void 0,
|
|
13867
|
+
"UserPromptSubmit",
|
|
13868
|
+
{ sessionId: session.id, prompt: content },
|
|
13869
|
+
{ configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
|
|
13870
|
+
);
|
|
13871
|
+
const promptDeny = promptDecisions.find((d) => d.action === "deny");
|
|
13872
|
+
if (promptDeny) {
|
|
13873
|
+
this.send({ type: "error", message: "Prompt blocked by hook: " + (promptDeny.reason ?? "UserPromptSubmit denied") });
|
|
13874
|
+
return;
|
|
13875
|
+
}
|
|
13876
|
+
for (const d of promptDecisions) {
|
|
13877
|
+
const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
|
|
13878
|
+
for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
|
|
13879
|
+
}
|
|
13880
|
+
const secretHits = scanString(content, { customRegexes: [] });
|
|
13881
|
+
if (secretHits.length > 0) {
|
|
13882
|
+
const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
|
|
13883
|
+
this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
|
|
13884
|
+
}
|
|
13241
13885
|
const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
|
|
13242
13886
|
const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
|
|
13243
13887
|
let msgContent;
|
|
@@ -13513,7 +14157,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
13513
14157
|
this.send({ type: "response_done", content, usage });
|
|
13514
14158
|
if (pendingTeeSave && isCleanDocumentBody(content)) {
|
|
13515
14159
|
try {
|
|
13516
|
-
|
|
14160
|
+
mkdirSync11(dirname5(pendingTeeSave), { recursive: true });
|
|
13517
14161
|
const bodyToSave = stripOuterCodeFence(content);
|
|
13518
14162
|
atomicWriteFileSync(pendingTeeSave, bodyToSave);
|
|
13519
14163
|
undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
|
|
@@ -13538,8 +14182,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
13538
14182
|
onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
|
|
13539
14183
|
requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
|
|
13540
14184
|
const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
13541
|
-
const pauseResp = await new Promise((
|
|
13542
|
-
this.pendingAutoPause.set(requestId,
|
|
14185
|
+
const pauseResp = await new Promise((resolve9) => {
|
|
14186
|
+
this.pendingAutoPause.set(requestId, resolve9);
|
|
13543
14187
|
this.send({
|
|
13544
14188
|
type: "auto_pause_request",
|
|
13545
14189
|
requestId,
|
|
@@ -13689,7 +14333,7 @@ ${summaryContent}`,
|
|
|
13689
14333
|
let isError = false;
|
|
13690
14334
|
let summary;
|
|
13691
14335
|
try {
|
|
13692
|
-
|
|
14336
|
+
mkdirSync11(dirname5(saveToFile), { recursive: true });
|
|
13693
14337
|
fileStream = createWriteStream(saveToFile);
|
|
13694
14338
|
const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
|
|
13695
14339
|
const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
|
|
@@ -13718,8 +14362,8 @@ ${summaryContent}`,
|
|
|
13718
14362
|
}
|
|
13719
14363
|
if (chunk.done) break;
|
|
13720
14364
|
}
|
|
13721
|
-
await new Promise((
|
|
13722
|
-
fileStream.end((err) => err ? reject(err) :
|
|
14365
|
+
await new Promise((resolve9, reject) => {
|
|
14366
|
+
fileStream.end((err) => err ? reject(err) : resolve9());
|
|
13723
14367
|
});
|
|
13724
14368
|
const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
|
|
13725
14369
|
if (verdict.kind === "reject") {
|
|
@@ -13745,7 +14389,7 @@ ${summaryContent}`,
|
|
|
13745
14389
|
} catch (err) {
|
|
13746
14390
|
if (fileStream) {
|
|
13747
14391
|
try {
|
|
13748
|
-
await new Promise((
|
|
14392
|
+
await new Promise((resolve9) => fileStream.end(() => resolve9()));
|
|
13749
14393
|
} catch {
|
|
13750
14394
|
}
|
|
13751
14395
|
}
|
|
@@ -14275,9 +14919,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14275
14919
|
let modifiedFiles = 0;
|
|
14276
14920
|
const diffLines2 = [];
|
|
14277
14921
|
for (const [filePath, { earliest }] of fileMap) {
|
|
14278
|
-
const currentContent =
|
|
14922
|
+
const currentContent = existsSync24(filePath) ? (() => {
|
|
14279
14923
|
try {
|
|
14280
|
-
return
|
|
14924
|
+
return readFileSync18(filePath, "utf-8");
|
|
14281
14925
|
} catch {
|
|
14282
14926
|
return null;
|
|
14283
14927
|
}
|
|
@@ -14378,7 +15022,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14378
15022
|
break;
|
|
14379
15023
|
}
|
|
14380
15024
|
const sub = args[0]?.toLowerCase();
|
|
14381
|
-
const
|
|
15025
|
+
const resolve9 = (ref) => {
|
|
14382
15026
|
const r = session.resolveBranchRef(ref);
|
|
14383
15027
|
if (r.ok) return r.id;
|
|
14384
15028
|
if (r.reason === "ambiguous") {
|
|
@@ -14432,7 +15076,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14432
15076
|
this.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
14433
15077
|
break;
|
|
14434
15078
|
}
|
|
14435
|
-
const id =
|
|
15079
|
+
const id = resolve9(ref);
|
|
14436
15080
|
if (!id) break;
|
|
14437
15081
|
const ok = session.switchBranch(id);
|
|
14438
15082
|
if (ok) {
|
|
@@ -14452,7 +15096,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14452
15096
|
this.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
14453
15097
|
break;
|
|
14454
15098
|
}
|
|
14455
|
-
const id =
|
|
15099
|
+
const id = resolve9(ref);
|
|
14456
15100
|
if (!id) break;
|
|
14457
15101
|
const ok = session.deleteBranch(id);
|
|
14458
15102
|
if (ok) {
|
|
@@ -14471,7 +15115,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14471
15115
|
this.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
14472
15116
|
break;
|
|
14473
15117
|
}
|
|
14474
|
-
const id =
|
|
15118
|
+
const id = resolve9(ref);
|
|
14475
15119
|
if (!id) break;
|
|
14476
15120
|
const ok = session.renameBranch(id, title);
|
|
14477
15121
|
if (ok) {
|
|
@@ -14489,7 +15133,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14489
15133
|
this.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
14490
15134
|
break;
|
|
14491
15135
|
}
|
|
14492
|
-
const id =
|
|
15136
|
+
const id = resolve9(ref);
|
|
14493
15137
|
if (!id) break;
|
|
14494
15138
|
const d = session.diffBranches(id);
|
|
14495
15139
|
if (!d) {
|
|
@@ -14531,7 +15175,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14531
15175
|
this.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
14532
15176
|
break;
|
|
14533
15177
|
}
|
|
14534
|
-
const id =
|
|
15178
|
+
const id = resolve9(ref);
|
|
14535
15179
|
if (!id) break;
|
|
14536
15180
|
const idx = parseInt(idxStr, 10);
|
|
14537
15181
|
if (Number.isNaN(idx)) {
|
|
@@ -14793,7 +15437,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14793
15437
|
case "test": {
|
|
14794
15438
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
14795
15439
|
try {
|
|
14796
|
-
const { executeTests } = await import("./run-tests-
|
|
15440
|
+
const { executeTests } = await import("./run-tests-D7YBY4XB.js");
|
|
14797
15441
|
const argStr = args.join(" ").trim();
|
|
14798
15442
|
let testArgs = {};
|
|
14799
15443
|
if (argStr) {
|
|
@@ -14810,9 +15454,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
14810
15454
|
// ── /init ───────────────────────────────────────────────────────
|
|
14811
15455
|
case "init": {
|
|
14812
15456
|
const cwd = process.cwd();
|
|
14813
|
-
const targetPath =
|
|
15457
|
+
const targetPath = join18(cwd, "AICLI.md");
|
|
14814
15458
|
const force = args.includes("--force");
|
|
14815
|
-
if (
|
|
15459
|
+
if (existsSync24(targetPath) && !force) {
|
|
14816
15460
|
this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
14817
15461
|
Use /init --force to overwrite.` });
|
|
14818
15462
|
break;
|
|
@@ -14845,7 +15489,7 @@ Use /context reload to load it.` });
|
|
|
14845
15489
|
lines.push("**Config Files:**");
|
|
14846
15490
|
lines.push(` Dir: ${configDir}`);
|
|
14847
15491
|
const checkFile = (label, filePath) => {
|
|
14848
|
-
const exists =
|
|
15492
|
+
const exists = existsSync24(filePath);
|
|
14849
15493
|
let extra = "";
|
|
14850
15494
|
if (exists) {
|
|
14851
15495
|
try {
|
|
@@ -14855,9 +15499,9 @@ Use /context reload to load it.` });
|
|
|
14855
15499
|
}
|
|
14856
15500
|
lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
|
|
14857
15501
|
};
|
|
14858
|
-
checkFile("config.json",
|
|
14859
|
-
checkFile("memory.md",
|
|
14860
|
-
checkFile("dev-state.md",
|
|
15502
|
+
checkFile("config.json", join18(configDir, "config.json"));
|
|
15503
|
+
checkFile("memory.md", join18(configDir, MEMORY_FILE_NAME));
|
|
15504
|
+
checkFile("dev-state.md", join18(configDir, "dev-state.md"));
|
|
14861
15505
|
lines.push("");
|
|
14862
15506
|
if (this.mcpManager) {
|
|
14863
15507
|
lines.push("**MCP Servers:**");
|
|
@@ -15044,8 +15688,8 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
|
|
|
15044
15688
|
this.send({ type: "info", message: "\u2713 All added directories removed from context." });
|
|
15045
15689
|
break;
|
|
15046
15690
|
}
|
|
15047
|
-
const dirPath =
|
|
15048
|
-
if (!
|
|
15691
|
+
const dirPath = resolve7(sub);
|
|
15692
|
+
if (!existsSync24(dirPath)) {
|
|
15049
15693
|
this.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
15050
15694
|
break;
|
|
15051
15695
|
}
|
|
@@ -15061,8 +15705,8 @@ It will be included in AI context for subsequent messages.` });
|
|
|
15061
15705
|
// ── /commands ───────────────────────────────────────────────────
|
|
15062
15706
|
case "commands": {
|
|
15063
15707
|
const configDir = this.config.getConfigDir();
|
|
15064
|
-
const commandsDir =
|
|
15065
|
-
if (!
|
|
15708
|
+
const commandsDir = join18(configDir, CUSTOM_COMMANDS_DIR_NAME);
|
|
15709
|
+
if (!existsSync24(commandsDir)) {
|
|
15066
15710
|
this.send({ type: "info", message: `No custom commands directory.
|
|
15067
15711
|
Create: ${commandsDir}/ with .md files.` });
|
|
15068
15712
|
break;
|
|
@@ -15088,7 +15732,7 @@ Add .md files to create commands.` });
|
|
|
15088
15732
|
// ── /plugins ────────────────────────────────────────────────────
|
|
15089
15733
|
case "plugins": {
|
|
15090
15734
|
const configDir = this.config.getConfigDir();
|
|
15091
|
-
const pluginsDir =
|
|
15735
|
+
const pluginsDir = join18(configDir, PLUGINS_DIR_NAME);
|
|
15092
15736
|
const pluginTools = this.toolRegistry.listPluginTools();
|
|
15093
15737
|
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
15094
15738
|
if (pluginTools.length === 0) {
|
|
@@ -15143,6 +15787,21 @@ Add .md files to create commands.` });
|
|
|
15143
15787
|
this.send({ type: "info", message: "Not enough messages to compact." });
|
|
15144
15788
|
return;
|
|
15145
15789
|
}
|
|
15790
|
+
const preDecisions = runLifecycleHooks(
|
|
15791
|
+
this.config.get("hooks") ?? void 0,
|
|
15792
|
+
"PreCompact",
|
|
15793
|
+
{ sessionId: session.id, messageCount: session.messages.length, instruction },
|
|
15794
|
+
{ configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
|
|
15795
|
+
);
|
|
15796
|
+
const preDeny = preDecisions.find((d) => d.action === "deny");
|
|
15797
|
+
if (preDeny) {
|
|
15798
|
+
this.send({ type: "error", message: "Compact blocked by hook: " + (preDeny.reason ?? "PreCompact denied") });
|
|
15799
|
+
return;
|
|
15800
|
+
}
|
|
15801
|
+
for (const d of preDecisions) {
|
|
15802
|
+
const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
|
|
15803
|
+
for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
|
|
15804
|
+
}
|
|
15146
15805
|
const provider = this.providers.get(this.currentProvider);
|
|
15147
15806
|
if (!provider) return;
|
|
15148
15807
|
this.send({ type: "info", message: "Compacting conversation..." });
|
|
@@ -15162,7 +15821,15 @@ Add .md files to create commands.` });
|
|
|
15162
15821
|
});
|
|
15163
15822
|
const summaryMsg = { role: "user", content: "[Conversation summary]", timestamp: /* @__PURE__ */ new Date() };
|
|
15164
15823
|
const ackMsg = { role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() };
|
|
15165
|
-
session.
|
|
15824
|
+
const beforeCount = session.messages.length;
|
|
15825
|
+
const removedCount = session.compact(summaryMsg, ackMsg, keepLast);
|
|
15826
|
+
const afterCount = session.messages.length;
|
|
15827
|
+
runLifecycleHooks(
|
|
15828
|
+
this.config.get("hooks") ?? void 0,
|
|
15829
|
+
"PostCompact",
|
|
15830
|
+
{ sessionId: session.id, beforeCount, afterCount, removedCount, keepLast, instruction },
|
|
15831
|
+
{ configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
|
|
15832
|
+
);
|
|
15166
15833
|
this.send({ type: "info", message: `Compacted: removed ${toSummarize.length} messages, kept last ${keepLast}.` });
|
|
15167
15834
|
this.sendStatus();
|
|
15168
15835
|
} catch (err) {
|
|
@@ -15262,11 +15929,11 @@ Add .md files to create commands.` });
|
|
|
15262
15929
|
}
|
|
15263
15930
|
memoryShow() {
|
|
15264
15931
|
const configDir = this.config.getConfigDir();
|
|
15265
|
-
const memPath =
|
|
15932
|
+
const memPath = join18(configDir, MEMORY_FILE_NAME);
|
|
15266
15933
|
let content = "";
|
|
15267
15934
|
try {
|
|
15268
|
-
if (
|
|
15269
|
-
content =
|
|
15935
|
+
if (existsSync24(memPath)) {
|
|
15936
|
+
content = readFileSync18(memPath, "utf-8");
|
|
15270
15937
|
}
|
|
15271
15938
|
} catch (err) {
|
|
15272
15939
|
process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
|
|
@@ -15280,11 +15947,11 @@ Add .md files to create commands.` });
|
|
|
15280
15947
|
}
|
|
15281
15948
|
memoryAdd(text) {
|
|
15282
15949
|
const configDir = this.config.getConfigDir();
|
|
15283
|
-
const memPath =
|
|
15950
|
+
const memPath = join18(configDir, MEMORY_FILE_NAME);
|
|
15284
15951
|
try {
|
|
15285
|
-
|
|
15952
|
+
mkdirSync11(configDir, { recursive: true });
|
|
15286
15953
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
15287
|
-
const previous =
|
|
15954
|
+
const previous = existsSync24(memPath) ? readFileSync18(memPath, "utf-8") : "";
|
|
15288
15955
|
const entry = `
|
|
15289
15956
|
- [${timestamp}] ${text}
|
|
15290
15957
|
`;
|
|
@@ -15296,7 +15963,7 @@ Add .md files to create commands.` });
|
|
|
15296
15963
|
}
|
|
15297
15964
|
memoryClear() {
|
|
15298
15965
|
const configDir = this.config.getConfigDir();
|
|
15299
|
-
const memPath =
|
|
15966
|
+
const memPath = join18(configDir, MEMORY_FILE_NAME);
|
|
15300
15967
|
try {
|
|
15301
15968
|
atomicWriteFileSync(memPath, "");
|
|
15302
15969
|
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
@@ -15314,7 +15981,7 @@ Add .md files to create commands.` });
|
|
|
15314
15981
|
return;
|
|
15315
15982
|
}
|
|
15316
15983
|
try {
|
|
15317
|
-
const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-
|
|
15984
|
+
const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-R2E27VXN.js");
|
|
15318
15985
|
const loaded = loadChatIndex2();
|
|
15319
15986
|
if (!loaded || loaded.idx.chunks.length === 0) {
|
|
15320
15987
|
this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
|
|
@@ -15350,7 +16017,7 @@ Add .md files to create commands.` });
|
|
|
15350
16017
|
}
|
|
15351
16018
|
async handleMemoryStatus() {
|
|
15352
16019
|
try {
|
|
15353
|
-
const { getChatIndexStatus } = await import("./chat-index-
|
|
16020
|
+
const { getChatIndexStatus } = await import("./chat-index-R2E27VXN.js");
|
|
15354
16021
|
const s = getChatIndexStatus();
|
|
15355
16022
|
this.send({
|
|
15356
16023
|
type: "memory_status",
|
|
@@ -15375,7 +16042,7 @@ Add .md files to create commands.` });
|
|
|
15375
16042
|
type: "info",
|
|
15376
16043
|
message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
|
|
15377
16044
|
});
|
|
15378
|
-
const { buildChatIndex } = await import("./chat-index-
|
|
16045
|
+
const { buildChatIndex } = await import("./chat-index-R2E27VXN.js");
|
|
15379
16046
|
const stats = await buildChatIndex({
|
|
15380
16047
|
full,
|
|
15381
16048
|
onProgress: (p) => {
|
|
@@ -15530,8 +16197,8 @@ async function setupProxy(configProxy) {
|
|
|
15530
16197
|
}
|
|
15531
16198
|
|
|
15532
16199
|
// src/web/auth.ts
|
|
15533
|
-
import { existsSync as
|
|
15534
|
-
import { join as
|
|
16200
|
+
import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync3, mkdirSync as mkdirSync12, readdirSync as readdirSync11, copyFileSync } from "fs";
|
|
16201
|
+
import { join as join19 } from "path";
|
|
15535
16202
|
import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
|
|
15536
16203
|
var USERS_FILE = "users.json";
|
|
15537
16204
|
var TOKEN_EXPIRY_HOURS = 24;
|
|
@@ -15546,7 +16213,7 @@ var AuthManager = class {
|
|
|
15546
16213
|
db;
|
|
15547
16214
|
constructor(baseDir) {
|
|
15548
16215
|
this.baseDir = baseDir;
|
|
15549
|
-
this.usersFile =
|
|
16216
|
+
this.usersFile = join19(baseDir, USERS_FILE);
|
|
15550
16217
|
this.db = this.loadOrCreate();
|
|
15551
16218
|
}
|
|
15552
16219
|
// ── Public API ─────────────────────────────────────────────────
|
|
@@ -15575,9 +16242,9 @@ var AuthManager = class {
|
|
|
15575
16242
|
}
|
|
15576
16243
|
const salt = randomBytes(16).toString("hex");
|
|
15577
16244
|
const passwordHash = this.hashPassword(password, salt);
|
|
15578
|
-
const dataDir =
|
|
15579
|
-
const fullDataDir =
|
|
15580
|
-
|
|
16245
|
+
const dataDir = join19(USERS_DIR, username);
|
|
16246
|
+
const fullDataDir = join19(this.baseDir, dataDir);
|
|
16247
|
+
mkdirSync12(join19(fullDataDir, "history"), { recursive: true });
|
|
15581
16248
|
const user = {
|
|
15582
16249
|
username,
|
|
15583
16250
|
passwordHash,
|
|
@@ -15692,7 +16359,7 @@ var AuthManager = class {
|
|
|
15692
16359
|
getUserDataDir(username) {
|
|
15693
16360
|
const user = this.db.users.find((u) => u.username === username);
|
|
15694
16361
|
if (!user) throw new Error(`User not found: ${username}`);
|
|
15695
|
-
return
|
|
16362
|
+
return join19(this.baseDir, user.dataDir);
|
|
15696
16363
|
}
|
|
15697
16364
|
/** List all usernames */
|
|
15698
16365
|
listUsers() {
|
|
@@ -15728,30 +16395,30 @@ var AuthManager = class {
|
|
|
15728
16395
|
const err = this.register(username, password);
|
|
15729
16396
|
if (err) return err;
|
|
15730
16397
|
const userDir = this.getUserDataDir(username);
|
|
15731
|
-
const globalConfig =
|
|
15732
|
-
if (
|
|
16398
|
+
const globalConfig = join19(this.baseDir, "config.json");
|
|
16399
|
+
if (existsSync25(globalConfig)) {
|
|
15733
16400
|
try {
|
|
15734
|
-
const content =
|
|
15735
|
-
writeFileSync3(
|
|
16401
|
+
const content = readFileSync19(globalConfig, "utf-8");
|
|
16402
|
+
writeFileSync3(join19(userDir, "config.json"), content, "utf-8");
|
|
15736
16403
|
} catch {
|
|
15737
16404
|
}
|
|
15738
16405
|
}
|
|
15739
|
-
const globalMemory =
|
|
15740
|
-
if (
|
|
16406
|
+
const globalMemory = join19(this.baseDir, "memory.md");
|
|
16407
|
+
if (existsSync25(globalMemory)) {
|
|
15741
16408
|
try {
|
|
15742
|
-
const content =
|
|
15743
|
-
writeFileSync3(
|
|
16409
|
+
const content = readFileSync19(globalMemory, "utf-8");
|
|
16410
|
+
writeFileSync3(join19(userDir, "memory.md"), content, "utf-8");
|
|
15744
16411
|
} catch {
|
|
15745
16412
|
}
|
|
15746
16413
|
}
|
|
15747
|
-
const globalHistory =
|
|
15748
|
-
if (
|
|
16414
|
+
const globalHistory = join19(this.baseDir, "history");
|
|
16415
|
+
if (existsSync25(globalHistory)) {
|
|
15749
16416
|
try {
|
|
15750
16417
|
const files = readdirSync11(globalHistory).filter((f) => f.endsWith(".json"));
|
|
15751
|
-
const userHistory =
|
|
16418
|
+
const userHistory = join19(userDir, "history");
|
|
15752
16419
|
for (const f of files) {
|
|
15753
16420
|
try {
|
|
15754
|
-
copyFileSync(
|
|
16421
|
+
copyFileSync(join19(globalHistory, f), join19(userHistory, f));
|
|
15755
16422
|
} catch {
|
|
15756
16423
|
}
|
|
15757
16424
|
}
|
|
@@ -15762,9 +16429,9 @@ var AuthManager = class {
|
|
|
15762
16429
|
}
|
|
15763
16430
|
// ── Private methods ────────────────────────────────────────────
|
|
15764
16431
|
loadOrCreate() {
|
|
15765
|
-
if (
|
|
16432
|
+
if (existsSync25(this.usersFile)) {
|
|
15766
16433
|
try {
|
|
15767
|
-
return JSON.parse(
|
|
16434
|
+
return JSON.parse(readFileSync19(this.usersFile, "utf-8"));
|
|
15768
16435
|
} catch {
|
|
15769
16436
|
}
|
|
15770
16437
|
}
|
|
@@ -15780,7 +16447,7 @@ var AuthManager = class {
|
|
|
15780
16447
|
this.saveDB(this.db);
|
|
15781
16448
|
}
|
|
15782
16449
|
saveDB(db) {
|
|
15783
|
-
|
|
16450
|
+
mkdirSync12(this.baseDir, { recursive: true });
|
|
15784
16451
|
atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
|
|
15785
16452
|
}
|
|
15786
16453
|
/** Legacy hash — kept only for migrating old users (v0.2.x) */
|
|
@@ -15816,7 +16483,7 @@ function getModuleDir() {
|
|
|
15816
16483
|
}
|
|
15817
16484
|
} catch {
|
|
15818
16485
|
}
|
|
15819
|
-
return
|
|
16486
|
+
return resolve8(".");
|
|
15820
16487
|
}
|
|
15821
16488
|
async function startWebServer(options = {}) {
|
|
15822
16489
|
const port = options.port ?? 3e3;
|
|
@@ -15881,8 +16548,8 @@ async function startWebServer(options = {}) {
|
|
|
15881
16548
|
}
|
|
15882
16549
|
}
|
|
15883
16550
|
let skillManager = null;
|
|
15884
|
-
const skillsDir =
|
|
15885
|
-
if (
|
|
16551
|
+
const skillsDir = join20(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
16552
|
+
if (existsSync26(skillsDir)) {
|
|
15886
16553
|
skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
|
|
15887
16554
|
skillManager.loadSkills();
|
|
15888
16555
|
const count = skillManager.listSkills().length;
|
|
@@ -15953,18 +16620,18 @@ async function startWebServer(options = {}) {
|
|
|
15953
16620
|
next();
|
|
15954
16621
|
};
|
|
15955
16622
|
const moduleDir = getModuleDir();
|
|
15956
|
-
let clientDir =
|
|
15957
|
-
if (!
|
|
15958
|
-
clientDir =
|
|
16623
|
+
let clientDir = join20(moduleDir, "web", "client");
|
|
16624
|
+
if (!existsSync26(clientDir)) {
|
|
16625
|
+
clientDir = join20(moduleDir, "client");
|
|
15959
16626
|
}
|
|
15960
|
-
if (!
|
|
15961
|
-
clientDir =
|
|
16627
|
+
if (!existsSync26(clientDir)) {
|
|
16628
|
+
clientDir = join20(moduleDir, "..", "..", "src", "web", "client");
|
|
15962
16629
|
}
|
|
15963
|
-
if (!
|
|
15964
|
-
clientDir =
|
|
16630
|
+
if (!existsSync26(clientDir)) {
|
|
16631
|
+
clientDir = join20(process.cwd(), "src", "web", "client");
|
|
15965
16632
|
}
|
|
15966
16633
|
console.log(` Static files: ${clientDir}`);
|
|
15967
|
-
app.use("/vendor", express.static(
|
|
16634
|
+
app.use("/vendor", express.static(join20(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
|
|
15968
16635
|
app.use(express.static(clientDir));
|
|
15969
16636
|
app.get("/api/status", (_req, res) => {
|
|
15970
16637
|
res.json({
|
|
@@ -16031,10 +16698,10 @@ async function startWebServer(options = {}) {
|
|
|
16031
16698
|
app.get("/api/files", requireAuth, (req, res) => {
|
|
16032
16699
|
const cwd = process.cwd();
|
|
16033
16700
|
const prefix = req.query.prefix || "";
|
|
16034
|
-
const targetDir =
|
|
16701
|
+
const targetDir = join20(cwd, prefix);
|
|
16035
16702
|
try {
|
|
16036
|
-
const canonicalTarget = realpathSync(
|
|
16037
|
-
const canonicalCwd = realpathSync(
|
|
16703
|
+
const canonicalTarget = realpathSync(resolve8(targetDir));
|
|
16704
|
+
const canonicalCwd = realpathSync(resolve8(cwd));
|
|
16038
16705
|
if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
|
|
16039
16706
|
res.json({ files: [] });
|
|
16040
16707
|
return;
|
|
@@ -16048,7 +16715,7 @@ async function startWebServer(options = {}) {
|
|
|
16048
16715
|
const entries = readdirSync12(targetDir, { withFileTypes: true });
|
|
16049
16716
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
16050
16717
|
name: e.name,
|
|
16051
|
-
path: relative4(cwd,
|
|
16718
|
+
path: relative4(cwd, join20(targetDir, e.name)).replace(/\\/g, "/"),
|
|
16052
16719
|
isDir: e.isDirectory()
|
|
16053
16720
|
}));
|
|
16054
16721
|
res.json({ files });
|
|
@@ -16084,8 +16751,8 @@ async function startWebServer(options = {}) {
|
|
|
16084
16751
|
try {
|
|
16085
16752
|
const authUser = req._authUser;
|
|
16086
16753
|
const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
|
|
16087
|
-
const filePath =
|
|
16088
|
-
if (!
|
|
16754
|
+
const filePath = join20(histDir, `${id}.json`);
|
|
16755
|
+
if (!existsSync26(filePath)) {
|
|
16089
16756
|
res.status(404).json({ error: "Session not found" });
|
|
16090
16757
|
return;
|
|
16091
16758
|
}
|
|
@@ -16100,7 +16767,7 @@ async function startWebServer(options = {}) {
|
|
|
16100
16767
|
res.status(404).json({ error: "Session not found" });
|
|
16101
16768
|
return;
|
|
16102
16769
|
}
|
|
16103
|
-
const data = JSON.parse(
|
|
16770
|
+
const data = JSON.parse(readFileSync20(filePath, "utf-8"));
|
|
16104
16771
|
res.json({ session: data });
|
|
16105
16772
|
} catch (err) {
|
|
16106
16773
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
@@ -16113,10 +16780,10 @@ async function startWebServer(options = {}) {
|
|
|
16113
16780
|
return;
|
|
16114
16781
|
}
|
|
16115
16782
|
const cwd = process.cwd();
|
|
16116
|
-
const fullPath =
|
|
16783
|
+
const fullPath = resolve8(join20(cwd, filePath));
|
|
16117
16784
|
try {
|
|
16118
16785
|
const canonicalFull = realpathSync(fullPath);
|
|
16119
|
-
const canonicalCwd = realpathSync(
|
|
16786
|
+
const canonicalCwd = realpathSync(resolve8(cwd));
|
|
16120
16787
|
if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
|
|
16121
16788
|
res.json({ error: "Access denied" });
|
|
16122
16789
|
return;
|
|
@@ -16131,7 +16798,7 @@ async function startWebServer(options = {}) {
|
|
|
16131
16798
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
16132
16799
|
return;
|
|
16133
16800
|
}
|
|
16134
|
-
const content =
|
|
16801
|
+
const content = readFileSync20(fullPath, "utf-8");
|
|
16135
16802
|
res.json({ content, size: stat.size });
|
|
16136
16803
|
} catch {
|
|
16137
16804
|
res.json({ error: "Cannot read file" });
|
|
@@ -16335,7 +17002,7 @@ async function startWebServer(options = {}) {
|
|
|
16335
17002
|
});
|
|
16336
17003
|
const MAX_PORT_ATTEMPTS = 10;
|
|
16337
17004
|
let actualPort = port;
|
|
16338
|
-
const result = await new Promise((
|
|
17005
|
+
const result = await new Promise((resolve9, reject) => {
|
|
16339
17006
|
const tryListen = (attempt) => {
|
|
16340
17007
|
server.once("error", (err) => {
|
|
16341
17008
|
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
@@ -16366,7 +17033,7 @@ async function startWebServer(options = {}) {
|
|
|
16366
17033
|
}
|
|
16367
17034
|
console.log(` Press Ctrl+C to stop
|
|
16368
17035
|
`);
|
|
16369
|
-
|
|
17036
|
+
resolve9({ port: actualPort, host, url });
|
|
16370
17037
|
});
|
|
16371
17038
|
};
|
|
16372
17039
|
tryListen(1);
|
|
@@ -16386,17 +17053,17 @@ function resolveProjectMcpPath() {
|
|
|
16386
17053
|
const cwd = process.cwd();
|
|
16387
17054
|
const gitRoot = getGitRoot(cwd);
|
|
16388
17055
|
const projectRoot = gitRoot ?? cwd;
|
|
16389
|
-
const configPath =
|
|
16390
|
-
return
|
|
17056
|
+
const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
17057
|
+
return existsSync26(configPath) ? configPath : null;
|
|
16391
17058
|
}
|
|
16392
17059
|
function loadProjectMcpConfig() {
|
|
16393
17060
|
const cwd = process.cwd();
|
|
16394
17061
|
const gitRoot = getGitRoot(cwd);
|
|
16395
17062
|
const projectRoot = gitRoot ?? cwd;
|
|
16396
|
-
const configPath =
|
|
16397
|
-
if (!
|
|
17063
|
+
const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
17064
|
+
if (!existsSync26(configPath)) return null;
|
|
16398
17065
|
try {
|
|
16399
|
-
const raw = JSON.parse(
|
|
17066
|
+
const raw = JSON.parse(readFileSync20(configPath, "utf-8"));
|
|
16400
17067
|
return raw.mcpServers ?? raw;
|
|
16401
17068
|
} catch {
|
|
16402
17069
|
return null;
|