codeep 2.18.1 → 2.20.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 +53 -16
- package/dist/acp/commands.js +11 -55
- package/dist/acp/protocol.d.ts +34 -0
- package/dist/acp/server.d.ts +6 -1
- package/dist/acp/server.js +97 -2
- package/dist/api/index.js +9 -0
- package/dist/commands/core/index.d.ts +19 -0
- package/dist/commands/core/index.js +28 -0
- package/dist/commands/core/keysync.d.ts +2 -0
- package/dist/commands/core/keysync.js +34 -0
- package/dist/commands/core/telemetry.d.ts +2 -0
- package/dist/commands/core/telemetry.js +34 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +7 -4
- package/dist/renderer/App.d.ts +9 -48
- package/dist/renderer/App.js +113 -338
- package/dist/renderer/Screen.d.ts +13 -0
- package/dist/renderer/Screen.js +22 -0
- package/dist/renderer/commands/registry.js +3 -3
- package/dist/renderer/commands.js +19 -51
- package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
- package/dist/renderer/components/CommandAutocomplete.js +103 -0
- package/dist/renderer/components/HunkPicker.d.ts +48 -0
- package/dist/renderer/components/HunkPicker.js +140 -0
- package/dist/renderer/components/MentionPicker.d.ts +60 -0
- package/dist/renderer/components/MentionPicker.js +111 -0
- package/dist/renderer/components/PasteDialog.d.ts +43 -0
- package/dist/renderer/components/PasteDialog.js +70 -0
- package/dist/renderer/layout.js +1 -0
- package/dist/renderer/main.js +15 -39
- package/dist/utils/agent.js +121 -26
- package/dist/utils/agentChat.d.ts +11 -4
- package/dist/utils/agentChat.js +53 -25
- package/dist/utils/codeepCloud.d.ts +3 -0
- package/dist/utils/codeepCloud.js +62 -7
- package/dist/utils/personalities.d.ts +63 -5
- package/dist/utils/personalities.js +583 -31
- package/dist/utils/shell.d.ts +11 -1
- package/dist/utils/shell.js +169 -82
- package/dist/utils/ssrfGuard.d.ts +18 -0
- package/dist/utils/ssrfGuard.js +83 -0
- package/dist/utils/taskPlanner.d.ts +7 -1
- package/dist/utils/taskPlanner.js +16 -7
- package/dist/utils/tokenTracker.js +5 -3
- package/dist/utils/toolExecution.d.ts +1 -0
- package/dist/utils/toolExecution.js +48 -88
- package/dist/utils/tools.d.ts +3 -3
- package/dist/utils/tools.js +18 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* listDirectory() and htmlToText() are private helpers.
|
|
7
7
|
* createActionLog() converts a ToolCall+ToolResult into a history ActionLog.
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
|
|
10
|
-
import { join, dirname, relative, resolve, isAbsolute } from 'path';
|
|
9
|
+
import { existsSync, readdirSync, statSync, lstatSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, rmSync, realpathSync } from 'fs';
|
|
10
|
+
import { join, dirname, relative, resolve, isAbsolute, sep } from 'path';
|
|
11
11
|
import { executeCommandAsync } from './shell.js';
|
|
12
12
|
import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history.js';
|
|
13
13
|
import { loadIgnoreRules, isIgnored } from './gitignore.js';
|
|
@@ -15,83 +15,13 @@ import { normalizeToolName } from './toolParsing.js';
|
|
|
15
15
|
import { getZaiMcpConfig, getZaiVisionConfig, getMinimaxMcpConfig, callZaiMcp, callZaiVisionApi, callMinimaxApi } from './mcpIntegration.js';
|
|
16
16
|
import { logger } from './logger.js';
|
|
17
17
|
import { runHook } from './hooks.js';
|
|
18
|
+
import { checkCommandRateLimit } from './ratelimit.js';
|
|
18
19
|
import { isMcpToolName, callSessionTool, isVirtualMcpToolName, callSessionVirtualTool } from './mcpRegistry.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* (169.254.169.254). NOTE: this does NOT apply to user-configured provider
|
|
25
|
-
* base URLs (Ollama localhost, custom vLLM/Tailscale endpoints) — those are
|
|
26
|
-
* trusted config and never routed through fetch_url.
|
|
27
|
-
*/
|
|
28
|
-
function isBlockedIp(ip) {
|
|
29
|
-
const s = ip.trim().toLowerCase();
|
|
30
|
-
if (s.includes(':')) {
|
|
31
|
-
// IPv6
|
|
32
|
-
if (s === '::1' || s === '::')
|
|
33
|
-
return true; // loopback / unspecified
|
|
34
|
-
if (s.startsWith('fe80') || s.startsWith('fc') || s.startsWith('fd'))
|
|
35
|
-
return true; // link-local / ULA
|
|
36
|
-
const mapped = s.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/); // IPv4-mapped
|
|
37
|
-
if (mapped)
|
|
38
|
-
return isBlockedIp(mapped[1]);
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const parts = s.split('.').map(Number);
|
|
42
|
-
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255))
|
|
43
|
-
return false;
|
|
44
|
-
const [a, b] = parts;
|
|
45
|
-
if (a === 127)
|
|
46
|
-
return true; // loopback
|
|
47
|
-
if (a === 10)
|
|
48
|
-
return true; // RFC1918
|
|
49
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
50
|
-
return true; // RFC1918
|
|
51
|
-
if (a === 192 && b === 168)
|
|
52
|
-
return true; // RFC1918
|
|
53
|
-
if (a === 169 && b === 254)
|
|
54
|
-
return true; // link-local incl. metadata 169.254.169.254
|
|
55
|
-
if (a === 0)
|
|
56
|
-
return true; // 0.0.0.0/8
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
/** Returns an error string if the URL must not be fetched, else null. */
|
|
60
|
-
async function assertFetchUrlAllowed(rawUrl) {
|
|
61
|
-
let u;
|
|
62
|
-
try {
|
|
63
|
-
u = new URL(rawUrl);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return 'Invalid URL format';
|
|
67
|
-
}
|
|
68
|
-
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
69
|
-
return `Blocked: only http/https URLs can be fetched (got "${u.protocol}")`;
|
|
70
|
-
}
|
|
71
|
-
const host = u.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
|
|
72
|
-
if (host === 'localhost' || host.endsWith('.localhost')) {
|
|
73
|
-
return 'Blocked: localhost is not fetchable by the agent';
|
|
74
|
-
}
|
|
75
|
-
if (/^[0-9.]+$/.test(host) || host.includes(':')) {
|
|
76
|
-
// Literal IP — check directly.
|
|
77
|
-
if (isBlockedIp(host))
|
|
78
|
-
return `Blocked: ${host} is a private/loopback/link-local address`;
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
// Resolve and check every address (catches internal hostnames + single-record rebinding).
|
|
82
|
-
try {
|
|
83
|
-
const addrs = await dnsLookup(host, { all: true });
|
|
84
|
-
for (const a of addrs) {
|
|
85
|
-
if (isBlockedIp(a.address)) {
|
|
86
|
-
return `Blocked: ${host} resolves to a private/internal address (${a.address})`;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// DNS failure — let curl attempt and fail naturally; not an SSRF risk.
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
20
|
+
// SSRF guard (isBlockedIp / assertFetchUrlAllowed) moved to ./ssrfGuard —
|
|
21
|
+
// shared with shell.ts for curl/wget URL checks. Re-exported here so the
|
|
22
|
+
// existing tests that import it from toolExecution keep working.
|
|
23
|
+
export { isBlockedIp, assertFetchUrlAllowed } from './ssrfGuard.js';
|
|
24
|
+
import { assertFetchUrlAllowed } from './ssrfGuard.js';
|
|
95
25
|
const debug = (...args) => {
|
|
96
26
|
if (process.env.CODEEP_DEBUG === '1') {
|
|
97
27
|
logger.debug(args.map(String).join(' '));
|
|
@@ -115,20 +45,37 @@ export function validatePath(path, projectRoot) {
|
|
|
115
45
|
if (relativePath.startsWith('..')) {
|
|
116
46
|
return { valid: false, absolutePath, error: `Path '${path}' is outside project directory` };
|
|
117
47
|
}
|
|
118
|
-
// Resolve
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
48
|
+
// Resolve the deepest existing ancestor, not only the complete target. A
|
|
49
|
+
// write to `project/link-to-outside/new.txt` has a non-existent leaf, but
|
|
50
|
+
// still follows the existing symlinked parent. lstat is intentional: unlike
|
|
51
|
+
// existsSync it also sees a broken symlink, which must fail closed rather
|
|
52
|
+
// than be followed by writeFileSync.
|
|
53
|
+
try {
|
|
54
|
+
const realRoot = realpathSync(projectRoot);
|
|
55
|
+
let existingAncestor = absolutePath;
|
|
56
|
+
while (true) {
|
|
57
|
+
try {
|
|
58
|
+
lstatSync(existingAncestor);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
const parent = dirname(existingAncestor);
|
|
63
|
+
if (parent === existingAncestor) {
|
|
64
|
+
return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
|
|
65
|
+
}
|
|
66
|
+
existingAncestor = parent;
|
|
125
67
|
}
|
|
126
68
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
69
|
+
const realAncestor = realpathSync(existingAncestor);
|
|
70
|
+
const ancestorRelative = relative(realRoot, realAncestor);
|
|
71
|
+
if (ancestorRelative === '..' || ancestorRelative.startsWith(`..${sep}`) || isAbsolute(ancestorRelative)) {
|
|
72
|
+
return { valid: false, absolutePath, error: `Path '${path}' resolves outside project directory (symlink traversal)` };
|
|
130
73
|
}
|
|
131
74
|
}
|
|
75
|
+
catch {
|
|
76
|
+
// realpathSync fails for broken symlinks and inaccessible ancestors.
|
|
77
|
+
return { valid: false, absolutePath, error: `Path '${path}' could not be resolved` };
|
|
78
|
+
}
|
|
132
79
|
return { valid: true, absolutePath };
|
|
133
80
|
}
|
|
134
81
|
/**
|
|
@@ -145,6 +92,11 @@ function listDirectory(dir, projectRoot, recursive, prefix = '', ignoreRules, vi
|
|
|
145
92
|
continue;
|
|
146
93
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
147
94
|
try {
|
|
95
|
+
// Recursive listing must not follow an in-workspace symlink into an
|
|
96
|
+
// external directory. Top-level paths already pass validatePath, but
|
|
97
|
+
// each discovered symlink needs the same boundary check.
|
|
98
|
+
if (entry.isSymbolicLink() && !validatePath(fullPath, projectRoot).valid)
|
|
99
|
+
continue;
|
|
148
100
|
const st = statSync(fullPath); // follows symlinks
|
|
149
101
|
if (st.isDirectory()) {
|
|
150
102
|
if (visitedInodes.has(st.ino))
|
|
@@ -533,6 +485,14 @@ async function dispatchTool(tool, parameters, projectRoot, fs, toolCall) {
|
|
|
533
485
|
const args = parameters.args || [];
|
|
534
486
|
if (!command)
|
|
535
487
|
return { success: false, output: '', error: 'Missing required parameter: command', tool, parameters };
|
|
488
|
+
// Command throttle — guards against agent loops that spawn commands
|
|
489
|
+
// every iteration (each can be up to 2 minutes of subprocess time).
|
|
490
|
+
// Rate-limited *after* permission resolution: an allowed command
|
|
491
|
+
// consumes budget, a denied one never reaches here.
|
|
492
|
+
const cmdRate = checkCommandRateLimit();
|
|
493
|
+
if (!cmdRate.allowed) {
|
|
494
|
+
return { success: false, output: '', error: cmdRate.message || 'Command rate limit exceeded', tool, parameters };
|
|
495
|
+
}
|
|
536
496
|
recordCommand(command, args);
|
|
537
497
|
const result = await executeCommandAsync(command, args, {
|
|
538
498
|
cwd: projectRoot,
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -328,16 +328,16 @@ export interface AdditionalToolDef {
|
|
|
328
328
|
* Optionally appends an "Additional tools" section listing tools from
|
|
329
329
|
* sources outside the built-in catalog (currently MCP servers).
|
|
330
330
|
*/
|
|
331
|
-
export declare function formatToolDefinitions(additionalTools?: AdditionalToolDef[]): string;
|
|
331
|
+
export declare function formatToolDefinitions(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): string;
|
|
332
332
|
/**
|
|
333
333
|
* Get tools in OpenAI Function Calling format.
|
|
334
334
|
* Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
|
|
335
335
|
*/
|
|
336
|
-
export declare function getOpenAITools(additionalTools?: AdditionalToolDef[]): OpenAITool[];
|
|
336
|
+
export declare function getOpenAITools(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): OpenAITool[];
|
|
337
337
|
/**
|
|
338
338
|
* Get tools in Anthropic Tool Use format.
|
|
339
339
|
* Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
|
|
340
340
|
*/
|
|
341
|
-
export declare function getAnthropicTools(additionalTools?: AdditionalToolDef[]): AnthropicTool[];
|
|
341
|
+
export declare function getAnthropicTools(additionalTools?: AdditionalToolDef[], allowedToolNames?: ReadonlySet<string>): AnthropicTool[];
|
|
342
342
|
export { normalizeToolName, parseOpenAIToolCalls, parseAnthropicToolCalls, parseToolCalls } from './toolParsing';
|
|
343
343
|
export { executeTool, validatePath, createActionLog } from './toolExecution';
|
package/dist/utils/tools.js
CHANGED
|
@@ -140,11 +140,13 @@ export const AGENT_TOOLS = {
|
|
|
140
140
|
/**
|
|
141
141
|
* Get filtered tool entries (excludes provider-specific tools when API key not available)
|
|
142
142
|
*/
|
|
143
|
-
function getFilteredToolEntries() {
|
|
143
|
+
function getFilteredToolEntries(allowedToolNames) {
|
|
144
144
|
const hasMcp = hasZaiMcpAccess();
|
|
145
145
|
const hasZaiVision = hasZaiVisionAccess();
|
|
146
146
|
const hasMinimaxMcp = hasMinimaxMcpAccess();
|
|
147
147
|
return Object.entries(AGENT_TOOLS).filter(([name]) => {
|
|
148
|
+
if (allowedToolNames && !allowedToolNames.has(name))
|
|
149
|
+
return false;
|
|
148
150
|
if (ZAI_MCP_TOOLS.includes(name))
|
|
149
151
|
return hasMcp;
|
|
150
152
|
if (ZAI_VISION_TOOLS.includes(name))
|
|
@@ -159,9 +161,9 @@ function getFilteredToolEntries() {
|
|
|
159
161
|
* Optionally appends an "Additional tools" section listing tools from
|
|
160
162
|
* sources outside the built-in catalog (currently MCP servers).
|
|
161
163
|
*/
|
|
162
|
-
export function formatToolDefinitions(additionalTools) {
|
|
164
|
+
export function formatToolDefinitions(additionalTools, allowedToolNames) {
|
|
163
165
|
const lines = [];
|
|
164
|
-
for (const [name, tool] of getFilteredToolEntries()) {
|
|
166
|
+
for (const [name, tool] of getFilteredToolEntries(allowedToolNames)) {
|
|
165
167
|
lines.push(`### ${name}`);
|
|
166
168
|
lines.push(tool.description);
|
|
167
169
|
lines.push('Parameters:');
|
|
@@ -171,7 +173,8 @@ export function formatToolDefinitions(additionalTools) {
|
|
|
171
173
|
}
|
|
172
174
|
lines.push('');
|
|
173
175
|
}
|
|
174
|
-
|
|
176
|
+
const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
|
|
177
|
+
if (allowedAdditionalTools?.length) {
|
|
175
178
|
// Token budget caps. MCP servers can return tools with absurdly verbose
|
|
176
179
|
// JSON Schema definitions (multi-KB per tool). Without caps a 30-tool
|
|
177
180
|
// server eats ~10K tokens of every prompt. Per-tool 2KB + total 16KB
|
|
@@ -183,7 +186,7 @@ export function formatToolDefinitions(additionalTools) {
|
|
|
183
186
|
let skipped = 0;
|
|
184
187
|
lines.push('## Additional tools (from MCP servers)');
|
|
185
188
|
lines.push('');
|
|
186
|
-
for (const tool of
|
|
189
|
+
for (const tool of allowedAdditionalTools) {
|
|
187
190
|
if (budget <= 0) {
|
|
188
191
|
skipped++;
|
|
189
192
|
continue;
|
|
@@ -227,8 +230,8 @@ function additionalToolSchema(tool) {
|
|
|
227
230
|
* Get tools in OpenAI Function Calling format.
|
|
228
231
|
* Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
|
|
229
232
|
*/
|
|
230
|
-
export function getOpenAITools(additionalTools) {
|
|
231
|
-
const builtin = getFilteredToolEntries().map(([name, tool]) => {
|
|
233
|
+
export function getOpenAITools(additionalTools, allowedToolNames) {
|
|
234
|
+
const builtin = getFilteredToolEntries(allowedToolNames).map(([name, tool]) => {
|
|
232
235
|
const properties = {};
|
|
233
236
|
const required = [];
|
|
234
237
|
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
@@ -251,9 +254,10 @@ export function getOpenAITools(additionalTools) {
|
|
|
251
254
|
},
|
|
252
255
|
};
|
|
253
256
|
});
|
|
254
|
-
|
|
257
|
+
const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
|
|
258
|
+
if (!allowedAdditionalTools?.length)
|
|
255
259
|
return builtin;
|
|
256
|
-
const extra =
|
|
260
|
+
const extra = allowedAdditionalTools.map(t => ({
|
|
257
261
|
type: 'function',
|
|
258
262
|
function: {
|
|
259
263
|
name: t.name,
|
|
@@ -269,8 +273,8 @@ export function getOpenAITools(additionalTools) {
|
|
|
269
273
|
* Get tools in Anthropic Tool Use format.
|
|
270
274
|
* Additional tools (e.g. MCP) are appended with their JSON-schema as-is.
|
|
271
275
|
*/
|
|
272
|
-
export function getAnthropicTools(additionalTools) {
|
|
273
|
-
const builtin = getFilteredToolEntries().map(([name, tool]) => {
|
|
276
|
+
export function getAnthropicTools(additionalTools, allowedToolNames) {
|
|
277
|
+
const builtin = getFilteredToolEntries(allowedToolNames).map(([name, tool]) => {
|
|
274
278
|
const properties = {};
|
|
275
279
|
const required = [];
|
|
276
280
|
for (const [param, info] of Object.entries(tool.parameters)) {
|
|
@@ -290,9 +294,10 @@ export function getAnthropicTools(additionalTools) {
|
|
|
290
294
|
input_schema: { type: 'object', properties, required },
|
|
291
295
|
};
|
|
292
296
|
});
|
|
293
|
-
|
|
297
|
+
const allowedAdditionalTools = additionalTools?.filter(tool => !allowedToolNames || allowedToolNames.has(tool.name));
|
|
298
|
+
if (!allowedAdditionalTools?.length)
|
|
294
299
|
return builtin;
|
|
295
|
-
const extra =
|
|
300
|
+
const extra = allowedAdditionalTools.map(t => ({
|
|
296
301
|
name: t.name,
|
|
297
302
|
description: t.description ?? `External tool: ${t.name}`,
|
|
298
303
|
input_schema: additionalToolSchema(t),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "2.
|
|
1
|
+
export declare const VERSION = "2.20.0";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
|
|
2
2
|
// Baked from package.json at build time so the bun-compiled binary reports
|
|
3
3
|
// the right version (it has no package.json on disk to read at runtime).
|
|
4
|
-
export const VERSION = '2.
|
|
4
|
+
export const VERSION = '2.20.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|