lcagent-cli 0.1.0
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 +166 -0
- package/dist/app/bootstrap.d.ts +8 -0
- package/dist/app/bootstrap.js +30 -0
- package/dist/app/session.d.ts +5 -0
- package/dist/app/session.js +5 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +268 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +11 -0
- package/dist/config/store.d.ts +5 -0
- package/dist/config/store.js +37 -0
- package/dist/core/engine.d.ts +15 -0
- package/dist/core/engine.js +29 -0
- package/dist/core/loop.d.ts +12 -0
- package/dist/core/loop.js +54 -0
- package/dist/core/message.d.ts +38 -0
- package/dist/core/message.js +6 -0
- package/dist/core/model.d.ts +28 -0
- package/dist/core/model.js +158 -0
- package/dist/core/systemPrompt.d.ts +1 -0
- package/dist/core/systemPrompt.js +11 -0
- package/dist/tools/editFile.d.ts +9 -0
- package/dist/tools/editFile.js +48 -0
- package/dist/tools/execute.d.ts +3 -0
- package/dist/tools/execute.js +17 -0
- package/dist/tools/grep.d.ts +10 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/readFile.d.ts +9 -0
- package/dist/tools/readFile.js +41 -0
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +7 -0
- package/dist/tools/runShell.d.ts +7 -0
- package/dist/tools/runShell.js +50 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.js +17 -0
- package/package.json +43 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type TextBlock = {
|
|
2
|
+
type: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
export type ToolUseBlock = {
|
|
6
|
+
type: 'tool_use';
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
input: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type ToolResultBlock = {
|
|
12
|
+
type: 'tool_result';
|
|
13
|
+
tool_use_id: string;
|
|
14
|
+
content: string;
|
|
15
|
+
is_error?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type AgentContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
|
|
18
|
+
export type AgentMessage = {
|
|
19
|
+
role: 'user' | 'assistant';
|
|
20
|
+
content: AgentContentBlock[];
|
|
21
|
+
};
|
|
22
|
+
export type AgentEvent = {
|
|
23
|
+
type: 'status';
|
|
24
|
+
message: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'assistant_text';
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'tool_call';
|
|
30
|
+
toolName: string;
|
|
31
|
+
input: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'tool_result';
|
|
34
|
+
toolName: string;
|
|
35
|
+
result: string;
|
|
36
|
+
isError?: boolean;
|
|
37
|
+
};
|
|
38
|
+
export declare function createUserTextMessage(text: string): AgentMessage;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AgentMessage, ToolUseBlock } from './message.js';
|
|
2
|
+
import { type ToolDefinition } from '../tools/types.js';
|
|
3
|
+
export type ModelClientConfig = {
|
|
4
|
+
provider: 'anthropic' | 'openai-compatible';
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
model: string;
|
|
8
|
+
maxTokens: number;
|
|
9
|
+
};
|
|
10
|
+
export type ModelResponse = {
|
|
11
|
+
id: string;
|
|
12
|
+
stopReason: string | null;
|
|
13
|
+
content: Array<{
|
|
14
|
+
type: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
} | ToolUseBlock>;
|
|
17
|
+
};
|
|
18
|
+
export declare class ModelClient {
|
|
19
|
+
private readonly config;
|
|
20
|
+
constructor(config: ModelClientConfig);
|
|
21
|
+
createMessage(params: {
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
messages: AgentMessage[];
|
|
24
|
+
tools: ToolDefinition[];
|
|
25
|
+
}): Promise<ModelResponse>;
|
|
26
|
+
private createAnthropicMessage;
|
|
27
|
+
private createOpenAICompatibleMessage;
|
|
28
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { toAnthropicTool, toOpenAICompatibleTool, } from '../tools/types.js';
|
|
2
|
+
function textFromBlocks(blocks) {
|
|
3
|
+
return blocks
|
|
4
|
+
.filter((block) => block.type === 'text')
|
|
5
|
+
.map(block => block.text)
|
|
6
|
+
.join('\n');
|
|
7
|
+
}
|
|
8
|
+
function toolUsesFromBlocks(blocks) {
|
|
9
|
+
return blocks.filter((block) => block.type === 'tool_use');
|
|
10
|
+
}
|
|
11
|
+
function toOpenAICompatibleMessages(systemPrompt, messages) {
|
|
12
|
+
const converted = [
|
|
13
|
+
{
|
|
14
|
+
role: 'system',
|
|
15
|
+
content: systemPrompt,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
if (message.role === 'assistant') {
|
|
20
|
+
const text = textFromBlocks(message.content);
|
|
21
|
+
const toolUses = toolUsesFromBlocks(message.content);
|
|
22
|
+
converted.push({
|
|
23
|
+
role: 'assistant',
|
|
24
|
+
content: text || undefined,
|
|
25
|
+
...(toolUses.length > 0
|
|
26
|
+
? {
|
|
27
|
+
tool_calls: toolUses.map(toolUse => ({
|
|
28
|
+
id: toolUse.id,
|
|
29
|
+
type: 'function',
|
|
30
|
+
function: {
|
|
31
|
+
name: toolUse.name,
|
|
32
|
+
arguments: JSON.stringify(toolUse.input),
|
|
33
|
+
},
|
|
34
|
+
})),
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const toolResults = message.content.filter((block) => block.type === 'tool_result');
|
|
41
|
+
if (toolResults.length > 0) {
|
|
42
|
+
for (const toolResult of toolResults) {
|
|
43
|
+
converted.push({
|
|
44
|
+
role: 'tool',
|
|
45
|
+
tool_call_id: toolResult.tool_use_id,
|
|
46
|
+
content: toolResult.content,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
converted.push({
|
|
52
|
+
role: 'user',
|
|
53
|
+
content: textFromBlocks(message.content),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return converted;
|
|
57
|
+
}
|
|
58
|
+
function parseToolArguments(raw) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return { raw };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class ModelClient {
|
|
67
|
+
config;
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.config = config;
|
|
70
|
+
}
|
|
71
|
+
async createMessage(params) {
|
|
72
|
+
if (this.config.provider === 'openai-compatible') {
|
|
73
|
+
return this.createOpenAICompatibleMessage(params);
|
|
74
|
+
}
|
|
75
|
+
return this.createAnthropicMessage(params);
|
|
76
|
+
}
|
|
77
|
+
async createAnthropicMessage(params) {
|
|
78
|
+
const url = `${this.config.baseUrl.replace(/\/+$/, '')}/v1/messages`;
|
|
79
|
+
const response = await fetch(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'content-type': 'application/json',
|
|
83
|
+
...(this.config.apiKey ? { 'x-api-key': this.config.apiKey } : {}),
|
|
84
|
+
'anthropic-version': '2023-06-01',
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: this.config.model,
|
|
88
|
+
max_tokens: this.config.maxTokens,
|
|
89
|
+
system: params.systemPrompt,
|
|
90
|
+
messages: params.messages,
|
|
91
|
+
tools: params.tools.map(toAnthropicTool),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
const data = (await response.json());
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const detail = data.error?.message ?? `HTTP ${response.status}`;
|
|
97
|
+
throw new Error(`Model API request failed: ${detail}`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: data.id,
|
|
101
|
+
stopReason: data.stop_reason,
|
|
102
|
+
content: data.content.map(block => {
|
|
103
|
+
if (block.type === 'tool_use') {
|
|
104
|
+
return {
|
|
105
|
+
type: 'tool_use',
|
|
106
|
+
id: block.id,
|
|
107
|
+
name: block.name,
|
|
108
|
+
input: block.input,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return block;
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async createOpenAICompatibleMessage(params) {
|
|
116
|
+
const url = `${this.config.baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'content-type': 'application/json',
|
|
121
|
+
...(this.config.apiKey
|
|
122
|
+
? { Authorization: `Bearer ${this.config.apiKey}` }
|
|
123
|
+
: {}),
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model: this.config.model,
|
|
127
|
+
messages: toOpenAICompatibleMessages(params.systemPrompt, params.messages),
|
|
128
|
+
tools: params.tools.map(toOpenAICompatibleTool),
|
|
129
|
+
tool_choice: 'auto',
|
|
130
|
+
max_tokens: this.config.maxTokens,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
const data = (await response.json());
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const detail = data.error?.message ?? `HTTP ${response.status}`;
|
|
136
|
+
throw new Error(`Model API request failed: ${detail}`);
|
|
137
|
+
}
|
|
138
|
+
const choice = data.choices[0];
|
|
139
|
+
if (!choice) {
|
|
140
|
+
throw new Error('Model API request failed: empty choices array');
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id: data.id,
|
|
144
|
+
stopReason: choice.finish_reason,
|
|
145
|
+
content: [
|
|
146
|
+
...(choice.message.content
|
|
147
|
+
? [{ type: 'text', text: choice.message.content }]
|
|
148
|
+
: []),
|
|
149
|
+
...((choice.message.tool_calls ?? []).map(toolCall => ({
|
|
150
|
+
type: 'tool_use',
|
|
151
|
+
id: toolCall.id,
|
|
152
|
+
name: toolCall.function.name,
|
|
153
|
+
input: parseToolArguments(toolCall.function.arguments),
|
|
154
|
+
}))),
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildSystemPrompt(cwd: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function buildSystemPrompt(cwd) {
|
|
2
|
+
return [
|
|
3
|
+
'You are a coding agent running inside a local CLI.',
|
|
4
|
+
'Prefer using tools when they help answer precisely.',
|
|
5
|
+
'Do not invent file contents or command output when tools can verify them.',
|
|
6
|
+
'Before editing code, read the relevant files.',
|
|
7
|
+
'Explain briefly and act directly.',
|
|
8
|
+
`Current working directory: ${cwd}`,
|
|
9
|
+
'Available tools may read files, edit files, search files, and run shell commands.',
|
|
10
|
+
].join('\n');
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from './types.js';
|
|
3
|
+
declare const inputSchema: z.ZodObject<{
|
|
4
|
+
path: z.ZodString;
|
|
5
|
+
oldText: z.ZodString;
|
|
6
|
+
newText: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare const editFileTool: ToolDefinition<z.infer<typeof inputSchema>>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const inputSchema = z.object({
|
|
5
|
+
path: z.string().min(1),
|
|
6
|
+
oldText: z.string(),
|
|
7
|
+
newText: z.string(),
|
|
8
|
+
});
|
|
9
|
+
function resolvePath(cwd, filePath) {
|
|
10
|
+
return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
|
|
11
|
+
}
|
|
12
|
+
export const editFileTool = {
|
|
13
|
+
name: 'edit_file',
|
|
14
|
+
description: 'Replace the first occurrence of oldText with newText in a text file.',
|
|
15
|
+
inputSchema,
|
|
16
|
+
inputSchemaJson: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string' },
|
|
20
|
+
oldText: { type: 'string' },
|
|
21
|
+
newText: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
required: ['path', 'oldText', 'newText'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
isReadOnly: false,
|
|
27
|
+
async execute(input, context) {
|
|
28
|
+
if (context.approvalMode === 'manual') {
|
|
29
|
+
return {
|
|
30
|
+
isError: true,
|
|
31
|
+
content: 'edit_file is blocked in manual approval mode. Switch config approvalMode to auto to enable file edits.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const absolutePath = resolvePath(context.cwd, input.path);
|
|
35
|
+
const current = await readFile(absolutePath, 'utf8');
|
|
36
|
+
if (!current.includes(input.oldText)) {
|
|
37
|
+
return {
|
|
38
|
+
isError: true,
|
|
39
|
+
content: 'oldText was not found in the target file.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const next = current.replace(input.oldText, input.newText);
|
|
43
|
+
await writeFile(absolutePath, next, 'utf8');
|
|
44
|
+
return {
|
|
45
|
+
content: `Updated ${input.path}`,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ToolUseBlock } from '../core/message.js';
|
|
2
|
+
import type { ToolContext, ToolDefinition, ToolExecutionResult } from './types.js';
|
|
3
|
+
export declare function executeToolCall(toolUse: ToolUseBlock, tools: ToolDefinition[], context: ToolContext): Promise<ToolExecutionResult>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function executeToolCall(toolUse, tools, context) {
|
|
2
|
+
const tool = tools.find(item => item.name === toolUse.name);
|
|
3
|
+
if (!tool) {
|
|
4
|
+
return {
|
|
5
|
+
isError: true,
|
|
6
|
+
content: `Unknown tool: ${toolUse.name}`,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const parsed = tool.inputSchema.safeParse(toolUse.input);
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
return {
|
|
12
|
+
isError: true,
|
|
13
|
+
content: parsed.error.message,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return tool.execute(parsed.data, context);
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from './types.js';
|
|
3
|
+
declare const inputSchema: z.ZodObject<{
|
|
4
|
+
pattern: z.ZodString;
|
|
5
|
+
path: z.ZodOptional<z.ZodString>;
|
|
6
|
+
maxResults: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
isRegex: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const grepTool: ToolDefinition<z.infer<typeof inputSchema>>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const inputSchema = z.object({
|
|
5
|
+
pattern: z.string().min(1),
|
|
6
|
+
path: z.string().optional(),
|
|
7
|
+
maxResults: z.number().int().positive().max(200).optional(),
|
|
8
|
+
isRegex: z.boolean().optional(),
|
|
9
|
+
});
|
|
10
|
+
const textExtensions = new Set([
|
|
11
|
+
'.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt', '.yml', '.yaml', '.css', '.html', '.mjs', '.cjs', '.sh', '.py', '.java', '.go', '.rs'
|
|
12
|
+
]);
|
|
13
|
+
async function walk(dir) {
|
|
14
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const fullPath = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
files.push(...(await walk(fullPath)));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
files.push(fullPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
export const grepTool = {
|
|
31
|
+
name: 'grep',
|
|
32
|
+
description: 'Search text files under the workspace for a pattern.',
|
|
33
|
+
inputSchema,
|
|
34
|
+
inputSchemaJson: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
pattern: { type: 'string' },
|
|
38
|
+
path: { type: 'string' },
|
|
39
|
+
maxResults: { type: 'integer' },
|
|
40
|
+
isRegex: { type: 'boolean' },
|
|
41
|
+
},
|
|
42
|
+
required: ['pattern'],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
isReadOnly: true,
|
|
46
|
+
async execute(input, context) {
|
|
47
|
+
const root = input.path
|
|
48
|
+
? isAbsolute(input.path)
|
|
49
|
+
? input.path
|
|
50
|
+
: resolve(context.cwd, input.path)
|
|
51
|
+
: context.cwd;
|
|
52
|
+
const matcher = input.isRegex
|
|
53
|
+
? new RegExp(input.pattern, 'i')
|
|
54
|
+
: null;
|
|
55
|
+
const files = await walk(root);
|
|
56
|
+
const results = [];
|
|
57
|
+
const maxResults = input.maxResults ?? 50;
|
|
58
|
+
for (const filePath of files) {
|
|
59
|
+
if (!textExtensions.has(extname(filePath))) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const contents = await readFile(filePath, 'utf8').catch(() => '');
|
|
63
|
+
if (!contents) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const lines = contents.split(/\r?\n/);
|
|
67
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
68
|
+
const line = lines[index] ?? '';
|
|
69
|
+
const matched = matcher
|
|
70
|
+
? matcher.test(line)
|
|
71
|
+
: line.toLowerCase().includes(input.pattern.toLowerCase());
|
|
72
|
+
if (!matched) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
results.push(`${filePath}:${index + 1}: ${line}`);
|
|
76
|
+
if (results.length >= maxResults) {
|
|
77
|
+
return { content: results.join('\n') };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
content: results.length > 0 ? results.join('\n') : 'No matches found.',
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from './types.js';
|
|
3
|
+
declare const inputSchema: z.ZodObject<{
|
|
4
|
+
path: z.ZodString;
|
|
5
|
+
startLine: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
endLine: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare const readFileTool: ToolDefinition<z.infer<typeof inputSchema>>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const inputSchema = z.object({
|
|
5
|
+
path: z.string().min(1),
|
|
6
|
+
startLine: z.number().int().positive().optional(),
|
|
7
|
+
endLine: z.number().int().positive().optional(),
|
|
8
|
+
});
|
|
9
|
+
function resolvePath(cwd, filePath) {
|
|
10
|
+
return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
|
|
11
|
+
}
|
|
12
|
+
export const readFileTool = {
|
|
13
|
+
name: 'read_file',
|
|
14
|
+
description: 'Read a text file from the workspace, optionally by line range.',
|
|
15
|
+
inputSchema,
|
|
16
|
+
inputSchemaJson: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: 'string', description: 'Relative or absolute file path.' },
|
|
20
|
+
startLine: { type: 'integer', description: '1-based inclusive start line.' },
|
|
21
|
+
endLine: { type: 'integer', description: '1-based inclusive end line.' },
|
|
22
|
+
},
|
|
23
|
+
required: ['path'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
isReadOnly: true,
|
|
27
|
+
async execute(input, context) {
|
|
28
|
+
const absolutePath = resolvePath(context.cwd, input.path);
|
|
29
|
+
const contents = await readFile(absolutePath, 'utf8');
|
|
30
|
+
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);
|
|
33
|
+
const slice = lines.slice(start, end);
|
|
34
|
+
const numbered = slice
|
|
35
|
+
.map((line, index) => `${start + index + 1}: ${line}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return {
|
|
38
|
+
content: numbered || '(empty file)',
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { editFileTool } from './editFile.js';
|
|
2
|
+
import { grepTool } from './grep.js';
|
|
3
|
+
import { readFileTool } from './readFile.js';
|
|
4
|
+
import { runShellTool } from './runShell.js';
|
|
5
|
+
export function getDefaultTools() {
|
|
6
|
+
return [readFileTool, editFileTool, grepTool, runShellTool];
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
const inputSchema = z.object({
|
|
6
|
+
command: z.string().min(1),
|
|
7
|
+
});
|
|
8
|
+
export const runShellTool = {
|
|
9
|
+
name: 'run_shell',
|
|
10
|
+
description: 'Run a shell command in the current working directory.',
|
|
11
|
+
inputSchema,
|
|
12
|
+
inputSchemaJson: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
command: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'Shell command to run.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ['command'],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
},
|
|
23
|
+
isReadOnly: false,
|
|
24
|
+
async execute(input, context) {
|
|
25
|
+
if (context.approvalMode === 'manual') {
|
|
26
|
+
return {
|
|
27
|
+
isError: true,
|
|
28
|
+
content: 'run_shell is blocked in manual approval mode. Switch config approvalMode to auto to enable shell execution.',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const { stdout, stderr } = await execAsync(input.command, {
|
|
33
|
+
cwd: context.cwd,
|
|
34
|
+
shell: '/bin/sh',
|
|
35
|
+
maxBuffer: 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
|
|
38
|
+
return {
|
|
39
|
+
content: output || '(command produced no output)',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
return {
|
|
45
|
+
isError: true,
|
|
46
|
+
content: message,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
export type ToolExecutionResult = {
|
|
3
|
+
content: string;
|
|
4
|
+
isError?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type ToolContext = {
|
|
7
|
+
cwd: string;
|
|
8
|
+
approvalMode: 'auto' | 'manual';
|
|
9
|
+
};
|
|
10
|
+
export type AnthropicToolDefinition = {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
input_schema: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
export type OpenAICompatibleToolDefinition = {
|
|
16
|
+
type: 'function';
|
|
17
|
+
function: {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type ToolDefinition<TInput = unknown> = {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
inputSchema: z.ZodType<TInput>;
|
|
27
|
+
inputSchemaJson: Record<string, unknown>;
|
|
28
|
+
isReadOnly: boolean;
|
|
29
|
+
execute(input: TInput, context: ToolContext): Promise<ToolExecutionResult>;
|
|
30
|
+
};
|
|
31
|
+
export declare function toAnthropicTool(tool: ToolDefinition): AnthropicToolDefinition;
|
|
32
|
+
export declare function toOpenAICompatibleTool(tool: ToolDefinition): OpenAICompatibleToolDefinition;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function toAnthropicTool(tool) {
|
|
2
|
+
return {
|
|
3
|
+
name: tool.name,
|
|
4
|
+
description: tool.description,
|
|
5
|
+
input_schema: tool.inputSchemaJson,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function toOpenAICompatibleTool(tool) {
|
|
9
|
+
return {
|
|
10
|
+
type: 'function',
|
|
11
|
+
function: {
|
|
12
|
+
name: tool.name,
|
|
13
|
+
description: tool.description,
|
|
14
|
+
parameters: tool.inputSchemaJson,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lcagent-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A minimal coding agent CLI for terminal-based coding workflows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"registry": "https://registry.npmjs.org/"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"preferGlobal": true,
|
|
14
|
+
"bin": {
|
|
15
|
+
"lcagent": "dist/bin/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "tsx src/bin/cli.ts",
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"start": "node dist/bin/cli.js",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"agent",
|
|
29
|
+
"cli",
|
|
30
|
+
"coding-agent",
|
|
31
|
+
"terminal"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^14.0.0",
|
|
35
|
+
"zod": "^4.1.5"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^24.6.0",
|
|
39
|
+
"tsx": "^4.20.5",
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"license": "UNLICENSED"
|
|
43
|
+
}
|