lcagent-cli 0.1.7 → 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 CHANGED
@@ -34,6 +34,7 @@
34
34
  `edit_file` 现在有两种模式:
35
35
 
36
36
  - 文本替换模式:传 `path + oldText + newText`
37
+ - 新建/整文件写入模式:传 `path + content`;若覆盖已有文件,额外传 `replaceEntireFile=true`
37
38
  - Patch 模式:传 `patch`,也可以额外传 `path`
38
39
 
39
40
  Patch 模式当前限制:
@@ -42,6 +43,11 @@ Patch 模式当前限制:
42
43
  - patch 需要能对当前文件内容干净应用
43
44
  - 如果 `path` 与 patch 头里的目标文件不一致,会直接报错
44
45
 
46
+ Windows 下如果模型需要创建目录或文件,优先建议:
47
+
48
+ - 用 `run_shell` 执行 Windows 兼容命令,而不是 Bash 专用语法
49
+ - 或者直接用 `edit_file` 的 `content` 模式创建文件
50
+
45
51
  ## 安装
46
52
 
47
53
  ```bash
@@ -1,11 +1,19 @@
1
1
  export function buildSystemPrompt(cwd) {
2
+ const platform = process.platform;
3
+ const platformGuidance = platform === 'win32'
4
+ ? 'Current platform is Windows. For run_shell, use cmd/PowerShell-compatible syntax. Avoid Bash-only constructs such as mkdir -p, brace expansion {a,b}, and heredoc blocks.'
5
+ : 'Current platform is Unix-like. Use standard POSIX shell syntax for run_shell.';
2
6
  return [
3
7
  'You are a coding agent running inside a local CLI.',
4
8
  'Prefer using tools when they help answer precisely.',
5
9
  'Do not invent file contents or command output when tools can verify them.',
6
10
  'Before editing code, read the relevant files.',
11
+ 'Use valid structured tool calls only. Never output literal pseudo-tags like <tool_call> or XML wrappers.',
12
+ 'Prefer edit_file for creating or updating files instead of shell redirection when possible.',
7
13
  'Explain briefly and act directly.',
8
14
  `Current working directory: ${cwd}`,
15
+ `Current platform: ${platform}`,
16
+ platformGuidance,
9
17
  'Available tools may read files, edit files, search files, and run shell commands.',
10
18
  ].join('\n');
11
19
  }
@@ -4,6 +4,11 @@ declare const inputSchema: z.ZodObject<{
4
4
  path: z.ZodOptional<z.ZodString>;
5
5
  filePath: z.ZodOptional<z.ZodString>;
6
6
  file_path: z.ZodOptional<z.ZodString>;
7
+ content: z.ZodOptional<z.ZodString>;
8
+ fileContent: z.ZodOptional<z.ZodString>;
9
+ file_content: z.ZodOptional<z.ZodString>;
10
+ replaceEntireFile: z.ZodOptional<z.ZodBoolean>;
11
+ replace_entire_file: z.ZodOptional<z.ZodBoolean>;
7
12
  patch: z.ZodOptional<z.ZodString>;
8
13
  diff: z.ZodOptional<z.ZodString>;
9
14
  unifiedDiff: z.ZodOptional<z.ZodString>;
@@ -7,6 +7,11 @@ const inputSchema = z.object({
7
7
  path: z.string().min(1).optional(),
8
8
  filePath: z.string().min(1).optional(),
9
9
  file_path: z.string().min(1).optional(),
10
+ content: z.string().optional(),
11
+ fileContent: z.string().optional(),
12
+ file_content: z.string().optional(),
13
+ replaceEntireFile: z.boolean().optional(),
14
+ replace_entire_file: z.boolean().optional(),
10
15
  patch: z.string().optional(),
11
16
  diff: z.string().optional(),
12
17
  unifiedDiff: z.string().optional(),
@@ -138,12 +143,20 @@ function buildNotFoundDiagnostic(source, oldText) {
138
143
  }
139
144
  export const editFileTool = {
140
145
  name: 'edit_file',
141
- description: 'Edit a file either by replacing text or by applying a single-file unified diff patch.',
146
+ description: 'Edit a file by replacing text, applying a single-file unified diff patch, or creating/replacing a whole file from content.',
142
147
  inputSchema,
143
148
  inputSchemaJson: {
144
149
  type: 'object',
145
150
  properties: {
146
151
  path: { type: 'string' },
152
+ content: {
153
+ type: 'string',
154
+ description: 'Full file contents to write. Use for creating a new file, or set replaceEntireFile=true to overwrite an existing file.',
155
+ },
156
+ replaceEntireFile: {
157
+ type: 'boolean',
158
+ description: 'Required with content mode when overwriting an existing file.',
159
+ },
147
160
  patch: {
148
161
  type: 'string',
149
162
  description: 'Unified diff patch for a single file. When provided, oldText/newText are not required.',
@@ -161,21 +174,33 @@ export const editFileTool = {
161
174
  isReadOnly: false,
162
175
  async execute(input, context) {
163
176
  const normalizedPath = input.path ?? input.filePath ?? input.file_path;
177
+ const fullContent = input.content ?? input.fileContent ?? input.file_content;
178
+ const replaceEntireFile = input.replaceEntireFile ?? input.replace_entire_file ?? false;
164
179
  const patchText = input.patch ?? input.diff ?? input.unifiedDiff ?? input.unified_diff;
165
180
  const oldText = input.oldText ?? input.old_text;
166
181
  const newText = input.newText ?? input.new_text;
167
182
  const replaceAll = input.replaceAll ?? input.replace_all ?? false;
168
183
  const occurrence = input.occurrence ?? input.occurrence_index;
169
- if (patchText !== undefined && (oldText !== undefined || newText !== undefined)) {
184
+ if (patchText !== undefined &&
185
+ (oldText !== undefined || newText !== undefined || fullContent !== undefined)) {
170
186
  return {
171
187
  isError: true,
172
- content: 'Use either patch mode or oldText/newText mode, not both in the same call.',
188
+ content: 'Use exactly one edit mode: patch, oldText/newText, or content.',
173
189
  };
174
190
  }
175
- if (patchText === undefined && (!normalizedPath || oldText === undefined || newText === undefined)) {
191
+ if (fullContent !== undefined &&
192
+ (oldText !== undefined || newText !== undefined)) {
176
193
  return {
177
194
  isError: true,
178
- content: 'Missing required fields. Use path + oldText + newText, or provide patch (and optionally path).',
195
+ content: 'Use either content mode or oldText/newText mode, not both in the same call.',
196
+ };
197
+ }
198
+ if (patchText === undefined &&
199
+ fullContent === undefined &&
200
+ (!normalizedPath || oldText === undefined || newText === undefined)) {
201
+ return {
202
+ isError: true,
203
+ content: 'Missing required fields. Use path + oldText + newText, or path + content, or provide patch (and optionally path).',
179
204
  };
180
205
  }
181
206
  if (patchText === undefined && oldText !== undefined && oldText.length === 0) {
@@ -227,9 +252,27 @@ export const editFileTool = {
227
252
  current = await readFile(absolutePath, 'utf8');
228
253
  }
229
254
  catch (error) {
255
+ if (fullContent !== undefined) {
256
+ current = '';
257
+ }
258
+ else {
259
+ return {
260
+ isError: true,
261
+ content: error instanceof Error ? error.message : String(error),
262
+ };
263
+ }
264
+ }
265
+ if (fullContent !== undefined) {
266
+ const fileExists = current.length > 0;
267
+ if (fileExists && !replaceEntireFile) {
268
+ return {
269
+ isError: true,
270
+ content: 'Content mode can create a new file, but overwriting an existing file requires replaceEntireFile=true.',
271
+ };
272
+ }
273
+ await writeFile(absolutePath, fullContent, 'utf8');
230
274
  return {
231
- isError: true,
232
- content: error instanceof Error ? error.message : String(error),
275
+ content: `${fileExists ? 'Replaced' : 'Created'} ${resolvedPath}.`,
233
276
  };
234
277
  }
235
278
  if (patchText !== undefined) {
@@ -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',
@@ -24,13 +75,16 @@ export const runShellTool = {
24
75
  isReadOnly: false,
25
76
  async execute(input, context) {
26
77
  try {
27
- const command = input.command ?? input.cmd;
28
- if (!command) {
78
+ const rawCommand = input.command ?? input.cmd;
79
+ if (!rawCommand) {
29
80
  return {
30
81
  isError: true,
31
82
  content: 'Missing required field: command',
32
83
  };
33
84
  }
85
+ const command = process.platform === 'win32'
86
+ ? normalizeWindowsCommand(rawCommand)
87
+ : rawCommand;
34
88
  const { stdout, stderr } = await execAsync(command, {
35
89
  cwd: context.cwd,
36
90
  shell: process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh',
@@ -43,9 +97,12 @@ export const runShellTool = {
43
97
  }
44
98
  catch (error) {
45
99
  const message = error instanceof Error ? error.message : String(error);
100
+ const shellHint = process.platform === 'win32'
101
+ ? buildWindowsShellHint(input.command ?? input.cmd ?? '')
102
+ : null;
46
103
  return {
47
104
  isError: true,
48
- content: message,
105
+ content: shellHint ? `${message}\n\n${shellHint}` : message,
49
106
  };
50
107
  }
51
108
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lcagent-cli",
3
- "version": "0.1.7",
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": {