nine-hundred 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +43 -0
  2. package/dist/agent/config/index.js +85 -0
  3. package/dist/agent/context/compress.js +145 -0
  4. package/dist/agent/context/index.js +68 -0
  5. package/dist/agent/context/token.js +30 -0
  6. package/dist/agent/hooks/config.js +41 -0
  7. package/dist/agent/hooks/core.js +177 -0
  8. package/dist/agent/hooks/events/index.js +9 -0
  9. package/dist/agent/hooks/events/post-tool-use-hook.js +7 -0
  10. package/dist/agent/hooks/events/pre-tool-use-hook.js +8 -0
  11. package/dist/agent/hooks/events/session-start-hook.js +8 -0
  12. package/dist/agent/hooks/events/stop-hook.js +10 -0
  13. package/dist/agent/hooks/events/user-prompt-submit-hook.js +7 -0
  14. package/dist/agent/hooks/index.js +7 -0
  15. package/dist/agent/hooks/types.js +32 -0
  16. package/dist/agent/index.js +465 -0
  17. package/dist/agent/llm/index.js +35 -0
  18. package/dist/agent/mcp/client.js +87 -0
  19. package/dist/agent/mcp/config.js +51 -0
  20. package/dist/agent/mcp/index.js +90 -0
  21. package/dist/agent/mcp/schema.js +56 -0
  22. package/dist/agent/permission/exec.js +71 -0
  23. package/dist/agent/permission/network.js +37 -0
  24. package/dist/agent/permission/read.js +27 -0
  25. package/dist/agent/permission/util/command-changes-directory.js +29 -0
  26. package/dist/agent/permission/util/dangerous-path.json +116 -0
  27. package/dist/agent/permission/util/detect-dangerous-operation.js +230 -0
  28. package/dist/agent/permission/util/detect-language-interpreter.js +66 -0
  29. package/dist/agent/permission/util/detect-safe-command.js +61 -0
  30. package/dist/agent/permission/util/index.js +16 -0
  31. package/dist/agent/permission/util/is-dangerous-path.js +98 -0
  32. package/dist/agent/permission/util/is-inside-cwd.js +83 -0
  33. package/dist/agent/permission/util/is-safe-domains.js +47 -0
  34. package/dist/agent/permission/write.js +31 -0
  35. package/dist/agent/prompt/index.js +42 -0
  36. package/dist/agent/skills/planner/SKILL.md +90 -0
  37. package/dist/agent/skills/programmer-resume/SKILL.md +113 -0
  38. package/dist/agent/skills/skill-creator/LICENSE.txt +202 -0
  39. package/dist/agent/skills/skill-creator/SKILL.md +495 -0
  40. package/dist/agent/skills/skill-creator/agents/analyzer.md +274 -0
  41. package/dist/agent/skills/skill-creator/agents/comparator.md +202 -0
  42. package/dist/agent/skills/skill-creator/agents/grader.md +223 -0
  43. package/dist/agent/skills/skill-creator/assets/eval_review.html +146 -0
  44. package/dist/agent/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  45. package/dist/agent/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  46. package/dist/agent/skills/skill-creator/references/schemas.md +430 -0
  47. package/dist/agent/skills/skill-creator/scripts/__init__.py +0 -0
  48. package/dist/agent/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  49. package/dist/agent/skills/skill-creator/scripts/generate_report.py +326 -0
  50. package/dist/agent/skills/skill-creator/scripts/improve_description.py +247 -0
  51. package/dist/agent/skills/skill-creator/scripts/package_skill.py +136 -0
  52. package/dist/agent/skills/skill-creator/scripts/quick_validate.py +103 -0
  53. package/dist/agent/skills/skill-creator/scripts/run_eval.py +310 -0
  54. package/dist/agent/skills/skill-creator/scripts/run_loop.py +328 -0
  55. package/dist/agent/skills/skill-creator/scripts/utils.py +47 -0
  56. package/dist/agent/skills.js +129 -0
  57. package/dist/agent/tools/agent_tool/agent_tool.test.js +64 -0
  58. package/dist/agent/tools/agent_tool/index.js +33 -0
  59. package/dist/agent/tools/exec_tool/exec_tool.test.js +48 -0
  60. package/dist/agent/tools/exec_tool/index.js +44 -0
  61. package/dist/agent/tools/load_skill_tool/index.js +5 -0
  62. package/dist/agent/tools/load_skill_tool/load_skill_tool.test.js +122 -0
  63. package/dist/agent/tools/memory_create_tool/index.js +8 -0
  64. package/dist/agent/tools/memory_create_tool/memory_create_tool.test.js +54 -0
  65. package/dist/agent/tools/memory_delete_tool/index.js +10 -0
  66. package/dist/agent/tools/memory_delete_tool/memory_delete_tool.test.js +39 -0
  67. package/dist/agent/tools/memory_retrieve_tool/index.js +61 -0
  68. package/dist/agent/tools/memory_retrieve_tool/memory_retrieve_tool.test.js +102 -0
  69. package/dist/agent/tools/profile_update_tool/index.js +30 -0
  70. package/dist/agent/tools/profile_update_tool/profile_update_tool.test.js +49 -0
  71. package/dist/agent/tools/read_file_tool/index.js +24 -0
  72. package/dist/agent/tools/read_file_tool/read_file_tool.test.js +43 -0
  73. package/dist/agent/tools/run_js_tool/index.js +48 -0
  74. package/dist/agent/tools/run_js_tool/run_js_tool.test.js +67 -0
  75. package/dist/agent/tools/run_py_tool/index.js +48 -0
  76. package/dist/agent/tools/run_py_tool/run_py_tool.test.js +67 -0
  77. package/dist/agent/tools/tool_logger.js +16 -0
  78. package/dist/agent/tools/tool_logger.test.js +22 -0
  79. package/dist/agent/tools/web_fetch_url/index.js +76 -0
  80. package/dist/agent/tools/web_fetch_url/web_fetch_url.test.js +102 -0
  81. package/dist/agent/tools/web_search_tool/index.js +26 -0
  82. package/dist/agent/tools/web_search_tool/web_search_tool.test.js +61 -0
  83. package/dist/agent/tools/write_file_tool/index.js +22 -0
  84. package/dist/agent/tools/write_file_tool/write_file_tool.test.js +46 -0
  85. package/dist/agent/tools.js +218 -0
  86. package/dist/cli/command/compact/index.js +14 -0
  87. package/dist/cli/command/index.js +62 -0
  88. package/dist/cli/command/invalid/index.js +4 -0
  89. package/dist/cli/command/new/chat-session.js +10 -0
  90. package/dist/cli/command/new/index.js +8 -0
  91. package/dist/cli/command/rewind/index.js +19 -0
  92. package/dist/cli/command/rewind/rewind-command.test.js +22 -0
  93. package/dist/cli/command/session/format.js +32 -0
  94. package/dist/cli/command/session/index.js +32 -0
  95. package/dist/cli/command/session/session-command.test.js +49 -0
  96. package/dist/cli/command/unknown/index.js +4 -0
  97. package/dist/cli/index.js +144 -0
  98. package/dist/db/checkpointer.js +15 -0
  99. package/dist/db/index.js +2 -0
  100. package/dist/db/path.js +8 -0
  101. package/dist/db/sessions.js +81 -0
  102. package/dist/db/tables/memory.js +12 -0
  103. package/dist/db/tables/memory_fts.js +29 -0
  104. package/dist/index.js +87 -0
  105. package/dist/install.js +154 -0
  106. package/package.json +51 -0
  107. package/pnpm-workspace.yaml +3 -0
@@ -0,0 +1,90 @@
1
+ import chalk from 'chalk';
2
+ import { McpServerClient } from './client.js';
3
+ import { isHttpConfig, isStdioConfig, readMcpConfig } from './config.js';
4
+ import { jsonSchemaToZod } from './schema.js';
5
+ const clients = new Map();
6
+ let wrappers = [];
7
+ let initialized = false;
8
+ function formatConnectionLabel(serverName, params) {
9
+ if (isStdioConfig(params)) {
10
+ return `${params.command}${params.args ? ' ' + params.args.join(' ') : ''}`;
11
+ }
12
+ if (isHttpConfig(params)) {
13
+ return params.url;
14
+ }
15
+ return serverName;
16
+ }
17
+ export async function initializeMcpClients() {
18
+ if (initialized)
19
+ return;
20
+ const config = await readMcpConfig();
21
+ const entries = Object.entries(config.mcpServers ?? {});
22
+ if (entries.length === 0) {
23
+ initialized = true;
24
+ return;
25
+ }
26
+ const serverCount = entries.length;
27
+ console.log(chalk.blue(`[MCP] 正在连接 ${serverCount} 个 MCP 服务器...`));
28
+ const newWrappers = [];
29
+ const seenNames = new Set();
30
+ let successCount = 0;
31
+ let failCount = 0;
32
+ for (const [serverName, params] of entries) {
33
+ const label = formatConnectionLabel(serverName, params);
34
+ const mode = isHttpConfig(params) ? 'HTTP' : 'stdio';
35
+ console.log(chalk.dim(`[MCP] [${mode}] 连接 "${serverName}" (${label})...`));
36
+ const client = new McpServerClient(serverName, params);
37
+ try {
38
+ await client.connect();
39
+ clients.set(serverName, client);
40
+ successCount++;
41
+ const tools = client.getTools();
42
+ console.log(chalk.green(`[MCP] "${serverName}" 连接成功`) + chalk.cyan(`,发现 ${tools.length} 个工具`));
43
+ for (const def of client.getTools()) {
44
+ if (seenNames.has(def.name)) {
45
+ console.warn(`[MCP] Duplicate tool name "${def.name}" from server "${serverName}", skipping.`);
46
+ continue;
47
+ }
48
+ seenNames.add(def.name);
49
+ newWrappers.push({
50
+ name: def.name,
51
+ description: def.description,
52
+ schema: jsonSchemaToZod(def.inputSchema),
53
+ invoke: (args) => client.callTool(def.name, args),
54
+ });
55
+ }
56
+ }
57
+ catch (err) {
58
+ failCount++;
59
+ console.warn(chalk.red(`[MCP] "${serverName}" 连接失败: ${err.message}`));
60
+ try {
61
+ await client.disconnect();
62
+ }
63
+ catch {
64
+ // ignore
65
+ }
66
+ }
67
+ }
68
+ wrappers = newWrappers;
69
+ initialized = true;
70
+ if (failCount === 0) {
71
+ console.log(chalk.green.bold(`[MCP] 全部 ${serverCount} 个服务器连接成功,共注册 ${newWrappers.length} 个工具`));
72
+ }
73
+ else if (successCount === 0) {
74
+ console.warn(chalk.red.bold(`[MCP] 所有 ${serverCount} 个服务器均连接失败`));
75
+ }
76
+ else {
77
+ console.log(chalk.yellow.bold(`[MCP] ${successCount}/${serverCount} 个服务器连接成功,共注册 ${newWrappers.length} 个工具`));
78
+ }
79
+ }
80
+ export function getMcpToolWrappers() {
81
+ return wrappers;
82
+ }
83
+ export async function shutdownMcpClients() {
84
+ for (const client of clients.values()) {
85
+ await client.disconnect().catch(() => { });
86
+ }
87
+ clients.clear();
88
+ wrappers = [];
89
+ initialized = false;
90
+ }
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ export function jsonSchemaToZod(schema) {
3
+ if (typeof schema !== 'object' || schema === null) {
4
+ return z.any();
5
+ }
6
+ const s = schema;
7
+ if (Array.isArray(s.enum) && s.enum.every((v) => typeof v === 'string')) {
8
+ return z.enum(s.enum);
9
+ }
10
+ switch (s.type) {
11
+ case 'string': {
12
+ let base = z.string();
13
+ if (typeof s.minLength === 'number')
14
+ base = base.min(s.minLength);
15
+ if (typeof s.maxLength === 'number')
16
+ base = base.max(s.maxLength);
17
+ return base;
18
+ }
19
+ case 'number':
20
+ return z.number();
21
+ case 'integer':
22
+ return z.number().int();
23
+ case 'boolean':
24
+ return z.boolean();
25
+ case 'array': {
26
+ const items = (s.items ?? {});
27
+ return z.array(jsonSchemaToZod(items));
28
+ }
29
+ case 'object': {
30
+ const properties = (s.properties ?? {});
31
+ const required = new Set(Array.isArray(s.required) ? s.required : []);
32
+ const shape = {};
33
+ for (const [key, propSchema] of Object.entries(properties)) {
34
+ let prop = jsonSchemaToZod(propSchema);
35
+ if (!required.has(key)) {
36
+ prop = prop.optional();
37
+ }
38
+ shape[key] = prop;
39
+ }
40
+ return z.object(shape).passthrough();
41
+ }
42
+ case 'null':
43
+ return z.null();
44
+ default: {
45
+ if (Array.isArray(s.anyOf)) {
46
+ const unions = s.anyOf.map(jsonSchemaToZod);
47
+ if (unions.length >= 2) {
48
+ return z.union(unions);
49
+ }
50
+ if (unions.length === 1)
51
+ return unions[0];
52
+ }
53
+ return z.any();
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,71 @@
1
+ import { commandChangesDirectory, detectDangerousOperation, detectLanguageInterpreter, detectSafeCommand, } from './util/index.js';
2
+ /**
3
+ * Decide whether an exec tool call should be auto-allowed, blocked, or
4
+ * sent to the human-in-the-loop interrupt.
5
+ *
6
+ * - explicit `cwd` arg (anything other than ".") → `block`
7
+ * (the tool would execute inside a subdirectory of the project root,
8
+ * leaving the current working directory)
9
+ * - `command` contains a `cd` / `pushd` to anywhere → `block`
10
+ * - `command` invokes a non-shell language interpreter → `block`,
11
+ * with a redirect to `run_py_tool` / `run_js_tool` when applicable
12
+ * - `command` is a known dangerous operation (sudo, rm, mv, chmod, kill,
13
+ * useradd, ssh, …) or a read-style command targeting a sensitive path
14
+ * → `block`
15
+ * - `command` consists entirely of read-only safe commands
16
+ * (ls, pwd, cat, head, tail, grep, find, git status/diff/log, …) → `allow`
17
+ * - otherwise → `ask` (fall through to interrupt)
18
+ *
19
+ * The shell-command scan only runs when args carries a non-empty `command`
20
+ * string. `run_js_tool` / `run_py_tool` carry source under `code`, not
21
+ * `command`, so they are unaffected.
22
+ */
23
+ export async function checkExecPermission(args) {
24
+ const cwd = args.cwd;
25
+ if (typeof cwd === 'string' && cwd.length > 0 && cwd !== '.') {
26
+ return {
27
+ action: 'block',
28
+ message: `⚠️ \`cwd="${cwd}"\` 会把命令切到子目录执行,不允许。` +
29
+ `exec 工具只能在当前工作目录执行。` +
30
+ `请去掉 cwd 参数,或直接在当前目录完成工作。`,
31
+ };
32
+ }
33
+ const command = args.command;
34
+ if (typeof command === 'string' && command.length > 0) {
35
+ const dirResult = commandChangesDirectory(command);
36
+ if (dirResult.changes) {
37
+ return {
38
+ action: 'block',
39
+ message: `⚠️ 命令 \`${dirResult.segment}\` 包含 \`cd\` / \`pushd\`,会切换到其他目录,不允许。` +
40
+ `exec 工具只能在当前工作目录执行。` +
41
+ `请去掉 cd / pushd,或在当前目录下完成工作。`,
42
+ };
43
+ }
44
+ const langResult = detectLanguageInterpreter(command);
45
+ if (langResult.found) {
46
+ return {
47
+ action: 'block',
48
+ message: langResult.tool
49
+ ? `⚠️ 命令 \`${langResult.segment}\` 调用了 ${langResult.lang} 解释器,不允许。` +
50
+ `请改用 \`${langResult.tool}\` 工具执行 ${langResult.lang} 代码。`
51
+ : `⚠️ 命令 \`${langResult.segment}\` 调用了 ${langResult.lang} 解释器,不允许。` +
52
+ `exec 工具只支持 shell / bash / sh 命令,不支持 ${langResult.lang}。`,
53
+ };
54
+ }
55
+ const dangerResult = detectDangerousOperation(command);
56
+ if (dangerResult.found) {
57
+ return {
58
+ action: 'block',
59
+ message: dangerResult.category === 'sensitive' && dangerResult.path
60
+ ? `⚠️ 命令 \`${dangerResult.segment}\` 访问了敏感路径 \`${dangerResult.path}\`,不允许。` +
61
+ `请改用其他安全方式。`
62
+ : `⚠️ 命令 \`${dangerResult.segment}\` 是危险操作 (${dangerResult.description}),不允许。` +
63
+ `请改用其他安全方式。`,
64
+ };
65
+ }
66
+ if (detectSafeCommand(command).safe) {
67
+ return { action: 'allow' };
68
+ }
69
+ }
70
+ return { action: 'ask' };
71
+ }
@@ -0,0 +1,37 @@
1
+ import { isSafeDomain, } from './util/index.js';
2
+ /**
3
+ * Decide whether a network tool call should be auto-allowed or sent to
4
+ * the human-in-the-loop interrupt.
5
+ *
6
+ * - no `url` in args → `allow`
7
+ * (e.g. `web_search_tool` only takes a `query`, not a URL)
8
+ * - `url` parses, has a hostname, and that hostname is in the safe-
9
+ * domain list → `allow`
10
+ * - `url` parses but the hostname is not safe → `ask`
11
+ * - `url` fails to parse, or parses with an empty hostname (e.g.
12
+ * `file:///etc/passwd`) → `ask`
13
+ *
14
+ * There is intentionally no `block` outcome — unknown hosts get a
15
+ * confirmation prompt rather than a hard rejection, so the user can
16
+ * still approve a one-off fetch.
17
+ */
18
+ export async function checkNetworkPermission(args, _options = {}) {
19
+ const url = args.url;
20
+ if (typeof url !== 'string' || url.length === 0) {
21
+ return { action: 'allow' };
22
+ }
23
+ let hostname;
24
+ try {
25
+ hostname = new URL(url).hostname;
26
+ }
27
+ catch {
28
+ return { action: 'ask' };
29
+ }
30
+ if (!hostname) {
31
+ return { action: 'ask' };
32
+ }
33
+ if (isSafeDomain(hostname)) {
34
+ return { action: 'allow' };
35
+ }
36
+ return { action: 'ask' };
37
+ }
@@ -0,0 +1,27 @@
1
+ import { isDangerousPath, } from './util/index.js';
2
+ /**
3
+ * Decide whether a read tool call should be auto-allowed, blocked, or sent to
4
+ * the human-in-the-loop interrupt.
5
+ *
6
+ * Read tools auto-execute as long as the path is not dangerous:
7
+ * - no `filePath` in args → `allow` (e.g. profile_update_tool)
8
+ * - `filePath` matches a dangerous-path entry → `block` with a message
9
+ * that is returned to the LLM
10
+ * - otherwise → `allow` (paths outside cwd are read-only and safe to open)
11
+ */
12
+ export async function checkReadPermission(args, _options = {}) {
13
+ const filePath = args.filePath;
14
+ if (typeof filePath !== 'string' || filePath.length === 0) {
15
+ return { action: 'allow' };
16
+ }
17
+ if (isDangerousPath(filePath)) {
18
+ return {
19
+ action: 'block',
20
+ message: `⚠️ 路径 \`${filePath}\` 是危险路径,不允许访问。` +
21
+ `危险路径包含 ~/.ssh、~/.aws、~/.gnupg、/etc、/var 等敏感目录,` +
22
+ `以及 Windows 上的 %APPDATA%、C:\\Windows\\System32 等。` +
23
+ `请改用工作目录内的其他文件。`,
24
+ };
25
+ }
26
+ return { action: 'allow' };
27
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Check whether a shell command string would change the working directory
3
+ * during execution (via `cd` or `pushd`).
4
+ *
5
+ * The parser is deliberately conservative: it splits on common shell chain
6
+ * operators (`&&`, `||`, `;`, `|`, `&`) and inspects each segment for a
7
+ * leading `cd` / `pushd`. Quoting, subshells, and variable expansion are
8
+ * NOT understood, so a literal `cd /tmp` inside an `echo` will be flagged
9
+ * as a change. Erring on the safe side is the point.
10
+ */
11
+ export function commandChangesDirectory(command) {
12
+ const segments = command.split(/&&|\|\||;|\||&/);
13
+ for (const raw of segments) {
14
+ const segment = raw.trim();
15
+ if (!segment)
16
+ continue;
17
+ // The argument group is optional so bare `cd` (defaults to HOME in
18
+ // the shell) and bare `pushd` (swaps to the previous directory) are
19
+ // both detected as directory changes.
20
+ const match = segment.match(/^(?:cd|pushd)(?:\s+(.+?))?\s*$/);
21
+ if (!match)
22
+ continue;
23
+ const arg = match[1]?.trim().replace(/^["']|["']$/g, '');
24
+ if (arg === '.' || arg === './')
25
+ continue;
26
+ return { changes: true, segment };
27
+ }
28
+ return { changes: false };
29
+ }
@@ -0,0 +1,116 @@
1
+ {
2
+ "macos": [
3
+ "~/.ssh",
4
+ "~/.gnupg",
5
+ "~/.aws",
6
+ "~/.azure",
7
+ "~/.config/gcloud",
8
+ "~/.kube",
9
+ "~/.docker",
10
+ "~/.netrc",
11
+ "~/Library/Keychains",
12
+ "~/Downloads",
13
+ "~/Library/Application Support/Google/Chrome",
14
+ "~/Library/Application Support/Chromium",
15
+ "~/Library/Application Support/Firefox",
16
+ "~/Library/Application Support/Slack",
17
+ "~/Library/Application Support/Discord",
18
+ "~/Library/Application Support/Telegram",
19
+ "~/Library/Application Support/Signal",
20
+ "~/.zshrc",
21
+ "~/.bashrc",
22
+ "~/.bash_profile",
23
+ "~/.profile",
24
+ "~/.env",
25
+ "~/.env.*",
26
+ "~/.gitconfig",
27
+ "~/.git-credentials",
28
+ "~/.npmrc",
29
+ "~/.pnpmrc",
30
+ "~/.yarnrc",
31
+ "~/.pypirc",
32
+ "~/.m2",
33
+ "~/.gradle",
34
+ "~/.terraform",
35
+ "~/.config/gh",
36
+ "~/.config/gitlab",
37
+ "~/.circleci",
38
+ "~/.vercel",
39
+ "~/.netlify",
40
+ "~/Library/Containers",
41
+ "/private",
42
+ "/etc",
43
+ "/var"
44
+ ],
45
+ "linux": [
46
+ "~/.ssh",
47
+ "~/.gnupg",
48
+ "~/.pki",
49
+ "~/.aws",
50
+ "~/.azure",
51
+ "~/.config/gcloud",
52
+ "~/.kube",
53
+ "~/.docker",
54
+ "~/.netrc",
55
+ "~/.local/share/keyrings",
56
+ "~/.config/google-chrome",
57
+ "~/.config/chromium",
58
+ "~/.mozilla/firefox",
59
+ "~/.config/Slack",
60
+ "~/.config/discord",
61
+ "~/.local/share/TelegramDesktop",
62
+ "~/.zshrc",
63
+ "~/.bashrc",
64
+ "~/.bash_profile",
65
+ "~/.profile",
66
+ "~/.env",
67
+ "~/.env.*",
68
+ "~/.gitconfig",
69
+ "~/.git-credentials",
70
+ "~/.npmrc",
71
+ "~/.pnpmrc",
72
+ "~/.yarnrc",
73
+ "~/.pypirc",
74
+ "~/.m2",
75
+ "~/.gradle",
76
+ "~/.terraform",
77
+ "~/.config/gh",
78
+ "~/.config/gitlab",
79
+ "~/.circleci",
80
+ "~/.vercel",
81
+ "~/.netlify",
82
+ "/etc/shadow",
83
+ "/etc/sudoers",
84
+ "/proc",
85
+ "/sys",
86
+ "/var/lib",
87
+ "/root"
88
+ ],
89
+ "windows": [
90
+ "%USERPROFILE%\\.ssh",
91
+ "%USERPROFILE%\\.gnupg",
92
+ "%USERPROFILE%\\.aws",
93
+ "%USERPROFILE%\\.azure",
94
+ "%USERPROFILE%\\.kube",
95
+ "%USERPROFILE%\\.docker",
96
+ "%LOCALAPPDATA%\\Google\\Chrome\\User Data",
97
+ "%LOCALAPPDATA%\\Chromium\\User Data",
98
+ "%APPDATA%\\Mozilla\\Firefox",
99
+ "%APPDATA%\\Slack",
100
+ "%APPDATA%\\Discord",
101
+ "%APPDATA%\\Telegram Desktop",
102
+ "%USERPROFILE%\\.gitconfig",
103
+ "%USERPROFILE%\\.git-credentials",
104
+ "%USERPROFILE%\\.npmrc",
105
+ "%USERPROFILE%\\.pnpmrc",
106
+ "%USERPROFILE%\\.yarnrc",
107
+ "%USERPROFILE%\\.m2",
108
+ "%USERPROFILE%\\.gradle",
109
+ "%APPDATA%",
110
+ "%LOCALAPPDATA%",
111
+ "C:\\Windows\\System32",
112
+ "C:\\Windows\\System32\\config\\SAM",
113
+ "C:\\Windows\\System32\\config\\SECURITY",
114
+ "C:\\Windows\\System32\\config\\SYSTEM"
115
+ ]
116
+ }
@@ -0,0 +1,230 @@
1
+ import { isDangerousPath } from './is-dangerous-path.js';
2
+ // Command-name blacklist. Keys are lowercased before lookup so that
3
+ // Windows-style mixed-case invocations (`DEL`, `Remove-Item`) are caught.
4
+ const DANGEROUS_COMMANDS = {
5
+ // ---- 超级权限 ----
6
+ sudo: { category: 'sudo', description: '使用超级权限' },
7
+ su: { category: 'sudo', description: '切换到超级用户' },
8
+ doas: { category: 'sudo', description: '使用超级权限(BSD)' },
9
+ runas: { category: 'sudo', description: '以其他用户身份运行(Windows)' },
10
+ // ---- 删除文件/目录 ----
11
+ rm: { category: 'delete', description: '删除文件/目录' },
12
+ rmdir: { category: 'delete', description: '删除空目录' },
13
+ del: { category: 'delete', description: '删除文件(Windows CMD)' },
14
+ erase: { category: 'delete', description: '删除文件(Windows CMD)' },
15
+ rd: { category: 'delete', description: '删除目录(Windows CMD)' },
16
+ rmdir_cmd: { category: 'delete', description: '删除目录(Windows CMD)' },
17
+ 'remove-item': { category: 'delete', description: '删除项目(PowerShell)' },
18
+ ri: { category: 'delete', description: '删除项目(PowerShell, Remove-Item 的别名)' },
19
+ del_cmd: { category: 'delete', description: '删除项目(PowerShell)' },
20
+ // ---- 修改文件/目录 ----
21
+ mv: { category: 'modify', description: '移动/重命名文件' },
22
+ 'move-item': { category: 'modify', description: '移动/重命名项目(PowerShell)' },
23
+ mi: { category: 'modify', description: '移动/重命名项目(PowerShell, Move-Item 的别名)' },
24
+ 'rename-item': { category: 'modify', description: '重命名项目(PowerShell)' },
25
+ rni: { category: 'modify', description: '重命名项目(PowerShell, Rename-Item 的别名)' },
26
+ 'set-content': { category: 'modify', description: '覆盖写入文件(PowerShell)' },
27
+ sc_powershell: {
28
+ category: 'modify',
29
+ description: '覆盖写入文件(PowerShell, Set-Content 的别名)',
30
+ },
31
+ 'add-content': { category: 'modify', description: '追加写入文件(PowerShell)' },
32
+ ac: { category: 'modify', description: '追加写入文件(PowerShell, Add-Content 的别名)' },
33
+ 'clear-content': { category: 'modify', description: '清空文件内容(PowerShell)' },
34
+ clc: { category: 'modify', description: '清空文件内容(PowerShell, Clear-Content 的别名)' },
35
+ 'out-file': { category: 'modify', description: '写入文件(PowerShell)' },
36
+ of: { category: 'modify', description: '写入文件(PowerShell, Out-File 的别名)' },
37
+ 'new-item': { category: 'modify', description: '新建项目(PowerShell)' },
38
+ ni: { category: 'modify', description: '新建项目(PowerShell, New-Item 的别名)' },
39
+ tee: { category: 'modify', description: '从 stdin 写入文件' },
40
+ truncate: { category: 'modify', description: '截断文件' },
41
+ dd: { category: 'modify', description: '转换/复制文件' },
42
+ install: { category: 'modify', description: '复制文件并设置属性' },
43
+ // ---- 修改权限 ----
44
+ chmod: { category: 'permissions', description: '修改文件权限' },
45
+ chown: { category: 'permissions', description: '修改文件所有者' },
46
+ chgrp: { category: 'permissions', description: '修改文件所属组' },
47
+ icacls: { category: 'permissions', description: '修改文件 ACL(Windows)' },
48
+ cacls: { category: 'permissions', description: '修改文件 ACL(Windows 旧)' },
49
+ takeown: { category: 'permissions', description: '夺取文件所有权(Windows)' },
50
+ 'set-acl': { category: 'permissions', description: '设置文件 ACL(PowerShell)' },
51
+ setfacl: { category: 'permissions', description: '设置文件 ACL(Linux)' },
52
+ // ---- 控制进程/服务 ----
53
+ kill: { category: 'process', description: '结束进程' },
54
+ killall: { category: 'process', description: '按名称结束进程' },
55
+ pkill: { category: 'process', description: '按模式结束进程' },
56
+ 'stop-process': { category: 'process', description: '结束进程(PowerShell)' },
57
+ spps: { category: 'process', description: '结束进程(PowerShell, Stop-Process 的别名)' },
58
+ taskkill: { category: 'process', description: '结束进程(Windows)' },
59
+ sc: { category: 'process', description: '服务控制(Windows)' },
60
+ systemctl: { category: 'process', description: '系统服务管理(Linux)' },
61
+ launchctl: { category: 'process', description: '服务管理(macOS)' },
62
+ 'start-service': { category: 'process', description: '启动服务(PowerShell)' },
63
+ sasv: { category: 'process', description: '启动服务(PowerShell, Start-Service 的别名)' },
64
+ 'stop-service': { category: 'process', description: '停止服务(PowerShell)' },
65
+ spsv: { category: 'process', description: '停止服务(PowerShell, Stop-Service 的别名)' },
66
+ 'restart-service': { category: 'process', description: '重启服务(PowerShell)' },
67
+ rspsv: { category: 'process', description: '重启服务(PowerShell, Restart-Service 的别名)' },
68
+ service: { category: 'process', description: '服务管理(Linux 旧)' },
69
+ // ---- 修改用户信息 ----
70
+ useradd: { category: 'user', description: '添加用户' },
71
+ usermod: { category: 'user', description: '修改用户' },
72
+ userdel: { category: 'user', description: '删除用户' },
73
+ groupadd: { category: 'user', description: '添加组' },
74
+ groupmod: { category: 'user', description: '修改组' },
75
+ groupdel: { category: 'user', description: '删除组' },
76
+ passwd: { category: 'user', description: '修改密码' },
77
+ chsh: { category: 'user', description: '修改默认 shell' },
78
+ chfn: { category: 'user', description: '修改用户信息' },
79
+ gpasswd: { category: 'user', description: '管理组密码/成员' },
80
+ 'new-localuser': { category: 'user', description: '新建本地用户(PowerShell)' },
81
+ 'set-localuser': { category: 'user', description: '修改本地用户(PowerShell)' },
82
+ 'remove-localuser': { category: 'user', description: '删除本地用户(PowerShell)' },
83
+ 'add-localgroupmember': { category: 'user', description: '添加本地组成员(PowerShell)' },
84
+ 'remove-localgroupmember': { category: 'user', description: '移除本地组成员(PowerShell)' },
85
+ // ---- 网络/远程控制 ----
86
+ ssh: { category: 'network', description: '远程登录' },
87
+ scp: { category: 'network', description: '安全复制(远程)' },
88
+ sftp: { category: 'network', description: '安全 FTP' },
89
+ rsync: { category: 'network', description: '远程同步' },
90
+ curl: { category: 'network', description: '网络请求' },
91
+ wget: { category: 'network', description: '网络下载' },
92
+ nc: { category: 'network', description: '网络工具(netcat)' },
93
+ ncat: { category: 'network', description: '网络工具(ncat)' },
94
+ netcat: { category: 'network', description: '网络工具(netcat)' },
95
+ telnet: { category: 'network', description: '远程登录(telnet)' },
96
+ ftp: { category: 'network', description: 'FTP 客户端' },
97
+ 'ssh-keygen': { category: 'network', description: '生成 SSH 密钥' },
98
+ 'ssh-add': { category: 'network', description: '添加 SSH 密钥到 agent' },
99
+ 'ssh-agent': { category: 'network', description: 'SSH 密钥代理' },
100
+ 'invoke-webrequest': { category: 'network', description: '网络请求(PowerShell)' },
101
+ iwr: { category: 'network', description: '网络请求(PowerShell, Invoke-WebRequest 的别名)' },
102
+ curl_alias: {
103
+ category: 'network',
104
+ description: '网络请求(PowerShell, Invoke-WebRequest 的别名)',
105
+ },
106
+ 'invoke-restmethod': { category: 'network', description: 'REST 请求(PowerShell)' },
107
+ irm: { category: 'network', description: 'REST 请求(PowerShell, Invoke-RestMethod 的别名)' },
108
+ 'invoke-expression': {
109
+ category: 'network',
110
+ description: '执行表达式(PowerShell,常被远程命令利用)',
111
+ },
112
+ iex: { category: 'network', description: '执行表达式(PowerShell, Invoke-Expression 的别名)' },
113
+ downloadstring: { category: 'network', description: '下载字符串(WebClient,常被远程命令利用)' },
114
+ downloadfile: { category: 'network', description: '下载文件(WebClient,常被远程命令利用)' },
115
+ };
116
+ // Read-style commands whose arguments should be scanned for sensitive
117
+ // paths. Entries include both the canonical names and the common
118
+ // PowerShell aliases (lowercased).
119
+ const SENSITIVE_PATH_COMMANDS = new Set([
120
+ // POSIX
121
+ 'cat',
122
+ 'less',
123
+ 'more',
124
+ 'head',
125
+ 'tail',
126
+ 'strings',
127
+ 'file',
128
+ 'stat',
129
+ 'ls',
130
+ 'dir',
131
+ 'find',
132
+ 'tree',
133
+ 'pwd',
134
+ 'grep',
135
+ 'egrep',
136
+ 'fgrep',
137
+ 'awk',
138
+ 'cut',
139
+ 'tac',
140
+ 'rev',
141
+ 'sort',
142
+ 'uniq',
143
+ 'wc',
144
+ 'xxd',
145
+ 'od',
146
+ 'hexdump',
147
+ 'base64',
148
+ 'md5sum',
149
+ 'sha256sum',
150
+ // Windows CMD
151
+ 'type',
152
+ // PowerShell
153
+ 'get-content',
154
+ 'gc',
155
+ 'cat',
156
+ 'select-string',
157
+ 'sls',
158
+ 'get-childitem',
159
+ 'gci',
160
+ 'ls',
161
+ 'dir',
162
+ 'get-item',
163
+ 'gi',
164
+ 'get-acl',
165
+ 'read-host',
166
+ ]);
167
+ /**
168
+ * Check whether a shell command string performs one of the dangerous
169
+ * operations enumerated in `DANGEROUS_COMMANDS`, or whether a
170
+ * read-style command targets a sensitive filesystem location.
171
+ *
172
+ * First-token comparison is case-insensitive so that `DEL`, `Del`, and
173
+ * `del` are all recognized on Windows. Only the first match per chain
174
+ * segment is reported.
175
+ */
176
+ export function detectDangerousOperation(command) {
177
+ const segments = command.split(/&&|\|\||;|\||&/);
178
+ for (const raw of segments) {
179
+ const segment = raw.trim();
180
+ if (!segment)
181
+ continue;
182
+ const firstTokenMatch = segment.match(/^([^\s"']+)/);
183
+ if (!firstTokenMatch)
184
+ continue;
185
+ const firstToken = firstTokenMatch[1].toLowerCase();
186
+ // 1. Direct command-name blacklist hit.
187
+ const info = DANGEROUS_COMMANDS[firstToken];
188
+ if (info) {
189
+ return {
190
+ found: true,
191
+ category: info.category,
192
+ description: info.description,
193
+ segment,
194
+ };
195
+ }
196
+ // 2. Read-style command whose arguments touch a sensitive path.
197
+ if (SENSITIVE_PATH_COMMANDS.has(firstToken)) {
198
+ const tail = segment.slice(firstToken.length);
199
+ for (const token of extractPathTokens(tail)) {
200
+ if (isDangerousPath(token)) {
201
+ return {
202
+ found: true,
203
+ category: 'sensitive',
204
+ description: '访问敏感路径',
205
+ segment,
206
+ path: token,
207
+ };
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return { found: false };
213
+ }
214
+ // Tokenize the argument tail of a command into candidate paths. Quoted
215
+ // strings stay together, flags (`-x`, `--foo`) are dropped, and the
216
+ // shell redirect operator `>` is included so that
217
+ // `cat foo > /etc/passwd` still surfaces the sensitive target.
218
+ function extractPathTokens(args) {
219
+ const matches = args.match(/(?:"[^"]*"|'[^']*'|\S+)/g) ?? [];
220
+ const out = [];
221
+ for (const raw of matches) {
222
+ const stripped = raw.replace(/^["']|["']$/g, '');
223
+ if (stripped.length === 0)
224
+ continue;
225
+ if (stripped.startsWith('-'))
226
+ continue;
227
+ out.push(stripped);
228
+ }
229
+ return out;
230
+ }