lcagent-cli 0.1.4 → 0.1.6

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
@@ -139,6 +139,14 @@ npm run start -- model
139
139
  npm run start -- doctor
140
140
  ```
141
141
 
142
+ 当模型触发工具时,CLI 会打印:
143
+
144
+ - `[tool-call] 工具名`:后面跟模型传入的原始 JSON 参数
145
+ - `[tool-result] 工具名`:显示工具执行结果
146
+ - `[tool-result] 工具名 (error)`:显示工具失败原因
147
+
148
+ 这样可以直接看出是模型参数字段名不对、路径不对,还是工具执行本身报错。
149
+
142
150
  ## 设计目标
143
151
 
144
152
  - 保留 Claude Code 式的核心代理循环
@@ -165,4 +173,4 @@ npm run start -- doctor
165
173
  npm run start -- doctor
166
174
  ```
167
175
 
168
- `doctor` 会额外探测多个端点(如 `/v1`、`/v1/models`、`/v1/chat/completions`),并尽量打印底层网络错误,方便区分 DNS、TCP 超时、HTTP 返回和认证问题。
176
+ `doctor` 会额外探测多个端点(如 `/v1`、`/v1/models`、`/v1/chat/completions`),并尽量打印底层网络错误;对于 OpenAI-compatible 服务,还会额外检查 tool calling 是否真的返回 `tool_calls`。
package/dist/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ import { fetch } from '../core/http.js';
7
7
  import { agentConfigSchema } from '../config/schema.js';
8
8
  import { getConfigPath, loadConfig, updateConfig } from '../config/store.js';
9
9
  import { getDefaultTools } from '../tools/registry.js';
10
+ import { toOpenAICompatibleTool } from '../tools/types.js';
10
11
  function trimTrailingSlash(value) {
11
12
  return value.replace(/\/+$/, '');
12
13
  }
@@ -45,6 +46,32 @@ function questionAsync(rl, prompt) {
45
46
  });
46
47
  });
47
48
  }
49
+ function formatToolInput(input) {
50
+ try {
51
+ return JSON.stringify(input, null, 2);
52
+ }
53
+ catch {
54
+ return String(input);
55
+ }
56
+ }
57
+ function printEvent(event) {
58
+ switch (event.type) {
59
+ case 'status':
60
+ console.log(`\n[status] ${event.message}`);
61
+ break;
62
+ case 'assistant_text':
63
+ console.log(`\n${event.text}`);
64
+ break;
65
+ case 'tool_call':
66
+ console.log(`\n[tool-call] ${event.toolName}`);
67
+ console.log(formatToolInput(event.input));
68
+ break;
69
+ case 'tool_result':
70
+ console.log(`\n[tool-result] ${event.toolName}${event.isError ? ' (error)' : ''}`);
71
+ console.log(event.result);
72
+ break;
73
+ }
74
+ }
48
75
  async function probeUrl(url) {
49
76
  try {
50
77
  const response = await fetchWithTimeout(url, {
@@ -80,6 +107,55 @@ async function runJsonProbe(params) {
80
107
  console.log(` failed: ${formatError(error)}`);
81
108
  }
82
109
  }
110
+ async function runOpenAIToolCallingProbe(params) {
111
+ const tool = toOpenAICompatibleTool(getDefaultTools()[0]);
112
+ console.log(`- POST /v1/chat/completions (tool calling): ${params.url}`);
113
+ try {
114
+ const response = await fetchWithTimeout(params.url, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'content-type': 'application/json',
118
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
119
+ },
120
+ body: JSON.stringify({
121
+ model: params.model,
122
+ messages: [
123
+ {
124
+ role: 'user',
125
+ content: 'Call the read_file tool with path set to README.md. Do not answer with plain text only.',
126
+ },
127
+ ],
128
+ tools: [tool],
129
+ tool_choice: 'auto',
130
+ max_tokens: 128,
131
+ }),
132
+ }, 8000);
133
+ const text = await response.text();
134
+ console.log(` HTTP ${response.status}`);
135
+ console.log(` preview: ${text.slice(0, 300) || '(empty body)'}`);
136
+ try {
137
+ const parsed = JSON.parse(text);
138
+ const toolCalls = parsed.choices?.[0]?.message?.tool_calls;
139
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
140
+ console.log(` tool calling: supported (${toolCalls.length} tool call returned)`);
141
+ }
142
+ else {
143
+ const finishReason = parsed.choices?.[0]?.finish_reason ?? 'unknown';
144
+ const content = parsed.choices?.[0]?.message?.content ?? '';
145
+ console.log(` tool calling: no tool_calls returned (finish_reason=${finishReason})`);
146
+ if (content) {
147
+ console.log(` assistant content: ${String(content).slice(0, 160)}`);
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ console.log(' tool calling: unable to parse JSON response body');
153
+ }
154
+ }
155
+ catch (error) {
156
+ console.log(` failed: ${formatError(error)}`);
157
+ }
158
+ }
83
159
  async function runDoctor() {
84
160
  const config = await loadConfig();
85
161
  const apiKey = config.apiKey ??
@@ -124,6 +200,11 @@ async function runDoctor() {
124
200
  max_tokens: 8,
125
201
  }),
126
202
  });
203
+ await runOpenAIToolCallingProbe({
204
+ url: `${trimmedBaseUrl}/chat/completions`,
205
+ apiKey,
206
+ model: config.model,
207
+ });
127
208
  return;
128
209
  }
129
210
  await runJsonProbe({
@@ -150,21 +231,7 @@ async function runDoctor() {
150
231
  async function printAgentRun(prompt) {
151
232
  const { engine } = await createApp(process.cwd());
152
233
  for await (const event of engine.submit(prompt)) {
153
- switch (event.type) {
154
- case 'status':
155
- console.log(`\n[status] ${event.message}`);
156
- break;
157
- case 'assistant_text':
158
- console.log(`\n${event.text}`);
159
- break;
160
- case 'tool_call':
161
- console.log(`\n[tool] ${event.toolName} ${JSON.stringify(event.input)}`);
162
- break;
163
- case 'tool_result':
164
- console.log(`\n[tool-result] ${event.toolName}${event.isError ? ' (error)' : ''}`);
165
- console.log(event.result);
166
- break;
167
- }
234
+ printEvent(event);
168
235
  }
169
236
  }
170
237
  async function runChat() {
@@ -182,21 +249,7 @@ async function runChat() {
182
249
  break;
183
250
  }
184
251
  for await (const event of engine.submit(line)) {
185
- switch (event.type) {
186
- case 'status':
187
- console.log(`[status] ${event.message}`);
188
- break;
189
- case 'assistant_text':
190
- console.log(event.text);
191
- break;
192
- case 'tool_call':
193
- console.log(`[tool] ${event.toolName} ${JSON.stringify(event.input)}`);
194
- break;
195
- case 'tool_result':
196
- console.log(`[tool-result] ${event.toolName}${event.isError ? ' (error)' : ''}`);
197
- console.log(event.result);
198
- break;
199
- }
252
+ printEvent(event);
200
253
  }
201
254
  }
202
255
  }
@@ -1,9 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDefinition } from './types.js';
3
3
  declare const inputSchema: z.ZodObject<{
4
- path: z.ZodString;
5
- oldText: z.ZodString;
6
- newText: z.ZodString;
4
+ path: z.ZodOptional<z.ZodString>;
5
+ filePath: z.ZodOptional<z.ZodString>;
6
+ file_path: z.ZodOptional<z.ZodString>;
7
+ oldText: z.ZodOptional<z.ZodString>;
8
+ old_text: z.ZodOptional<z.ZodString>;
9
+ newText: z.ZodOptional<z.ZodString>;
10
+ new_text: z.ZodOptional<z.ZodString>;
7
11
  }, z.core.$strip>;
8
12
  export declare const editFileTool: ToolDefinition<z.infer<typeof inputSchema>>;
9
13
  export {};
@@ -2,9 +2,13 @@ import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  const inputSchema = z.object({
5
- path: z.string().min(1),
6
- oldText: z.string(),
7
- newText: z.string(),
5
+ path: z.string().min(1).optional(),
6
+ filePath: z.string().min(1).optional(),
7
+ file_path: z.string().min(1).optional(),
8
+ oldText: z.string().optional(),
9
+ old_text: z.string().optional(),
10
+ newText: z.string().optional(),
11
+ new_text: z.string().optional(),
8
12
  });
9
13
  function resolvePath(cwd, filePath) {
10
14
  return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
@@ -31,18 +35,27 @@ export const editFileTool = {
31
35
  content: 'edit_file is blocked in manual approval mode. Switch config approvalMode to auto to enable file edits.',
32
36
  };
33
37
  }
34
- const absolutePath = resolvePath(context.cwd, input.path);
38
+ const normalizedPath = input.path ?? input.filePath ?? input.file_path;
39
+ const oldText = input.oldText ?? input.old_text;
40
+ const newText = input.newText ?? input.new_text;
41
+ if (!normalizedPath || oldText === undefined || newText === undefined) {
42
+ return {
43
+ isError: true,
44
+ content: 'Missing required fields: path, oldText, or newText',
45
+ };
46
+ }
47
+ const absolutePath = resolvePath(context.cwd, normalizedPath);
35
48
  const current = await readFile(absolutePath, 'utf8');
36
- if (!current.includes(input.oldText)) {
49
+ if (!current.includes(oldText)) {
37
50
  return {
38
51
  isError: true,
39
52
  content: 'oldText was not found in the target file.',
40
53
  };
41
54
  }
42
- const next = current.replace(input.oldText, input.newText);
55
+ const next = current.replace(oldText, newText);
43
56
  await writeFile(absolutePath, next, 'utf8');
44
57
  return {
45
- content: `Updated ${input.path}`,
58
+ content: `Updated ${normalizedPath}`,
46
59
  };
47
60
  },
48
61
  };
@@ -13,5 +13,13 @@ export async function executeToolCall(toolUse, tools, context) {
13
13
  content: parsed.error.message,
14
14
  };
15
15
  }
16
- return tool.execute(parsed.data, context);
16
+ try {
17
+ return await tool.execute(parsed.data, context);
18
+ }
19
+ catch (error) {
20
+ return {
21
+ isError: true,
22
+ content: error instanceof Error ? error.message : String(error),
23
+ };
24
+ }
17
25
  }
@@ -1,10 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDefinition } from './types.js';
3
3
  declare const inputSchema: z.ZodObject<{
4
- pattern: z.ZodString;
4
+ pattern: z.ZodOptional<z.ZodString>;
5
+ query: z.ZodOptional<z.ZodString>;
5
6
  path: z.ZodOptional<z.ZodString>;
7
+ directory: z.ZodOptional<z.ZodString>;
6
8
  maxResults: z.ZodOptional<z.ZodNumber>;
9
+ max_results: z.ZodOptional<z.ZodNumber>;
7
10
  isRegex: z.ZodOptional<z.ZodBoolean>;
11
+ is_regex: z.ZodOptional<z.ZodBoolean>;
8
12
  }, z.core.$strip>;
9
13
  export declare const grepTool: ToolDefinition<z.infer<typeof inputSchema>>;
10
14
  export {};
@@ -2,10 +2,14 @@ import { readdir, readFile } from 'node:fs/promises';
2
2
  import { extname, isAbsolute, join, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  const inputSchema = z.object({
5
- pattern: z.string().min(1),
5
+ pattern: z.string().min(1).optional(),
6
+ query: z.string().min(1).optional(),
6
7
  path: z.string().optional(),
8
+ directory: z.string().optional(),
7
9
  maxResults: z.number().int().positive().max(200).optional(),
10
+ max_results: z.number().int().positive().max(200).optional(),
8
11
  isRegex: z.boolean().optional(),
12
+ is_regex: z.boolean().optional(),
9
13
  });
10
14
  const textExtensions = new Set([
11
15
  '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt', '.yml', '.yaml', '.css', '.html', '.mjs', '.cjs', '.sh', '.py', '.java', '.go', '.rs'
@@ -44,17 +48,26 @@ export const grepTool = {
44
48
  },
45
49
  isReadOnly: true,
46
50
  async execute(input, context) {
47
- const root = input.path
48
- ? isAbsolute(input.path)
49
- ? input.path
50
- : resolve(context.cwd, input.path)
51
+ const pattern = input.pattern ?? input.query;
52
+ if (!pattern) {
53
+ return {
54
+ isError: true,
55
+ content: 'Missing required field: pattern',
56
+ };
57
+ }
58
+ const searchPath = input.path ?? input.directory;
59
+ const root = searchPath
60
+ ? isAbsolute(searchPath)
61
+ ? searchPath
62
+ : resolve(context.cwd, searchPath)
51
63
  : context.cwd;
52
- const matcher = input.isRegex
53
- ? new RegExp(input.pattern, 'i')
64
+ const isRegex = input.isRegex ?? input.is_regex;
65
+ const matcher = isRegex
66
+ ? new RegExp(pattern, 'i')
54
67
  : null;
55
68
  const files = await walk(root);
56
69
  const results = [];
57
- const maxResults = input.maxResults ?? 50;
70
+ const maxResults = input.maxResults ?? input.max_results ?? 50;
58
71
  for (const filePath of files) {
59
72
  if (!textExtensions.has(extname(filePath))) {
60
73
  continue;
@@ -68,7 +81,7 @@ export const grepTool = {
68
81
  const line = lines[index] ?? '';
69
82
  const matched = matcher
70
83
  ? matcher.test(line)
71
- : line.toLowerCase().includes(input.pattern.toLowerCase());
84
+ : line.toLowerCase().includes(pattern.toLowerCase());
72
85
  if (!matched) {
73
86
  continue;
74
87
  }
@@ -1,9 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDefinition } from './types.js';
3
3
  declare const inputSchema: z.ZodObject<{
4
- path: z.ZodString;
4
+ path: z.ZodOptional<z.ZodString>;
5
+ filePath: z.ZodOptional<z.ZodString>;
6
+ file_path: z.ZodOptional<z.ZodString>;
5
7
  startLine: z.ZodOptional<z.ZodNumber>;
8
+ start_line: z.ZodOptional<z.ZodNumber>;
6
9
  endLine: z.ZodOptional<z.ZodNumber>;
10
+ end_line: z.ZodOptional<z.ZodNumber>;
7
11
  }, z.core.$strip>;
8
12
  export declare const readFileTool: ToolDefinition<z.infer<typeof inputSchema>>;
9
13
  export {};
@@ -2,9 +2,13 @@ import { readFile } from 'node:fs/promises';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  const inputSchema = z.object({
5
- path: z.string().min(1),
5
+ path: z.string().min(1).optional(),
6
+ filePath: z.string().min(1).optional(),
7
+ file_path: z.string().min(1).optional(),
6
8
  startLine: z.number().int().positive().optional(),
9
+ start_line: z.number().int().positive().optional(),
7
10
  endLine: z.number().int().positive().optional(),
11
+ end_line: z.number().int().positive().optional(),
8
12
  });
9
13
  function resolvePath(cwd, filePath) {
10
14
  return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
@@ -25,11 +29,20 @@ export const readFileTool = {
25
29
  },
26
30
  isReadOnly: true,
27
31
  async execute(input, context) {
28
- const absolutePath = resolvePath(context.cwd, input.path);
32
+ const normalizedPath = input.path ?? input.filePath ?? input.file_path;
33
+ if (!normalizedPath) {
34
+ return {
35
+ isError: true,
36
+ content: 'Missing required field: path',
37
+ };
38
+ }
39
+ const startLine = input.startLine ?? input.start_line;
40
+ const endLine = input.endLine ?? input.end_line;
41
+ const absolutePath = resolvePath(context.cwd, normalizedPath);
29
42
  const contents = await readFile(absolutePath, 'utf8');
30
43
  const lines = contents.split(/\r?\n/);
31
- const start = Math.max((input.startLine ?? 1) - 1, 0);
32
- const end = Math.min(input.endLine ?? lines.length, lines.length);
44
+ const start = Math.max((startLine ?? 1) - 1, 0);
45
+ const end = Math.min(endLine ?? lines.length, lines.length);
33
46
  const slice = lines.slice(start, end);
34
47
  const numbered = slice
35
48
  .map((line, index) => `${start + index + 1}: ${line}`)
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDefinition } from './types.js';
3
3
  declare const inputSchema: z.ZodObject<{
4
- command: z.ZodString;
4
+ command: z.ZodOptional<z.ZodString>;
5
+ cmd: z.ZodOptional<z.ZodString>;
5
6
  }, z.core.$strip>;
6
7
  export declare const runShellTool: ToolDefinition<z.infer<typeof inputSchema>>;
7
8
  export {};
@@ -3,7 +3,8 @@ import { promisify } from 'node:util';
3
3
  import { z } from 'zod';
4
4
  const execAsync = promisify(exec);
5
5
  const inputSchema = z.object({
6
- command: z.string().min(1),
6
+ command: z.string().min(1).optional(),
7
+ cmd: z.string().min(1).optional(),
7
8
  });
8
9
  export const runShellTool = {
9
10
  name: 'run_shell',
@@ -29,9 +30,16 @@ export const runShellTool = {
29
30
  };
30
31
  }
31
32
  try {
32
- const { stdout, stderr } = await execAsync(input.command, {
33
+ const command = input.command ?? input.cmd;
34
+ if (!command) {
35
+ return {
36
+ isError: true,
37
+ content: 'Missing required field: command',
38
+ };
39
+ }
40
+ const { stdout, stderr } = await execAsync(command, {
33
41
  cwd: context.cwd,
34
- shell: '/bin/sh',
42
+ shell: process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh',
35
43
  maxBuffer: 1024 * 1024,
36
44
  });
37
45
  const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lcagent-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A minimal coding agent CLI for terminal-based coding workflows.",
5
5
  "type": "module",
6
6
  "publishConfig": {