icopilot 2.2.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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { assertSandbox } from './sandbox.js';
|
|
6
|
+
export async function grepTool(args) {
|
|
7
|
+
const root = path.resolve(config.cwd, args.path || '.');
|
|
8
|
+
assertSandbox(root, config.cwd);
|
|
9
|
+
const maxResults = args.maxResults ?? 200;
|
|
10
|
+
const matcher = createMatcher(args.pattern, Boolean(args.regex), Boolean(args.ignoreCase));
|
|
11
|
+
const files = await fg('**/*', {
|
|
12
|
+
cwd: root,
|
|
13
|
+
onlyFiles: true,
|
|
14
|
+
dot: true,
|
|
15
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
|
|
16
|
+
});
|
|
17
|
+
const matches = [];
|
|
18
|
+
let truncated = false;
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
if (matches.length >= maxResults) {
|
|
21
|
+
truncated = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
const abs = path.join(root, file);
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = fs.readFileSync(abs, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const lines = content.split(/\r?\n/);
|
|
33
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
34
|
+
if (matcher(lines[i] || '')) {
|
|
35
|
+
matches.push({
|
|
36
|
+
file: path.relative(config.cwd, abs),
|
|
37
|
+
line: i + 1,
|
|
38
|
+
text: lines[i] || '',
|
|
39
|
+
});
|
|
40
|
+
if (matches.length >= maxResults) {
|
|
41
|
+
truncated = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify({ matches, truncated });
|
|
48
|
+
}
|
|
49
|
+
function createMatcher(pattern, regex, ignoreCase) {
|
|
50
|
+
if (regex) {
|
|
51
|
+
const re = new RegExp(pattern, ignoreCase ? 'i' : '');
|
|
52
|
+
return (line) => re.test(line);
|
|
53
|
+
}
|
|
54
|
+
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
|
|
55
|
+
return (line) => (ignoreCase ? line.toLowerCase() : line).includes(needle);
|
|
56
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
|
|
5
|
+
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
6
|
+
const MIME_TYPES = {
|
|
7
|
+
'.png': 'image/png',
|
|
8
|
+
'.jpg': 'image/jpeg',
|
|
9
|
+
'.jpeg': 'image/jpeg',
|
|
10
|
+
'.gif': 'image/gif',
|
|
11
|
+
'.webp': 'image/webp',
|
|
12
|
+
'.svg': 'image/svg+xml',
|
|
13
|
+
};
|
|
14
|
+
export function readImage(args) {
|
|
15
|
+
try {
|
|
16
|
+
const abs = path.resolve(config.cwd, args.path);
|
|
17
|
+
const ext = path.extname(abs).toLowerCase();
|
|
18
|
+
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
|
19
|
+
return { error: `unsupported image extension: ${ext || '(none)'}` };
|
|
20
|
+
}
|
|
21
|
+
if (!fs.existsSync(abs)) {
|
|
22
|
+
return { error: `file not found: ${args.path}` };
|
|
23
|
+
}
|
|
24
|
+
const stat = fs.statSync(abs);
|
|
25
|
+
if (!stat.isFile()) {
|
|
26
|
+
return { error: `not a file: ${args.path}` };
|
|
27
|
+
}
|
|
28
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
29
|
+
return { error: `image exceeds 10MB limit: ${args.path}` };
|
|
30
|
+
}
|
|
31
|
+
const buffer = fs.readFileSync(abs);
|
|
32
|
+
const dimensions = getDimensions(buffer, ext);
|
|
33
|
+
return {
|
|
34
|
+
path: abs,
|
|
35
|
+
mimeType: MIME_TYPES[ext],
|
|
36
|
+
width: dimensions?.width,
|
|
37
|
+
height: dimensions?.height,
|
|
38
|
+
sizeBytes: stat.size,
|
|
39
|
+
base64: buffer.toString('base64'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return { error: e?.message || String(e) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const DESCRIBE_IMAGE_SCHEMA = {
|
|
47
|
+
type: 'function',
|
|
48
|
+
function: {
|
|
49
|
+
name: 'describe_image',
|
|
50
|
+
description: 'Read an image file and return its base64 content for visual analysis',
|
|
51
|
+
parameters: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
path: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Path to the image file, relative to the current working directory.',
|
|
57
|
+
},
|
|
58
|
+
maxWidth: {
|
|
59
|
+
type: 'number',
|
|
60
|
+
description: 'Optional hint for downstream visual analysis pipelines.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['path'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function getDimensions(buffer, ext) {
|
|
68
|
+
switch (ext) {
|
|
69
|
+
case '.png':
|
|
70
|
+
return readPngSize(buffer);
|
|
71
|
+
case '.jpg':
|
|
72
|
+
case '.jpeg':
|
|
73
|
+
return readJpegSize(buffer);
|
|
74
|
+
case '.gif':
|
|
75
|
+
return readGifSize(buffer);
|
|
76
|
+
case '.webp':
|
|
77
|
+
return readWebpSize(buffer);
|
|
78
|
+
case '.svg':
|
|
79
|
+
return readSvgSize(buffer);
|
|
80
|
+
default:
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readPngSize(buffer) {
|
|
85
|
+
if (buffer.length < 24)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (buffer.toString('ascii', 1, 4) !== 'PNG')
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
width: buffer.readUInt32BE(16),
|
|
91
|
+
height: buffer.readUInt32BE(20),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function readGifSize(buffer) {
|
|
95
|
+
if (buffer.length < 10)
|
|
96
|
+
return undefined;
|
|
97
|
+
const signature = buffer.toString('ascii', 0, 6);
|
|
98
|
+
if (signature !== 'GIF87a' && signature !== 'GIF89a')
|
|
99
|
+
return undefined;
|
|
100
|
+
return {
|
|
101
|
+
width: buffer.readUInt16LE(6),
|
|
102
|
+
height: buffer.readUInt16LE(8),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function readJpegSize(buffer) {
|
|
106
|
+
if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
|
|
107
|
+
return undefined;
|
|
108
|
+
let offset = 2;
|
|
109
|
+
while (offset + 9 < buffer.length) {
|
|
110
|
+
if (buffer[offset] !== 0xff) {
|
|
111
|
+
offset += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const marker = buffer[offset + 1];
|
|
115
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
116
|
+
break;
|
|
117
|
+
if (offset + 4 > buffer.length)
|
|
118
|
+
break;
|
|
119
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
120
|
+
if (length < 2 || offset + 2 + length > buffer.length)
|
|
121
|
+
break;
|
|
122
|
+
if (isSofMarker(marker)) {
|
|
123
|
+
if (offset + 9 > buffer.length)
|
|
124
|
+
break;
|
|
125
|
+
return {
|
|
126
|
+
height: buffer.readUInt16BE(offset + 5),
|
|
127
|
+
width: buffer.readUInt16BE(offset + 7),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
offset += 2 + length;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function isSofMarker(marker) {
|
|
135
|
+
return ((marker >= 0xc0 && marker <= 0xc3) ||
|
|
136
|
+
(marker >= 0xc5 && marker <= 0xc7) ||
|
|
137
|
+
(marker >= 0xc9 && marker <= 0xcb) ||
|
|
138
|
+
(marker >= 0xcd && marker <= 0xcf));
|
|
139
|
+
}
|
|
140
|
+
function readWebpSize(buffer) {
|
|
141
|
+
if (buffer.length < 30)
|
|
142
|
+
return undefined;
|
|
143
|
+
if (buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WEBP') {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const chunkType = buffer.toString('ascii', 12, 16);
|
|
147
|
+
if (chunkType === 'VP8X' && buffer.length >= 30) {
|
|
148
|
+
return {
|
|
149
|
+
width: 1 + buffer.readUIntLE(24, 3),
|
|
150
|
+
height: 1 + buffer.readUIntLE(27, 3),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (chunkType === 'VP8 ' && buffer.length >= 30) {
|
|
154
|
+
return {
|
|
155
|
+
width: buffer.readUInt16LE(26) & 0x3fff,
|
|
156
|
+
height: buffer.readUInt16LE(28) & 0x3fff,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (chunkType === 'VP8L' && buffer.length >= 25) {
|
|
160
|
+
const bits = buffer.readUInt32LE(21);
|
|
161
|
+
return {
|
|
162
|
+
width: (bits & 0x3fff) + 1,
|
|
163
|
+
height: ((bits >> 14) & 0x3fff) + 1,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function readSvgSize(buffer) {
|
|
169
|
+
const text = buffer.toString('utf8');
|
|
170
|
+
const width = parseSvgLength(text.match(/\bwidth\s*=\s*['"]([^'"]+)['"]/i)?.[1]);
|
|
171
|
+
const height = parseSvgLength(text.match(/\bheight\s*=\s*['"]([^'"]+)['"]/i)?.[1]);
|
|
172
|
+
if (width !== undefined || height !== undefined) {
|
|
173
|
+
return { width, height };
|
|
174
|
+
}
|
|
175
|
+
const viewBox = text.match(/\bviewBox\s*=\s*['"]([^'"]+)['"]/i)?.[1];
|
|
176
|
+
if (!viewBox)
|
|
177
|
+
return undefined;
|
|
178
|
+
const parts = viewBox
|
|
179
|
+
.trim()
|
|
180
|
+
.split(/[\s,]+/)
|
|
181
|
+
.map((part) => Number(part));
|
|
182
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part)))
|
|
183
|
+
return undefined;
|
|
184
|
+
return { width: parts[2], height: parts[3] };
|
|
185
|
+
}
|
|
186
|
+
function parseSvgLength(value) {
|
|
187
|
+
if (!value)
|
|
188
|
+
return undefined;
|
|
189
|
+
const match = value.trim().match(/^([0-9]*\.?[0-9]+)/);
|
|
190
|
+
if (!match)
|
|
191
|
+
return undefined;
|
|
192
|
+
const parsed = Number(match[1]);
|
|
193
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
194
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { assertSandbox } from './sandbox.js';
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 2;
|
|
6
|
+
const MAX_ENTRIES = 200;
|
|
7
|
+
const DEFAULT_IGNORES = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
+
export const listDirectorySchema = {
|
|
9
|
+
type: 'function',
|
|
10
|
+
function: {
|
|
11
|
+
name: 'list_directory',
|
|
12
|
+
description: 'List directory contents as a formatted tree, respecting .gitignore and optional glob filters.',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
path: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Directory path relative to the current working directory.',
|
|
19
|
+
},
|
|
20
|
+
recursive: {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
description: 'Whether to recursively include subdirectories.',
|
|
23
|
+
},
|
|
24
|
+
maxDepth: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
default: DEFAULT_MAX_DEPTH,
|
|
27
|
+
description: 'Maximum nested depth when recursive is enabled.',
|
|
28
|
+
},
|
|
29
|
+
pattern: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Optional glob pattern used to filter returned entries.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['path'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export async function listDirectory(args) {
|
|
39
|
+
try {
|
|
40
|
+
const root = path.resolve(config.cwd, args.path || '.');
|
|
41
|
+
assertSandbox(root, config.cwd);
|
|
42
|
+
if (!fs.existsSync(root)) {
|
|
43
|
+
return `Error: directory not found: ${args.path}`;
|
|
44
|
+
}
|
|
45
|
+
const stat = fs.statSync(root);
|
|
46
|
+
if (!stat.isDirectory()) {
|
|
47
|
+
return `Error: not a directory: ${args.path}`;
|
|
48
|
+
}
|
|
49
|
+
const recursive = Boolean(args.recursive);
|
|
50
|
+
const maxDepth = recursive ? normalizeMaxDepth(args.maxDepth) : 1;
|
|
51
|
+
const ignorePatterns = readGitignorePatterns(config.cwd);
|
|
52
|
+
const matcher = createPatternMatcher(args.pattern);
|
|
53
|
+
const rootLabel = formatRootLabel(args.path, root);
|
|
54
|
+
const tree = collectEntries(root, root, 1, maxDepth, recursive, ignorePatterns, matcher);
|
|
55
|
+
const totalEntries = countEntries(tree);
|
|
56
|
+
const lines = [`${rootLabel}/`];
|
|
57
|
+
const emitted = { count: 0 };
|
|
58
|
+
renderTree(lines, tree, '', emitted);
|
|
59
|
+
if (totalEntries === 0) {
|
|
60
|
+
lines.push('(empty)');
|
|
61
|
+
}
|
|
62
|
+
else if (totalEntries > MAX_ENTRIES) {
|
|
63
|
+
lines.push(`… truncated ${totalEntries - MAX_ENTRIES} additional entr${totalEntries - MAX_ENTRIES === 1 ? 'y' : 'ies'} (showing first ${MAX_ENTRIES} of ${totalEntries})`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return `Error: ${e?.message || String(e)}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function collectEntries(currentDir, rootDir, depth, maxDepth, recursive, ignorePatterns, matcher) {
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const nodes = [];
|
|
80
|
+
const sorted = entries
|
|
81
|
+
.slice()
|
|
82
|
+
.sort((left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) ||
|
|
83
|
+
left.name.localeCompare(right.name));
|
|
84
|
+
for (const entry of sorted) {
|
|
85
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
86
|
+
const relativeToRoot = normalizeSlashes(path.relative(rootDir, absolutePath));
|
|
87
|
+
const relativeToCwd = normalizeSlashes(path.relative(config.cwd, absolutePath));
|
|
88
|
+
const isDirectory = entry.isDirectory();
|
|
89
|
+
if (shouldIgnore(relativeToCwd, isDirectory, ignorePatterns)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
let stats;
|
|
93
|
+
try {
|
|
94
|
+
stats = fs.statSync(absolutePath);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const node = {
|
|
100
|
+
name: entry.name,
|
|
101
|
+
path: relativeToCwd || '.',
|
|
102
|
+
type: isDirectory ? 'dir' : 'file',
|
|
103
|
+
size: isDirectory ? undefined : stats.size,
|
|
104
|
+
modified: stats.mtime.toISOString(),
|
|
105
|
+
};
|
|
106
|
+
if (isDirectory && recursive && depth < maxDepth) {
|
|
107
|
+
node.children = collectEntries(absolutePath, rootDir, depth + 1, maxDepth, recursive, ignorePatterns, matcher);
|
|
108
|
+
}
|
|
109
|
+
const matches = matcher ? matcher(relativeToRoot, entry.name) : true;
|
|
110
|
+
if (isDirectory) {
|
|
111
|
+
if (matches || (node.children?.length ?? 0) > 0 || !matcher) {
|
|
112
|
+
nodes.push(node);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (matches) {
|
|
116
|
+
nodes.push(node);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return nodes;
|
|
120
|
+
}
|
|
121
|
+
function renderTree(lines, nodes, prefix, emitted) {
|
|
122
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
123
|
+
if (emitted.count >= MAX_ENTRIES) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const node = nodes[index];
|
|
127
|
+
const isLast = index === nodes.length - 1;
|
|
128
|
+
const connector = isLast ? '└─ ' : '├─ ';
|
|
129
|
+
lines.push(`${prefix}${connector}${formatNode(node)}`);
|
|
130
|
+
emitted.count += 1;
|
|
131
|
+
if (node.type === 'dir' && node.children?.length) {
|
|
132
|
+
renderTree(lines, node.children, `${prefix}${isLast ? ' ' : '│ '}`, emitted);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function formatNode(node) {
|
|
137
|
+
const details = [
|
|
138
|
+
node.type,
|
|
139
|
+
...(typeof node.size === 'number' ? [`${node.size} B`] : []),
|
|
140
|
+
...(node.modified ? [`modified ${node.modified}`] : []),
|
|
141
|
+
];
|
|
142
|
+
return `${node.name}${node.type === 'dir' ? '/' : ''} [${details.join(', ')}]`;
|
|
143
|
+
}
|
|
144
|
+
function formatRootLabel(inputPath, absolutePath) {
|
|
145
|
+
if (!inputPath || inputPath === '.')
|
|
146
|
+
return '.';
|
|
147
|
+
const relative = path.relative(config.cwd, absolutePath);
|
|
148
|
+
return relative ? normalizeSlashes(relative) : '.';
|
|
149
|
+
}
|
|
150
|
+
function countEntries(nodes) {
|
|
151
|
+
return nodes.reduce((total, node) => total + 1 + countEntries(node.children ?? []), 0);
|
|
152
|
+
}
|
|
153
|
+
function normalizeMaxDepth(maxDepth) {
|
|
154
|
+
if (typeof maxDepth !== 'number' || !Number.isFinite(maxDepth)) {
|
|
155
|
+
return DEFAULT_MAX_DEPTH;
|
|
156
|
+
}
|
|
157
|
+
return Math.max(1, Math.floor(maxDepth));
|
|
158
|
+
}
|
|
159
|
+
function readGitignorePatterns(cwd) {
|
|
160
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
161
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return fs
|
|
166
|
+
.readFileSync(gitignorePath, 'utf8')
|
|
167
|
+
.split(/\r?\n/)
|
|
168
|
+
.map((line) => line.trim())
|
|
169
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function shouldIgnore(relativePath, isDirectory, patterns) {
|
|
176
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const normalized = normalizeSlashes(relativePath);
|
|
180
|
+
const segments = normalized.split('/');
|
|
181
|
+
if (segments.some((segment) => DEFAULT_IGNORES.has(segment))) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return patterns.some((pattern) => matchesGitignorePattern(normalized, segments, isDirectory, pattern));
|
|
185
|
+
}
|
|
186
|
+
function matchesGitignorePattern(normalizedPath, segments, isDirectory, pattern) {
|
|
187
|
+
const normalizedPattern = normalizeSlashes(pattern).replace(/^\/+/, '');
|
|
188
|
+
const directoryOnly = normalizedPattern.endsWith('/');
|
|
189
|
+
const barePattern = normalizedPattern.replace(/\/+$/, '');
|
|
190
|
+
if (directoryOnly && !isDirectory && barePattern === normalizedPath) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (!barePattern) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (!hasGlob(barePattern) && !barePattern.includes('/')) {
|
|
197
|
+
return segments.includes(barePattern);
|
|
198
|
+
}
|
|
199
|
+
if (!hasGlob(barePattern)) {
|
|
200
|
+
return normalizedPath === barePattern || normalizedPath.startsWith(`${barePattern}/`);
|
|
201
|
+
}
|
|
202
|
+
const regex = globToRegExp(barePattern);
|
|
203
|
+
return regex.test(normalizedPath);
|
|
204
|
+
}
|
|
205
|
+
function createPatternMatcher(pattern) {
|
|
206
|
+
if (!pattern?.trim()) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const normalizedPattern = normalizeSlashes(pattern.trim());
|
|
210
|
+
const hasSlash = normalizedPattern.includes('/');
|
|
211
|
+
const regex = globToRegExp(normalizedPattern);
|
|
212
|
+
return (relativePath, name) => regex.test(hasSlash ? relativePath : name);
|
|
213
|
+
}
|
|
214
|
+
function hasGlob(value) {
|
|
215
|
+
return /[*?[\]]/.test(value);
|
|
216
|
+
}
|
|
217
|
+
function globToRegExp(pattern) {
|
|
218
|
+
const source = pattern
|
|
219
|
+
.replace(/[|\\{}()[\]^$+.]/g, '\\$&')
|
|
220
|
+
.replace(/\*\*/g, '::DOUBLE_STAR::')
|
|
221
|
+
.replace(/\*/g, '[^/]*')
|
|
222
|
+
.replace(/\?/g, '[^/]')
|
|
223
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
224
|
+
return new RegExp(`^${source}$`);
|
|
225
|
+
}
|
|
226
|
+
function normalizeSlashes(value) {
|
|
227
|
+
return value.split(path.sep).join('/');
|
|
228
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class ToolMemory {
|
|
2
|
+
allowShell = new Set();
|
|
3
|
+
allowWritePath = new Set();
|
|
4
|
+
rememberShell(cmd) {
|
|
5
|
+
this.allowShell.add(cmd);
|
|
6
|
+
}
|
|
7
|
+
isShellRemembered(cmd) {
|
|
8
|
+
return this.allowShell.has(cmd);
|
|
9
|
+
}
|
|
10
|
+
rememberWrite(absPath) {
|
|
11
|
+
this.allowWritePath.add(absPath);
|
|
12
|
+
}
|
|
13
|
+
isWriteRemembered(absPath) {
|
|
14
|
+
return this.allowWritePath.has(absPath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const toolMemory = new ToolMemory();
|