lcagent-cli 0.1.6 → 0.1.8
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 +138 -1
- package/dist/app/bootstrap.d.ts +4 -1
- package/dist/app/bootstrap.js +2 -2
- package/dist/bin/cli.js +81 -8
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +2 -0
- package/dist/core/engine.d.ts +3 -2
- package/dist/core/engine.js +100 -10
- package/dist/core/loop.d.ts +7 -2
- package/dist/core/loop.js +9 -2
- package/dist/core/message.d.ts +9 -0
- package/dist/core/message.js +9 -0
- package/dist/core/systemPrompt.js +8 -0
- package/dist/tools/editFile.d.ts +13 -0
- package/dist/tools/editFile.js +305 -15
- package/dist/tools/editUtils.d.ts +15 -0
- package/dist/tools/editUtils.js +142 -0
- package/dist/tools/execute.js +56 -7
- package/dist/tools/permissions.d.ts +12 -0
- package/dist/tools/permissions.js +105 -0
- package/dist/tools/runShell.js +61 -10
- package/dist/tools/types.d.ts +28 -0
- package/package.json +3 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isAbsolute, normalize, relative, resolve, sep } from 'node:path';
|
|
2
|
+
export const DANGEROUS_FILES = [
|
|
3
|
+
'.gitconfig',
|
|
4
|
+
'.gitmodules',
|
|
5
|
+
'.bashrc',
|
|
6
|
+
'.bash_profile',
|
|
7
|
+
'.zshrc',
|
|
8
|
+
'.zprofile',
|
|
9
|
+
'.profile',
|
|
10
|
+
'.mcp.json',
|
|
11
|
+
'.claude.json',
|
|
12
|
+
];
|
|
13
|
+
export const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea', '.claude'];
|
|
14
|
+
function normalizeCaseForComparison(filePath) {
|
|
15
|
+
return filePath.toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function resolveTargetPath(cwd, targetPath) {
|
|
18
|
+
return isAbsolute(targetPath) ? normalize(targetPath) : normalize(resolve(cwd, targetPath));
|
|
19
|
+
}
|
|
20
|
+
function toDisplayPath(cwd, absolutePath) {
|
|
21
|
+
const relativePath = relative(cwd, absolutePath);
|
|
22
|
+
if (!relativePath || relativePath.startsWith(`..${sep}`) || relativePath === '..') {
|
|
23
|
+
return absolutePath;
|
|
24
|
+
}
|
|
25
|
+
return relativePath;
|
|
26
|
+
}
|
|
27
|
+
function isDangerousPath(absolutePath) {
|
|
28
|
+
const normalizedPath = normalizeCaseForComparison(absolutePath);
|
|
29
|
+
const pathSegments = normalizedPath.split(/[\\/]+/).filter(Boolean);
|
|
30
|
+
const fileName = pathSegments[pathSegments.length - 1];
|
|
31
|
+
if (fileName && DANGEROUS_FILES.includes(fileName)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return pathSegments.some(segment => DANGEROUS_DIRECTORIES.includes(segment));
|
|
35
|
+
}
|
|
36
|
+
function extractPatchTarget(patchText) {
|
|
37
|
+
const lines = patchText.split(/\r?\n/);
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (line.startsWith('+++ ') || line.startsWith('--- ')) {
|
|
40
|
+
const rawPath = line.slice(4).trim();
|
|
41
|
+
if (!rawPath || rawPath === '/dev/null') {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
return rawPath.replace(/^(a|b)\//, '');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function extractCandidateTargets(toolName, input) {
|
|
50
|
+
if (!input || typeof input !== 'object') {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const record = input;
|
|
54
|
+
const directPath = [record.path, record.filePath, record.file_path]
|
|
55
|
+
.find(value => typeof value === 'string' && value.trim().length > 0);
|
|
56
|
+
if (toolName === 'grep') {
|
|
57
|
+
const searchPath = [record.path, record.directory]
|
|
58
|
+
.find(value => typeof value === 'string' && value.trim().length > 0);
|
|
59
|
+
return searchPath ? [String(searchPath)] : [];
|
|
60
|
+
}
|
|
61
|
+
if (toolName === 'run_shell') {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const patchText = [record.patch, record.diff, record.unifiedDiff, record.unified_diff]
|
|
65
|
+
.find(value => typeof value === 'string' && value.trim().length > 0);
|
|
66
|
+
const patchPath = typeof patchText === 'string' ? extractPatchTarget(patchText) : null;
|
|
67
|
+
return [directPath, patchPath].filter((value) => Boolean(value));
|
|
68
|
+
}
|
|
69
|
+
export function buildApprovalRequest(tool, input, context) {
|
|
70
|
+
if (context.approvalMode === 'auto') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (tool.isReadOnly) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const candidateTargets = extractCandidateTargets(tool.name, input);
|
|
77
|
+
const absoluteTargets = candidateTargets.map(target => resolveTargetPath(context.cwd, target));
|
|
78
|
+
const displayTargets = absoluteTargets.map(target => toDisplayPath(context.cwd, target));
|
|
79
|
+
const hasDangerousTarget = absoluteTargets.some(isDangerousPath);
|
|
80
|
+
if (tool.name === 'run_shell') {
|
|
81
|
+
const command = input && typeof input === 'object'
|
|
82
|
+
? input.command ?? input.cmd
|
|
83
|
+
: undefined;
|
|
84
|
+
return {
|
|
85
|
+
toolName: tool.name,
|
|
86
|
+
summary: `Shell execution requested${typeof command === 'string' ? `: ${command}` : '.'}`,
|
|
87
|
+
reason: 'run_shell can execute arbitrary commands and modify the filesystem or environment.',
|
|
88
|
+
targets: [],
|
|
89
|
+
risk: 'high',
|
|
90
|
+
input,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
toolName: tool.name,
|
|
95
|
+
summary: displayTargets.length > 0
|
|
96
|
+
? `Write access requested for ${displayTargets.join(', ')}`
|
|
97
|
+
: 'Write access requested.',
|
|
98
|
+
reason: hasDangerousTarget
|
|
99
|
+
? 'Target includes a protected configuration or metadata path from the Claude Code-style danger list.'
|
|
100
|
+
: 'This tool can modify files on disk.',
|
|
101
|
+
targets: displayTargets,
|
|
102
|
+
risk: hasDangerousTarget ? 'high' : 'medium',
|
|
103
|
+
input,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/tools/runShell.js
CHANGED
|
@@ -6,9 +6,60 @@ const inputSchema = z.object({
|
|
|
6
6
|
command: z.string().min(1).optional(),
|
|
7
7
|
cmd: z.string().min(1).optional(),
|
|
8
8
|
});
|
|
9
|
+
function expandBracePattern(pattern) {
|
|
10
|
+
const match = pattern.match(/^(.*)\{([^{}]+)\}(.*)$/);
|
|
11
|
+
if (!match) {
|
|
12
|
+
return [pattern];
|
|
13
|
+
}
|
|
14
|
+
const [, prefix, variants, suffix] = match;
|
|
15
|
+
return variants
|
|
16
|
+
.split(',')
|
|
17
|
+
.flatMap(variant => expandBracePattern(`${prefix}${variant}${suffix}`));
|
|
18
|
+
}
|
|
19
|
+
function tokenizeShellArgs(command) {
|
|
20
|
+
return command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
21
|
+
}
|
|
22
|
+
function rewriteWindowsMkdirSegment(segment) {
|
|
23
|
+
const trimmed = segment.trim();
|
|
24
|
+
const mkdirMatch = trimmed.match(/^mkdir\s+-p\s+(.+)$/i);
|
|
25
|
+
if (!mkdirMatch) {
|
|
26
|
+
return segment;
|
|
27
|
+
}
|
|
28
|
+
const rawArgs = tokenizeShellArgs(mkdirMatch[1]);
|
|
29
|
+
const expandedArgs = rawArgs.flatMap(arg => {
|
|
30
|
+
const unquoted = arg.replace(/^(["'])(.*)\1$/, '$2');
|
|
31
|
+
return expandBracePattern(unquoted);
|
|
32
|
+
});
|
|
33
|
+
if (expandedArgs.length === 0) {
|
|
34
|
+
return segment;
|
|
35
|
+
}
|
|
36
|
+
return expandedArgs
|
|
37
|
+
.map(path => `mkdir "${path}"`)
|
|
38
|
+
.join(' && ');
|
|
39
|
+
}
|
|
40
|
+
function normalizeWindowsCommand(command) {
|
|
41
|
+
const segments = command.split(/\s*&&\s*/);
|
|
42
|
+
return segments.map(rewriteWindowsMkdirSegment).join(' && ');
|
|
43
|
+
}
|
|
44
|
+
function buildWindowsShellHint(command) {
|
|
45
|
+
const hints = [];
|
|
46
|
+
if (/mkdir\s+-p/i.test(command)) {
|
|
47
|
+
hints.push('`cmd.exe` 不支持 `mkdir -p`;Windows 下可直接用 `mkdir dir` 创建多级目录。');
|
|
48
|
+
}
|
|
49
|
+
if (/\{[^{}]+,[^{}]+\}/.test(command)) {
|
|
50
|
+
hints.push('`cmd.exe` 不支持 Bash 的 `{a,b}` brace expansion。');
|
|
51
|
+
}
|
|
52
|
+
if (/<<\s*['"]?\w+['"]?/.test(command)) {
|
|
53
|
+
hints.push('`cmd.exe` 不支持 Bash heredoc(如 `cat <<EOF`)。');
|
|
54
|
+
}
|
|
55
|
+
if (hints.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return ['Windows shell 提示:', ...hints, '优先使用 Windows `cmd`/PowerShell 语法,或直接使用 `edit_file` 写文件。'].join('\n');
|
|
59
|
+
}
|
|
9
60
|
export const runShellTool = {
|
|
10
61
|
name: 'run_shell',
|
|
11
|
-
description: 'Run a shell command in the current working directory.',
|
|
62
|
+
description: 'Run a shell command in the current working directory. On Windows, prefer cmd/PowerShell syntax rather than Bash-only syntax like mkdir -p, brace expansion, or heredoc.',
|
|
12
63
|
inputSchema,
|
|
13
64
|
inputSchemaJson: {
|
|
14
65
|
type: 'object',
|
|
@@ -23,20 +74,17 @@ export const runShellTool = {
|
|
|
23
74
|
},
|
|
24
75
|
isReadOnly: false,
|
|
25
76
|
async execute(input, context) {
|
|
26
|
-
if (context.approvalMode === 'manual') {
|
|
27
|
-
return {
|
|
28
|
-
isError: true,
|
|
29
|
-
content: 'run_shell is blocked in manual approval mode. Switch config approvalMode to auto to enable shell execution.',
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
77
|
try {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
78
|
+
const rawCommand = input.command ?? input.cmd;
|
|
79
|
+
if (!rawCommand) {
|
|
35
80
|
return {
|
|
36
81
|
isError: true,
|
|
37
82
|
content: 'Missing required field: command',
|
|
38
83
|
};
|
|
39
84
|
}
|
|
85
|
+
const command = process.platform === 'win32'
|
|
86
|
+
? normalizeWindowsCommand(rawCommand)
|
|
87
|
+
: rawCommand;
|
|
40
88
|
const { stdout, stderr } = await execAsync(command, {
|
|
41
89
|
cwd: context.cwd,
|
|
42
90
|
shell: process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh',
|
|
@@ -49,9 +97,12 @@ export const runShellTool = {
|
|
|
49
97
|
}
|
|
50
98
|
catch (error) {
|
|
51
99
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
const shellHint = process.platform === 'win32'
|
|
101
|
+
? buildWindowsShellHint(input.command ?? input.cmd ?? '')
|
|
102
|
+
: null;
|
|
52
103
|
return {
|
|
53
104
|
isError: true,
|
|
54
|
-
content: message,
|
|
105
|
+
content: shellHint ? `${message}\n\n${shellHint}` : message,
|
|
55
106
|
};
|
|
56
107
|
}
|
|
57
108
|
},
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -2,10 +2,38 @@ import type { z } from 'zod';
|
|
|
2
2
|
export type ToolExecutionResult = {
|
|
3
3
|
content: string;
|
|
4
4
|
isError?: boolean;
|
|
5
|
+
meta?: ToolExecutionMeta;
|
|
6
|
+
};
|
|
7
|
+
export type ToolFailureStage = 'lookup' | 'validation' | 'approval' | 'execution';
|
|
8
|
+
export type ToolApprovalRequest = {
|
|
9
|
+
toolName: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
targets: string[];
|
|
13
|
+
risk: 'low' | 'medium' | 'high';
|
|
14
|
+
input: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type ToolApprovalResponse = {
|
|
17
|
+
approved: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
};
|
|
20
|
+
export type ToolApprovalMeta = {
|
|
21
|
+
mode: 'auto' | 'manual';
|
|
22
|
+
required: boolean;
|
|
23
|
+
approved?: boolean;
|
|
24
|
+
risk?: 'low' | 'medium' | 'high';
|
|
25
|
+
targets?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type ToolExecutionMeta = {
|
|
28
|
+
cwd: string;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
approval: ToolApprovalMeta;
|
|
31
|
+
failureStage?: ToolFailureStage;
|
|
5
32
|
};
|
|
6
33
|
export type ToolContext = {
|
|
7
34
|
cwd: string;
|
|
8
35
|
approvalMode: 'auto' | 'manual';
|
|
36
|
+
requestApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResponse>;
|
|
9
37
|
};
|
|
10
38
|
export type AnthropicToolDefinition = {
|
|
11
39
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lcagent-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "A minimal coding agent CLI for terminal-based coding workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dev": "tsx src/bin/cli.ts",
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
20
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"smoke": "npm run build && node ./scripts/smoke.mjs",
|
|
21
22
|
"start": "node dist/bin/cli.js",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
23
24
|
},
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
],
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"commander": "^11.1.0",
|
|
36
|
+
"diff": "^8.0.4",
|
|
35
37
|
"undici": "^5.28.5",
|
|
36
38
|
"zod": "^4.1.5"
|
|
37
39
|
},
|