lcagent-cli 0.1.5 → 0.1.7

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.
@@ -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
+ }
@@ -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',
@@ -22,16 +23,17 @@ export const runShellTool = {
22
23
  },
23
24
  isReadOnly: false,
24
25
  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
26
  try {
32
- const { stdout, stderr } = await execAsync(input.command, {
27
+ const command = input.command ?? input.cmd;
28
+ if (!command) {
29
+ return {
30
+ isError: true,
31
+ content: 'Missing required field: command',
32
+ };
33
+ }
34
+ const { stdout, stderr } = await execAsync(command, {
33
35
  cwd: context.cwd,
34
- shell: '/bin/sh',
36
+ shell: process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : '/bin/sh',
35
37
  maxBuffer: 1024 * 1024,
36
38
  });
37
39
  const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
@@ -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.5",
3
+ "version": "0.1.7",
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
  },