deepagents 1.10.2 → 1.10.5
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 +19 -0
- package/dist/agent-2caqZpg2.d.cts +4023 -0
- package/dist/agent-DURA4_mf.d.ts +4024 -0
- package/dist/browser.cjs +43 -0
- package/dist/browser.d.cts +3 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +2 -0
- package/dist/index.cjs +52 -8849
- package/dist/index.d.cts +2 -3959
- package/dist/index.d.ts +2 -3961
- package/dist/index.js +3 -8763
- package/dist/langsmith-ZfNZ_Pyb.cjs +7639 -0
- package/dist/langsmith-ZfNZ_Pyb.cjs.map +1 -0
- package/dist/langsmith-wdF8zG42.js +7321 -0
- package/dist/langsmith-wdF8zG42.js.map +1 -0
- package/dist/node.cjs +53 -0
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +3 -0
- package/dist/src-DybKvM4T.js +1402 -0
- package/dist/src-DybKvM4T.js.map +1 -0
- package/dist/src-plSII1Kf.cjs +1451 -0
- package/dist/src-plSII1Kf.cjs.map +1 -0
- package/package.json +31 -10
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1402 @@
|
|
|
1
|
+
import { G as getMimeType, K as isTextMimeType, R as SandboxError, W as checkEmptyContent, q as performStringReplacement } from "./langsmith-wdF8zG42.js";
|
|
2
|
+
import { createMiddleware } from "langchain";
|
|
3
|
+
import micromatch from "micromatch";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import yaml from "yaml";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import fs$1 from "node:fs/promises";
|
|
10
|
+
import cp, { spawn } from "node:child_process";
|
|
11
|
+
import fg from "fast-glob";
|
|
12
|
+
//#region src/config.ts
|
|
13
|
+
/**
|
|
14
|
+
* Configuration and settings for deepagents.
|
|
15
|
+
*
|
|
16
|
+
* Provides project detection, path management, and environment configuration
|
|
17
|
+
* for skills and agent memory middleware.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Find the project root by looking for .git directory.
|
|
21
|
+
*
|
|
22
|
+
* Walks up the directory tree from startPath (or cwd) looking for a .git
|
|
23
|
+
* directory, which indicates the project root.
|
|
24
|
+
*
|
|
25
|
+
* @param startPath - Directory to start searching from. Defaults to current working directory.
|
|
26
|
+
* @returns Path to the project root if found, null otherwise.
|
|
27
|
+
*/
|
|
28
|
+
function findProjectRoot(startPath) {
|
|
29
|
+
let current = path.resolve(startPath || process.cwd());
|
|
30
|
+
while (current !== path.dirname(current)) {
|
|
31
|
+
const gitDir = path.join(current, ".git");
|
|
32
|
+
if (fs.existsSync(gitDir)) return current;
|
|
33
|
+
current = path.dirname(current);
|
|
34
|
+
}
|
|
35
|
+
const rootGitDir = path.join(current, ".git");
|
|
36
|
+
if (fs.existsSync(rootGitDir)) return current;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate agent name to prevent invalid filesystem paths and security issues.
|
|
41
|
+
*
|
|
42
|
+
* @param agentName - The agent name to validate
|
|
43
|
+
* @returns True if valid, false otherwise
|
|
44
|
+
*/
|
|
45
|
+
function isValidAgentName(agentName) {
|
|
46
|
+
if (!agentName || !agentName.trim()) return false;
|
|
47
|
+
return /^[a-zA-Z0-9_\-\s]+$/.test(agentName);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a Settings instance with detected environment.
|
|
51
|
+
*
|
|
52
|
+
* @param options - Configuration options
|
|
53
|
+
* @returns Settings instance with project detection and path management
|
|
54
|
+
*/
|
|
55
|
+
function createSettings(options = {}) {
|
|
56
|
+
const projectRoot = findProjectRoot(options.startPath);
|
|
57
|
+
const userDeepagentsDir = path.join(os.homedir(), ".deepagents");
|
|
58
|
+
return {
|
|
59
|
+
projectRoot,
|
|
60
|
+
userDeepagentsDir,
|
|
61
|
+
hasProject: projectRoot !== null,
|
|
62
|
+
getAgentDir(agentName) {
|
|
63
|
+
if (!isValidAgentName(agentName)) throw new Error(`Invalid agent name: ${JSON.stringify(agentName)}. Agent names can only contain letters, numbers, hyphens, underscores, and spaces.`);
|
|
64
|
+
return path.join(userDeepagentsDir, agentName);
|
|
65
|
+
},
|
|
66
|
+
ensureAgentDir(agentName) {
|
|
67
|
+
const agentDir = this.getAgentDir(agentName);
|
|
68
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
69
|
+
return agentDir;
|
|
70
|
+
},
|
|
71
|
+
getUserAgentMdPath(agentName) {
|
|
72
|
+
return path.join(this.getAgentDir(agentName), "agent.md");
|
|
73
|
+
},
|
|
74
|
+
getProjectAgentMdPath() {
|
|
75
|
+
if (!projectRoot) return null;
|
|
76
|
+
return path.join(projectRoot, ".deepagents", "agent.md");
|
|
77
|
+
},
|
|
78
|
+
getUserSkillsDir(agentName) {
|
|
79
|
+
return path.join(this.getAgentDir(agentName), "skills");
|
|
80
|
+
},
|
|
81
|
+
ensureUserSkillsDir(agentName) {
|
|
82
|
+
const skillsDir = this.getUserSkillsDir(agentName);
|
|
83
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
84
|
+
return skillsDir;
|
|
85
|
+
},
|
|
86
|
+
getProjectSkillsDir() {
|
|
87
|
+
if (!projectRoot) return null;
|
|
88
|
+
return path.join(projectRoot, ".deepagents", "skills");
|
|
89
|
+
},
|
|
90
|
+
ensureProjectSkillsDir() {
|
|
91
|
+
const skillsDir = this.getProjectSkillsDir();
|
|
92
|
+
if (!skillsDir) return null;
|
|
93
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
94
|
+
return skillsDir;
|
|
95
|
+
},
|
|
96
|
+
ensureProjectDeepagentsDir() {
|
|
97
|
+
if (!projectRoot) return null;
|
|
98
|
+
const deepagentsDir = path.join(projectRoot, ".deepagents");
|
|
99
|
+
fs.mkdirSync(deepagentsDir, { recursive: true });
|
|
100
|
+
return deepagentsDir;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/middleware/agent-memory.ts
|
|
106
|
+
/**
|
|
107
|
+
* Middleware for loading agent-specific long-term memory into the system prompt.
|
|
108
|
+
*
|
|
109
|
+
* This middleware loads the agent's long-term memory from agent.md files
|
|
110
|
+
* and injects it into the system prompt. Memory is loaded from:
|
|
111
|
+
* - User memory: ~/.deepagents/{agent_name}/agent.md
|
|
112
|
+
* - Project memory: {project_root}/.deepagents/agent.md
|
|
113
|
+
*
|
|
114
|
+
* @deprecated Use `createMemoryMiddleware` from `./memory.js` instead.
|
|
115
|
+
* This middleware uses direct filesystem access (Node.js fs module) which is not
|
|
116
|
+
* portable across backends. The `createMemoryMiddleware` function uses the
|
|
117
|
+
* `BackendProtocol` abstraction and follows the AGENTS.md specification.
|
|
118
|
+
*
|
|
119
|
+
* Migration example:
|
|
120
|
+
* ```typescript
|
|
121
|
+
* // Before (deprecated):
|
|
122
|
+
* import { createAgentMemoryMiddleware } from "./agent-memory.js";
|
|
123
|
+
* const middleware = createAgentMemoryMiddleware({ settings, assistantId });
|
|
124
|
+
*
|
|
125
|
+
* // After (recommended):
|
|
126
|
+
* import { createMemoryMiddleware } from "./memory.js";
|
|
127
|
+
* import { FilesystemBackend } from "../backends/filesystem.js";
|
|
128
|
+
*
|
|
129
|
+
* const middleware = createMemoryMiddleware({
|
|
130
|
+
* backend: new FilesystemBackend({ rootDir: "/" }),
|
|
131
|
+
* sources: [
|
|
132
|
+
* `~/.deepagents/${assistantId}/AGENTS.md`,
|
|
133
|
+
* `${projectRoot}/.deepagents/AGENTS.md`,
|
|
134
|
+
* ],
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* State schema for agent memory middleware.
|
|
140
|
+
*/
|
|
141
|
+
const AgentMemoryStateSchema = z.object({
|
|
142
|
+
/** Personal preferences from ~/.deepagents/{agent}/ (applies everywhere) */
|
|
143
|
+
userMemory: z.string().optional(),
|
|
144
|
+
/** Project-specific context (loaded from project root) */
|
|
145
|
+
projectMemory: z.string().optional()
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* Default template for memory injection.
|
|
149
|
+
*/
|
|
150
|
+
const DEFAULT_MEMORY_TEMPLATE = `<user_memory>
|
|
151
|
+
{user_memory}
|
|
152
|
+
</user_memory>
|
|
153
|
+
|
|
154
|
+
<project_memory>
|
|
155
|
+
{project_memory}
|
|
156
|
+
</project_memory>`;
|
|
157
|
+
/**
|
|
158
|
+
* Long-term Memory Documentation system prompt.
|
|
159
|
+
*/
|
|
160
|
+
const LONGTERM_MEMORY_SYSTEM_PROMPT = `
|
|
161
|
+
|
|
162
|
+
## Long-term Memory
|
|
163
|
+
|
|
164
|
+
Your long-term memory is stored in files on the filesystem and persists across sessions.
|
|
165
|
+
|
|
166
|
+
**User Memory Location**: \`{agent_dir_absolute}\` (displays as \`{agent_dir_display}\`)
|
|
167
|
+
**Project Memory Location**: {project_memory_info}
|
|
168
|
+
|
|
169
|
+
Your system prompt is loaded from TWO sources at startup:
|
|
170
|
+
1. **User agent.md**: \`{agent_dir_absolute}/agent.md\` - Your personal preferences across all projects
|
|
171
|
+
2. **Project agent.md**: Loaded from project root if available - Project-specific instructions
|
|
172
|
+
|
|
173
|
+
Project-specific agent.md is loaded from these locations (both combined if both exist):
|
|
174
|
+
- \`[project-root]/.deepagents/agent.md\` (preferred)
|
|
175
|
+
- \`[project-root]/agent.md\` (fallback, but also included if both exist)
|
|
176
|
+
|
|
177
|
+
**When to CHECK/READ memories (CRITICAL - do this FIRST):**
|
|
178
|
+
- **At the start of ANY new session**: Check both user and project memories
|
|
179
|
+
- User: \`ls {agent_dir_absolute}\`
|
|
180
|
+
- Project: \`ls {project_deepagents_dir}\` (if in a project)
|
|
181
|
+
- **BEFORE answering questions**: If asked "what do you know about X?" or "how do I do Y?", check project memories FIRST, then user
|
|
182
|
+
- **When user asks you to do something**: Check if you have project-specific guides or examples
|
|
183
|
+
- **When user references past work**: Search project memory files for related context
|
|
184
|
+
|
|
185
|
+
**Memory-first response pattern:**
|
|
186
|
+
1. User asks a question → Check project directory first: \`ls {project_deepagents_dir}\`
|
|
187
|
+
2. If relevant files exist → Read them with \`read_file '{project_deepagents_dir}/[filename]'\`
|
|
188
|
+
3. Check user memory if needed → \`ls {agent_dir_absolute}\`
|
|
189
|
+
4. Base your answer on saved knowledge supplemented by general knowledge
|
|
190
|
+
|
|
191
|
+
**When to update memories:**
|
|
192
|
+
- **IMMEDIATELY when the user describes your role or how you should behave**
|
|
193
|
+
- **IMMEDIATELY when the user gives feedback on your work** - Update memories to capture what was wrong and how to do it better
|
|
194
|
+
- When the user explicitly asks you to remember something
|
|
195
|
+
- When patterns or preferences emerge (coding styles, conventions, workflows)
|
|
196
|
+
- After significant work where context would help in future sessions
|
|
197
|
+
|
|
198
|
+
**Learning from feedback:**
|
|
199
|
+
- When user says something is better/worse, capture WHY and encode it as a pattern
|
|
200
|
+
- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions
|
|
201
|
+
- When user says "you should remember X" or "be careful about Y", treat this as HIGH PRIORITY - update memories IMMEDIATELY
|
|
202
|
+
- Look for the underlying principle behind corrections, not just the specific mistake
|
|
203
|
+
|
|
204
|
+
## Deciding Where to Store Memory
|
|
205
|
+
|
|
206
|
+
When writing or updating agent memory, decide whether each fact, configuration, or behavior belongs in:
|
|
207
|
+
|
|
208
|
+
### User Agent File: \`{agent_dir_absolute}/agent.md\`
|
|
209
|
+
→ Describes the agent's **personality, style, and universal behavior** across all projects.
|
|
210
|
+
|
|
211
|
+
**Store here:**
|
|
212
|
+
- Your general tone and communication style
|
|
213
|
+
- Universal coding preferences (formatting, comment style, etc.)
|
|
214
|
+
- General workflows and methodologies you follow
|
|
215
|
+
- Tool usage patterns that apply everywhere
|
|
216
|
+
- Personal preferences that don't change per-project
|
|
217
|
+
|
|
218
|
+
**Examples:**
|
|
219
|
+
- "Be concise and direct in responses"
|
|
220
|
+
- "Always use type hints in Python"
|
|
221
|
+
- "Prefer functional programming patterns"
|
|
222
|
+
|
|
223
|
+
### Project Agent File: \`{project_deepagents_dir}/agent.md\`
|
|
224
|
+
→ Describes **how this specific project works** and **how the agent should behave here only.**
|
|
225
|
+
|
|
226
|
+
**Store here:**
|
|
227
|
+
- Project-specific architecture and design patterns
|
|
228
|
+
- Coding conventions specific to this codebase
|
|
229
|
+
- Project structure and organization
|
|
230
|
+
- Testing strategies for this project
|
|
231
|
+
- Deployment processes and workflows
|
|
232
|
+
- Team conventions and guidelines
|
|
233
|
+
|
|
234
|
+
**Examples:**
|
|
235
|
+
- "This project uses FastAPI with SQLAlchemy"
|
|
236
|
+
- "Tests go in tests/ directory mirroring src/ structure"
|
|
237
|
+
- "All API changes require updating OpenAPI spec"
|
|
238
|
+
|
|
239
|
+
### Project Memory Files: \`{project_deepagents_dir}/*.md\`
|
|
240
|
+
→ Use for **project-specific reference information** and structured notes.
|
|
241
|
+
|
|
242
|
+
**Store here:**
|
|
243
|
+
- API design documentation
|
|
244
|
+
- Architecture decisions and rationale
|
|
245
|
+
- Deployment procedures
|
|
246
|
+
- Common debugging patterns
|
|
247
|
+
- Onboarding information
|
|
248
|
+
|
|
249
|
+
**Examples:**
|
|
250
|
+
- \`{project_deepagents_dir}/api-design.md\` - REST API patterns used
|
|
251
|
+
- \`{project_deepagents_dir}/architecture.md\` - System architecture overview
|
|
252
|
+
- \`{project_deepagents_dir}/deployment.md\` - How to deploy this project
|
|
253
|
+
|
|
254
|
+
### File Operations:
|
|
255
|
+
|
|
256
|
+
**User memory:**
|
|
257
|
+
\`\`\`
|
|
258
|
+
ls {agent_dir_absolute} # List user memory files
|
|
259
|
+
read_file '{agent_dir_absolute}/agent.md' # Read user preferences
|
|
260
|
+
edit_file '{agent_dir_absolute}/agent.md' ... # Update user preferences
|
|
261
|
+
\`\`\`
|
|
262
|
+
|
|
263
|
+
**Project memory (preferred for project-specific information):**
|
|
264
|
+
\`\`\`
|
|
265
|
+
ls {project_deepagents_dir} # List project memory files
|
|
266
|
+
read_file '{project_deepagents_dir}/agent.md' # Read project instructions
|
|
267
|
+
edit_file '{project_deepagents_dir}/agent.md' ... # Update project instructions
|
|
268
|
+
write_file '{project_deepagents_dir}/agent.md' ... # Create project memory file
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
**Important**:
|
|
272
|
+
- Project memory files are stored in \`.deepagents/\` inside the project root
|
|
273
|
+
- Always use absolute paths for file operations
|
|
274
|
+
- Check project memories BEFORE user when answering project-specific questions`;
|
|
275
|
+
/**
|
|
276
|
+
* Create middleware for loading agent-specific long-term memory.
|
|
277
|
+
*
|
|
278
|
+
* This middleware loads the agent's long-term memory from a file (agent.md)
|
|
279
|
+
* and injects it into the system prompt. The memory is loaded once at the
|
|
280
|
+
* start of the conversation and stored in state.
|
|
281
|
+
*
|
|
282
|
+
* @param options - Configuration options
|
|
283
|
+
* @returns AgentMiddleware for memory loading and injection
|
|
284
|
+
*
|
|
285
|
+
* @deprecated Use `createMemoryMiddleware` from `./memory.js` instead.
|
|
286
|
+
* This function uses direct filesystem access which limits portability.
|
|
287
|
+
*/
|
|
288
|
+
function createAgentMemoryMiddleware(options) {
|
|
289
|
+
const { settings, assistantId, systemPromptTemplate } = options;
|
|
290
|
+
const agentDir = settings.getAgentDir(assistantId);
|
|
291
|
+
const agentDirDisplay = `~/.deepagents/${assistantId}`;
|
|
292
|
+
const agentDirAbsolute = agentDir;
|
|
293
|
+
const projectRoot = settings.projectRoot;
|
|
294
|
+
const projectMemoryInfo = projectRoot ? `\`${projectRoot}\` (detected)` : "None (not in a git project)";
|
|
295
|
+
const projectDeepagentsDir = projectRoot ? `${projectRoot}/.deepagents` : "[project-root]/.deepagents (not in a project)";
|
|
296
|
+
const template = systemPromptTemplate || DEFAULT_MEMORY_TEMPLATE;
|
|
297
|
+
return createMiddleware({
|
|
298
|
+
name: "AgentMemoryMiddleware",
|
|
299
|
+
stateSchema: AgentMemoryStateSchema,
|
|
300
|
+
beforeAgent(state) {
|
|
301
|
+
const result = {};
|
|
302
|
+
if (!("userMemory" in state)) {
|
|
303
|
+
const userPath = settings.getUserAgentMdPath(assistantId);
|
|
304
|
+
if (fs.existsSync(userPath)) try {
|
|
305
|
+
result.userMemory = fs.readFileSync(userPath, "utf-8");
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
if (!("projectMemory" in state)) {
|
|
309
|
+
const projectPath = settings.getProjectAgentMdPath();
|
|
310
|
+
if (projectPath && fs.existsSync(projectPath)) try {
|
|
311
|
+
result.projectMemory = fs.readFileSync(projectPath, "utf-8");
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
315
|
+
},
|
|
316
|
+
wrapModelCall(request, handler) {
|
|
317
|
+
const userMemory = request.state?.userMemory;
|
|
318
|
+
const projectMemory = request.state?.projectMemory;
|
|
319
|
+
const baseSystemPrompt = request.systemPrompt || "";
|
|
320
|
+
const memorySection = template.replace("{user_memory}", userMemory || "(No user agent.md)").replace("{project_memory}", projectMemory || "(No project agent.md)");
|
|
321
|
+
const memoryDocs = LONGTERM_MEMORY_SYSTEM_PROMPT.replaceAll("{agent_dir_absolute}", agentDirAbsolute).replaceAll("{agent_dir_display}", agentDirDisplay).replaceAll("{project_memory_info}", projectMemoryInfo).replaceAll("{project_deepagents_dir}", projectDeepagentsDir);
|
|
322
|
+
let systemPrompt = memorySection;
|
|
323
|
+
if (baseSystemPrompt) systemPrompt += "\n\n" + baseSystemPrompt;
|
|
324
|
+
systemPrompt += "\n\n" + memoryDocs;
|
|
325
|
+
return handler({
|
|
326
|
+
...request,
|
|
327
|
+
systemPrompt
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const MAX_SKILL_DESCRIPTION_LENGTH = 1024;
|
|
333
|
+
/** Pattern for validating skill names per Agent Skills spec */
|
|
334
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
335
|
+
/** Pattern for extracting YAML frontmatter */
|
|
336
|
+
const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
337
|
+
/**
|
|
338
|
+
* Check if a path is safely contained within base_dir.
|
|
339
|
+
*
|
|
340
|
+
* This prevents directory traversal attacks via symlinks or path manipulation.
|
|
341
|
+
* The function resolves both paths to their canonical form (following symlinks)
|
|
342
|
+
* and verifies that the target path is within the base directory.
|
|
343
|
+
*
|
|
344
|
+
* @param targetPath - The path to validate
|
|
345
|
+
* @param baseDir - The base directory that should contain the path
|
|
346
|
+
* @returns True if the path is safely within baseDir, false otherwise
|
|
347
|
+
*/
|
|
348
|
+
function isSafePath(targetPath, baseDir) {
|
|
349
|
+
try {
|
|
350
|
+
const resolvedPath = fs.realpathSync(targetPath);
|
|
351
|
+
const resolvedBase = fs.realpathSync(baseDir);
|
|
352
|
+
return resolvedPath.startsWith(resolvedBase + path.sep) || resolvedPath === resolvedBase;
|
|
353
|
+
} catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Validate skill name per Agent Skills spec.
|
|
359
|
+
*
|
|
360
|
+
* Requirements:
|
|
361
|
+
* - Max 64 characters
|
|
362
|
+
* - Lowercase alphanumeric and hyphens only (a-z, 0-9, -)
|
|
363
|
+
* - Cannot start or end with hyphen
|
|
364
|
+
* - No consecutive hyphens
|
|
365
|
+
* - Must match parent directory name
|
|
366
|
+
*
|
|
367
|
+
* @param name - The skill name from YAML frontmatter
|
|
368
|
+
* @param directoryName - The parent directory name
|
|
369
|
+
* @returns Validation result with error message if invalid
|
|
370
|
+
*/
|
|
371
|
+
function validateSkillName(name, directoryName) {
|
|
372
|
+
if (!name) return {
|
|
373
|
+
valid: false,
|
|
374
|
+
error: "name is required"
|
|
375
|
+
};
|
|
376
|
+
if (name.length > 64) return {
|
|
377
|
+
valid: false,
|
|
378
|
+
error: "name exceeds 64 characters"
|
|
379
|
+
};
|
|
380
|
+
if (!SKILL_NAME_PATTERN.test(name)) return {
|
|
381
|
+
valid: false,
|
|
382
|
+
error: "name must be lowercase alphanumeric with single hyphens only"
|
|
383
|
+
};
|
|
384
|
+
if (name !== directoryName) return {
|
|
385
|
+
valid: false,
|
|
386
|
+
error: `name '${name}' must match directory name '${directoryName}'`
|
|
387
|
+
};
|
|
388
|
+
return { valid: true };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Parse YAML frontmatter from content.
|
|
392
|
+
*
|
|
393
|
+
* @param content - The file content
|
|
394
|
+
* @returns Parsed frontmatter object, or null if parsing fails
|
|
395
|
+
*/
|
|
396
|
+
function parseFrontmatter(content) {
|
|
397
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
398
|
+
if (!match) return null;
|
|
399
|
+
try {
|
|
400
|
+
const parsed = yaml.parse(match[1]);
|
|
401
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Parse YAML frontmatter from a SKILL.md file per Agent Skills spec.
|
|
408
|
+
*
|
|
409
|
+
* @param skillMdPath - Path to the SKILL.md file
|
|
410
|
+
* @param source - Source of the skill ('user' or 'project')
|
|
411
|
+
* @returns SkillMetadata with all fields, or null if parsing fails
|
|
412
|
+
*/
|
|
413
|
+
function parseSkillMetadata(skillMdPath, source) {
|
|
414
|
+
try {
|
|
415
|
+
const stats = fs.statSync(skillMdPath);
|
|
416
|
+
if (stats.size > 10485760) {
|
|
417
|
+
console.warn(`Skipping ${skillMdPath}: file too large (${stats.size} bytes)`);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const frontmatter = parseFrontmatter(fs.readFileSync(skillMdPath, "utf-8"));
|
|
421
|
+
if (!frontmatter) {
|
|
422
|
+
console.warn(`Skipping ${skillMdPath}: no valid YAML frontmatter found`);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const name = frontmatter.name;
|
|
426
|
+
const description = frontmatter.description;
|
|
427
|
+
if (!name || !description) {
|
|
428
|
+
console.warn(`Skipping ${skillMdPath}: missing required 'name' or 'description'`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const directoryName = path.basename(path.dirname(skillMdPath));
|
|
432
|
+
const validation = validateSkillName(String(name), directoryName);
|
|
433
|
+
if (!validation.valid) console.warn(`Skill '${name}' in ${skillMdPath} does not follow Agent Skills spec: ${validation.error}. Consider renaming to be spec-compliant.`);
|
|
434
|
+
let descriptionStr = String(description);
|
|
435
|
+
if (descriptionStr.length > 1024) {
|
|
436
|
+
console.warn(`Description exceeds ${MAX_SKILL_DESCRIPTION_LENGTH} chars in ${skillMdPath}, truncating`);
|
|
437
|
+
descriptionStr = descriptionStr.slice(0, MAX_SKILL_DESCRIPTION_LENGTH);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
name: String(name),
|
|
441
|
+
description: descriptionStr,
|
|
442
|
+
path: skillMdPath,
|
|
443
|
+
source,
|
|
444
|
+
license: frontmatter.license ? String(frontmatter.license) : void 0,
|
|
445
|
+
compatibility: frontmatter.compatibility ? String(frontmatter.compatibility) : void 0,
|
|
446
|
+
metadata: frontmatter.metadata && typeof frontmatter.metadata === "object" ? frontmatter.metadata : void 0,
|
|
447
|
+
allowedTools: frontmatter["allowed-tools"] ? String(frontmatter["allowed-tools"]) : void 0
|
|
448
|
+
};
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.warn(`Error reading ${skillMdPath}: ${error}`);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* List all skills from a single skills directory (internal helper).
|
|
456
|
+
*
|
|
457
|
+
* Scans the skills directory for subdirectories containing SKILL.md files,
|
|
458
|
+
* parses YAML frontmatter, and returns skill metadata.
|
|
459
|
+
*
|
|
460
|
+
* Skills are organized as:
|
|
461
|
+
* ```
|
|
462
|
+
* skills/
|
|
463
|
+
* ├── skill-name/
|
|
464
|
+
* │ ├── SKILL.md # Required: instructions with YAML frontmatter
|
|
465
|
+
* │ ├── script.py # Optional: supporting files
|
|
466
|
+
* │ └── config.json # Optional: supporting files
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @param skillsDir - Path to the skills directory
|
|
470
|
+
* @param source - Source of the skills ('user' or 'project')
|
|
471
|
+
* @returns List of skill metadata
|
|
472
|
+
*/
|
|
473
|
+
function listSkillsFromDir(skillsDir, source) {
|
|
474
|
+
const expandedDir = skillsDir.startsWith("~") ? path.join(process.env.HOME || process.env.USERPROFILE || "", skillsDir.slice(1)) : skillsDir;
|
|
475
|
+
if (!fs.existsSync(expandedDir)) return [];
|
|
476
|
+
let resolvedBase;
|
|
477
|
+
try {
|
|
478
|
+
resolvedBase = fs.realpathSync(expandedDir);
|
|
479
|
+
} catch {
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
const skills = [];
|
|
483
|
+
let entries;
|
|
484
|
+
try {
|
|
485
|
+
entries = fs.readdirSync(resolvedBase, { withFileTypes: true });
|
|
486
|
+
} catch {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
const skillDir = path.join(resolvedBase, entry.name);
|
|
491
|
+
if (!isSafePath(skillDir, resolvedBase)) continue;
|
|
492
|
+
if (!entry.isDirectory()) continue;
|
|
493
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
494
|
+
if (!fs.existsSync(skillMdPath)) continue;
|
|
495
|
+
if (!isSafePath(skillMdPath, resolvedBase)) continue;
|
|
496
|
+
const metadata = parseSkillMetadata(skillMdPath, source);
|
|
497
|
+
if (metadata) skills.push(metadata);
|
|
498
|
+
}
|
|
499
|
+
return skills;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* List skills from user and/or project directories.
|
|
503
|
+
*
|
|
504
|
+
* When both directories are provided, project skills with the same name as
|
|
505
|
+
* user skills will override them.
|
|
506
|
+
*
|
|
507
|
+
* @param options - Options specifying which directories to search
|
|
508
|
+
* @returns Merged list of skill metadata from both sources, with project skills
|
|
509
|
+
* taking precedence over user skills when names conflict
|
|
510
|
+
*/
|
|
511
|
+
function listSkills(options) {
|
|
512
|
+
const allSkills = /* @__PURE__ */ new Map();
|
|
513
|
+
if (options.userSkillsDir) {
|
|
514
|
+
const userSkills = listSkillsFromDir(options.userSkillsDir, "user");
|
|
515
|
+
for (const skill of userSkills) allSkills.set(skill.name, skill);
|
|
516
|
+
}
|
|
517
|
+
if (options.projectSkillsDir) {
|
|
518
|
+
const projectSkills = listSkillsFromDir(options.projectSkillsDir, "project");
|
|
519
|
+
for (const skill of projectSkills) allSkills.set(skill.name, skill);
|
|
520
|
+
}
|
|
521
|
+
return Array.from(allSkills.values());
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/backends/filesystem.ts
|
|
525
|
+
/**
|
|
526
|
+
* FilesystemBackend: Read and write files directly from the filesystem.
|
|
527
|
+
*
|
|
528
|
+
* Security and search upgrades:
|
|
529
|
+
* - Secure path resolution with root containment when in virtual_mode (sandboxed to cwd)
|
|
530
|
+
* - Prevent symlink-following on file I/O using O_NOFOLLOW when available
|
|
531
|
+
* - Ripgrep-powered grep with literal (fixed-string) search, plus substring fallback
|
|
532
|
+
* and optional glob include filtering, while preserving virtual path behavior
|
|
533
|
+
*/
|
|
534
|
+
const SUPPORTS_NOFOLLOW = fs.constants.O_NOFOLLOW !== void 0;
|
|
535
|
+
/**
|
|
536
|
+
* Backend that reads and writes files directly from the filesystem.
|
|
537
|
+
*
|
|
538
|
+
* Files are accessed using their actual filesystem paths. Relative paths are
|
|
539
|
+
* resolved relative to the current working directory. Content is read/written
|
|
540
|
+
* as plain text, and metadata (timestamps) are derived from filesystem stats.
|
|
541
|
+
*/
|
|
542
|
+
var FilesystemBackend = class {
|
|
543
|
+
cwd;
|
|
544
|
+
virtualMode;
|
|
545
|
+
maxFileSizeBytes;
|
|
546
|
+
constructor(options = {}) {
|
|
547
|
+
const { rootDir, virtualMode = false, maxFileSizeMb = 10 } = options;
|
|
548
|
+
this.cwd = rootDir ? path.resolve(rootDir) : process.cwd();
|
|
549
|
+
this.virtualMode = virtualMode;
|
|
550
|
+
this.maxFileSizeBytes = maxFileSizeMb * 1024 * 1024;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Resolve a file path with security checks.
|
|
554
|
+
*
|
|
555
|
+
* When virtualMode=true, treat incoming paths as virtual absolute paths under
|
|
556
|
+
* this.cwd, disallow traversal (.., ~) and ensure resolved path stays within root.
|
|
557
|
+
* When virtualMode=false, preserve legacy behavior: absolute paths are allowed
|
|
558
|
+
* as-is; relative paths resolve under cwd.
|
|
559
|
+
*
|
|
560
|
+
* @param key - File path (absolute, relative, or virtual when virtualMode=true)
|
|
561
|
+
* @returns Resolved absolute path string
|
|
562
|
+
* @throws Error if path traversal detected or path outside root
|
|
563
|
+
*/
|
|
564
|
+
resolvePath(key) {
|
|
565
|
+
if (this.virtualMode) {
|
|
566
|
+
const vpath = key.startsWith("/") ? key : "/" + key;
|
|
567
|
+
if (vpath.includes("..") || vpath.startsWith("~")) throw new Error("Path traversal not allowed");
|
|
568
|
+
const full = path.resolve(this.cwd, vpath.substring(1));
|
|
569
|
+
const relative = path.relative(this.cwd, full);
|
|
570
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path: ${full} outside root directory: ${this.cwd}`);
|
|
571
|
+
return full;
|
|
572
|
+
}
|
|
573
|
+
if (path.isAbsolute(key)) return key;
|
|
574
|
+
return path.resolve(this.cwd, key);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* List files and directories in the specified directory (non-recursive).
|
|
578
|
+
*
|
|
579
|
+
* @param dirPath - Absolute directory path to list files from
|
|
580
|
+
* @returns List of FileInfo objects for files and directories directly in the directory.
|
|
581
|
+
* Directories have a trailing / in their path and is_dir=true.
|
|
582
|
+
*/
|
|
583
|
+
async ls(dirPath) {
|
|
584
|
+
try {
|
|
585
|
+
const resolvedPath = this.resolvePath(dirPath);
|
|
586
|
+
if (!(await fs$1.stat(resolvedPath)).isDirectory()) return { files: [] };
|
|
587
|
+
const entries = await fs$1.readdir(resolvedPath, { withFileTypes: true });
|
|
588
|
+
const results = [];
|
|
589
|
+
const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const fullPath = path.join(resolvedPath, entry.name);
|
|
592
|
+
try {
|
|
593
|
+
const entryStat = await fs$1.stat(fullPath);
|
|
594
|
+
const isFile = entryStat.isFile();
|
|
595
|
+
const isDir = entryStat.isDirectory();
|
|
596
|
+
if (!this.virtualMode) {
|
|
597
|
+
if (isFile) results.push({
|
|
598
|
+
path: fullPath,
|
|
599
|
+
is_dir: false,
|
|
600
|
+
size: entryStat.size,
|
|
601
|
+
modified_at: entryStat.mtime.toISOString()
|
|
602
|
+
});
|
|
603
|
+
else if (isDir) results.push({
|
|
604
|
+
path: fullPath + path.sep,
|
|
605
|
+
is_dir: true,
|
|
606
|
+
size: 0,
|
|
607
|
+
modified_at: entryStat.mtime.toISOString()
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
let relativePath;
|
|
611
|
+
if (fullPath.startsWith(cwdStr)) relativePath = fullPath.substring(cwdStr.length);
|
|
612
|
+
else if (fullPath.startsWith(this.cwd)) relativePath = fullPath.substring(this.cwd.length).replace(/^[/\\]/, "");
|
|
613
|
+
else relativePath = fullPath;
|
|
614
|
+
relativePath = relativePath.split(path.sep).join("/");
|
|
615
|
+
const virtPath = "/" + relativePath;
|
|
616
|
+
if (isFile) results.push({
|
|
617
|
+
path: virtPath,
|
|
618
|
+
is_dir: false,
|
|
619
|
+
size: entryStat.size,
|
|
620
|
+
modified_at: entryStat.mtime.toISOString()
|
|
621
|
+
});
|
|
622
|
+
else if (isDir) results.push({
|
|
623
|
+
path: virtPath + "/",
|
|
624
|
+
is_dir: true,
|
|
625
|
+
size: 0,
|
|
626
|
+
modified_at: entryStat.mtime.toISOString()
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
634
|
+
return { files: results };
|
|
635
|
+
} catch {
|
|
636
|
+
return { files: [] };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Read file content with line numbers.
|
|
641
|
+
*
|
|
642
|
+
* @param filePath - Absolute or relative file path
|
|
643
|
+
* @param offset - Line offset to start reading from (0-indexed)
|
|
644
|
+
* @param limit - Maximum number of lines to read
|
|
645
|
+
* @returns Formatted file content with line numbers, or error message
|
|
646
|
+
*/
|
|
647
|
+
async read(filePath, offset = 0, limit = 500) {
|
|
648
|
+
try {
|
|
649
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
650
|
+
const mimeType = getMimeType(filePath);
|
|
651
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
652
|
+
let content;
|
|
653
|
+
if (SUPPORTS_NOFOLLOW) {
|
|
654
|
+
if (!(await fs$1.stat(resolvedPath)).isFile()) return { error: `File '${filePath}' not found` };
|
|
655
|
+
const fd = await fs$1.open(resolvedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
656
|
+
try {
|
|
657
|
+
if (isBinary) {
|
|
658
|
+
const buffer = await fd.readFile();
|
|
659
|
+
return {
|
|
660
|
+
content: new Uint8Array(buffer),
|
|
661
|
+
mimeType
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
content = await fd.readFile({ encoding: "utf-8" });
|
|
665
|
+
} finally {
|
|
666
|
+
await fd.close();
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
const stat = await fs$1.lstat(resolvedPath);
|
|
670
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
671
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
672
|
+
if (isBinary) {
|
|
673
|
+
const buffer = await fs$1.readFile(resolvedPath);
|
|
674
|
+
return {
|
|
675
|
+
content: new Uint8Array(buffer),
|
|
676
|
+
mimeType
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
content = await fs$1.readFile(resolvedPath, "utf-8");
|
|
680
|
+
}
|
|
681
|
+
const emptyMsg = checkEmptyContent(content);
|
|
682
|
+
if (emptyMsg) return {
|
|
683
|
+
content: emptyMsg,
|
|
684
|
+
mimeType
|
|
685
|
+
};
|
|
686
|
+
const lines = content.split("\n");
|
|
687
|
+
const startIdx = offset;
|
|
688
|
+
const endIdx = Math.min(startIdx + limit, lines.length);
|
|
689
|
+
if (startIdx >= lines.length) return { error: `Line offset ${offset} exceeds file length (${lines.length} lines)` };
|
|
690
|
+
return {
|
|
691
|
+
content: lines.slice(startIdx, endIdx).join("\n"),
|
|
692
|
+
mimeType
|
|
693
|
+
};
|
|
694
|
+
} catch (e) {
|
|
695
|
+
return { error: `Error reading file '${filePath}': ${e.message}` };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Read file content as raw FileData.
|
|
700
|
+
*
|
|
701
|
+
* @param filePath - Absolute file path
|
|
702
|
+
* @returns ReadRawResult with raw file data on success or error on failure
|
|
703
|
+
*/
|
|
704
|
+
async readRaw(filePath) {
|
|
705
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
706
|
+
const mimeType = getMimeType(filePath);
|
|
707
|
+
const isBinary = !isTextMimeType(mimeType);
|
|
708
|
+
let content;
|
|
709
|
+
let stat;
|
|
710
|
+
if (SUPPORTS_NOFOLLOW) {
|
|
711
|
+
stat = await fs$1.stat(resolvedPath);
|
|
712
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
713
|
+
const fd = await fs$1.open(resolvedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
714
|
+
try {
|
|
715
|
+
if (isBinary) {
|
|
716
|
+
const buffer = await fd.readFile();
|
|
717
|
+
return { data: {
|
|
718
|
+
content: new Uint8Array(buffer),
|
|
719
|
+
mimeType,
|
|
720
|
+
created_at: stat.ctime.toISOString(),
|
|
721
|
+
modified_at: stat.mtime.toISOString()
|
|
722
|
+
} };
|
|
723
|
+
}
|
|
724
|
+
content = await fd.readFile({ encoding: "utf-8" });
|
|
725
|
+
} finally {
|
|
726
|
+
await fd.close();
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
stat = await fs$1.lstat(resolvedPath);
|
|
730
|
+
if (stat.isSymbolicLink()) return { error: `Symlinks are not allowed: ${filePath}` };
|
|
731
|
+
if (!stat.isFile()) return { error: `File '${filePath}' not found` };
|
|
732
|
+
if (isBinary) {
|
|
733
|
+
const buffer = await fs$1.readFile(resolvedPath);
|
|
734
|
+
return { data: {
|
|
735
|
+
content: new Uint8Array(buffer),
|
|
736
|
+
mimeType,
|
|
737
|
+
created_at: stat.ctime.toISOString(),
|
|
738
|
+
modified_at: stat.mtime.toISOString()
|
|
739
|
+
} };
|
|
740
|
+
}
|
|
741
|
+
content = await fs$1.readFile(resolvedPath, "utf-8");
|
|
742
|
+
}
|
|
743
|
+
return { data: {
|
|
744
|
+
content,
|
|
745
|
+
mimeType,
|
|
746
|
+
created_at: stat.ctime.toISOString(),
|
|
747
|
+
modified_at: stat.mtime.toISOString()
|
|
748
|
+
} };
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Create a new file with content.
|
|
752
|
+
* Returns WriteResult. External storage sets filesUpdate=null.
|
|
753
|
+
*/
|
|
754
|
+
async write(filePath, content) {
|
|
755
|
+
try {
|
|
756
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
757
|
+
const isBinary = !isTextMimeType(getMimeType(filePath));
|
|
758
|
+
try {
|
|
759
|
+
if ((await fs$1.lstat(resolvedPath)).isSymbolicLink()) return { error: `Cannot write to ${filePath} because it is a symlink. Symlinks are not allowed.` };
|
|
760
|
+
return { error: `Cannot write to ${filePath} because it already exists. Read and then make an edit, or write to a new path.` };
|
|
761
|
+
} catch {}
|
|
762
|
+
await fs$1.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
763
|
+
if (SUPPORTS_NOFOLLOW) {
|
|
764
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW;
|
|
765
|
+
const fd = await fs$1.open(resolvedPath, flags, 420);
|
|
766
|
+
try {
|
|
767
|
+
if (isBinary) {
|
|
768
|
+
const buffer = Buffer.from(content, "base64");
|
|
769
|
+
await fd.writeFile(buffer);
|
|
770
|
+
} else await fd.writeFile(content, "utf-8");
|
|
771
|
+
} finally {
|
|
772
|
+
await fd.close();
|
|
773
|
+
}
|
|
774
|
+
} else if (isBinary) {
|
|
775
|
+
const buffer = Buffer.from(content, "base64");
|
|
776
|
+
await fs$1.writeFile(resolvedPath, buffer);
|
|
777
|
+
} else await fs$1.writeFile(resolvedPath, content, "utf-8");
|
|
778
|
+
return {
|
|
779
|
+
path: filePath,
|
|
780
|
+
filesUpdate: null
|
|
781
|
+
};
|
|
782
|
+
} catch (e) {
|
|
783
|
+
return { error: `Error writing file '${filePath}': ${e.message}` };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Edit a file by replacing string occurrences.
|
|
788
|
+
* Returns EditResult. External storage sets filesUpdate=null.
|
|
789
|
+
*/
|
|
790
|
+
async edit(filePath, oldString, newString, replaceAll = false) {
|
|
791
|
+
try {
|
|
792
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
793
|
+
let content;
|
|
794
|
+
if (SUPPORTS_NOFOLLOW) {
|
|
795
|
+
if (!(await fs$1.stat(resolvedPath)).isFile()) return { error: `Error: File '${filePath}' not found` };
|
|
796
|
+
const fd = await fs$1.open(resolvedPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
797
|
+
try {
|
|
798
|
+
content = await fd.readFile({ encoding: "utf-8" });
|
|
799
|
+
} finally {
|
|
800
|
+
await fd.close();
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
const stat = await fs$1.lstat(resolvedPath);
|
|
804
|
+
if (stat.isSymbolicLink()) return { error: `Error: Symlinks are not allowed: ${filePath}` };
|
|
805
|
+
if (!stat.isFile()) return { error: `Error: File '${filePath}' not found` };
|
|
806
|
+
content = await fs$1.readFile(resolvedPath, "utf-8");
|
|
807
|
+
}
|
|
808
|
+
const result = performStringReplacement(content, oldString, newString, replaceAll);
|
|
809
|
+
if (typeof result === "string") return { error: result };
|
|
810
|
+
const [newContent, occurrences] = result;
|
|
811
|
+
if (SUPPORTS_NOFOLLOW) {
|
|
812
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW;
|
|
813
|
+
const fd = await fs$1.open(resolvedPath, flags);
|
|
814
|
+
try {
|
|
815
|
+
await fd.writeFile(newContent, "utf-8");
|
|
816
|
+
} finally {
|
|
817
|
+
await fd.close();
|
|
818
|
+
}
|
|
819
|
+
} else await fs$1.writeFile(resolvedPath, newContent, "utf-8");
|
|
820
|
+
return {
|
|
821
|
+
path: filePath,
|
|
822
|
+
filesUpdate: null,
|
|
823
|
+
occurrences
|
|
824
|
+
};
|
|
825
|
+
} catch (e) {
|
|
826
|
+
return { error: `Error editing file '${filePath}': ${e.message}` };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Search for a literal text pattern in files.
|
|
831
|
+
*
|
|
832
|
+
* Uses ripgrep if available, falling back to substring search.
|
|
833
|
+
*
|
|
834
|
+
* @param pattern - Literal string to search for (NOT regex).
|
|
835
|
+
* @param dirPath - Directory or file path to search in. Defaults to current directory.
|
|
836
|
+
* @param glob - Optional glob pattern to filter which files to search.
|
|
837
|
+
* @returns List of GrepMatch dicts containing path, line number, and matched text.
|
|
838
|
+
*/
|
|
839
|
+
async grep(pattern, dirPath = "/", glob = null) {
|
|
840
|
+
let baseFull;
|
|
841
|
+
try {
|
|
842
|
+
baseFull = this.resolvePath(dirPath || ".");
|
|
843
|
+
} catch {
|
|
844
|
+
return { matches: [] };
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
await fs$1.stat(baseFull);
|
|
848
|
+
} catch {
|
|
849
|
+
return { matches: [] };
|
|
850
|
+
}
|
|
851
|
+
let results = await this.ripgrepSearch(pattern, baseFull, glob);
|
|
852
|
+
if (results === null) results = await this.literalSearch(pattern, baseFull, glob);
|
|
853
|
+
const matches = [];
|
|
854
|
+
for (const [fpath, items] of Object.entries(results)) for (const [lineNum, lineText] of items) matches.push({
|
|
855
|
+
path: fpath,
|
|
856
|
+
line: lineNum,
|
|
857
|
+
text: lineText
|
|
858
|
+
});
|
|
859
|
+
return { matches };
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Search using ripgrep with fixed-string (literal) mode.
|
|
863
|
+
*
|
|
864
|
+
* @param pattern - Literal string to search for (unescaped).
|
|
865
|
+
* @param baseFull - Resolved base path to search in.
|
|
866
|
+
* @param includeGlob - Optional glob pattern to filter files.
|
|
867
|
+
* @returns Dict mapping file paths to list of (line_number, line_text) tuples.
|
|
868
|
+
* Returns null if ripgrep is unavailable or times out.
|
|
869
|
+
*/
|
|
870
|
+
async ripgrepSearch(pattern, baseFull, includeGlob) {
|
|
871
|
+
return new Promise((resolve) => {
|
|
872
|
+
const args = ["--json", "-F"];
|
|
873
|
+
if (includeGlob) args.push("--glob", includeGlob);
|
|
874
|
+
args.push("--", pattern, baseFull);
|
|
875
|
+
const proc = spawn("rg", args, { timeout: 3e4 });
|
|
876
|
+
const results = {};
|
|
877
|
+
let output = "";
|
|
878
|
+
proc.stdout.on("data", (data) => {
|
|
879
|
+
output += data.toString();
|
|
880
|
+
});
|
|
881
|
+
proc.on("close", (code) => {
|
|
882
|
+
if (code !== 0 && code !== 1) {
|
|
883
|
+
resolve(null);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
for (const line of output.split("\n")) {
|
|
887
|
+
if (!line.trim()) continue;
|
|
888
|
+
try {
|
|
889
|
+
const data = JSON.parse(line);
|
|
890
|
+
if (data.type !== "match") continue;
|
|
891
|
+
const pdata = data.data || {};
|
|
892
|
+
const ftext = pdata.path?.text;
|
|
893
|
+
if (!ftext) continue;
|
|
894
|
+
let virtPath;
|
|
895
|
+
if (this.virtualMode) try {
|
|
896
|
+
const resolved = path.resolve(ftext);
|
|
897
|
+
const relative = path.relative(this.cwd, resolved);
|
|
898
|
+
if (relative.startsWith("..")) continue;
|
|
899
|
+
virtPath = "/" + relative.split(path.sep).join("/");
|
|
900
|
+
} catch {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
else virtPath = ftext;
|
|
904
|
+
const ln = pdata.line_number;
|
|
905
|
+
const lt = pdata.lines?.text?.replace(/\n$/, "") || "";
|
|
906
|
+
if (ln === void 0) continue;
|
|
907
|
+
if (!results[virtPath]) results[virtPath] = [];
|
|
908
|
+
results[virtPath].push([ln, lt]);
|
|
909
|
+
} catch {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
resolve(results);
|
|
914
|
+
});
|
|
915
|
+
proc.on("error", () => {
|
|
916
|
+
resolve(null);
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Fallback search using literal substring matching when ripgrep is unavailable.
|
|
922
|
+
*
|
|
923
|
+
* Recursively searches files, respecting maxFileSizeBytes limit.
|
|
924
|
+
*
|
|
925
|
+
* @param pattern - Literal string to search for.
|
|
926
|
+
* @param baseFull - Resolved base path to search in.
|
|
927
|
+
* @param includeGlob - Optional glob pattern to filter files by name.
|
|
928
|
+
* @returns Dict mapping file paths to list of (line_number, line_text) tuples.
|
|
929
|
+
*/
|
|
930
|
+
async literalSearch(pattern, baseFull, includeGlob) {
|
|
931
|
+
const results = {};
|
|
932
|
+
const files = await fg("**/*", {
|
|
933
|
+
cwd: (await fs$1.stat(baseFull)).isDirectory() ? baseFull : path.dirname(baseFull),
|
|
934
|
+
absolute: true,
|
|
935
|
+
onlyFiles: true,
|
|
936
|
+
dot: true
|
|
937
|
+
});
|
|
938
|
+
for (const fp of files) try {
|
|
939
|
+
if (!isTextMimeType(getMimeType(fp))) continue;
|
|
940
|
+
if (includeGlob && !micromatch.isMatch(path.basename(fp), includeGlob)) continue;
|
|
941
|
+
if ((await fs$1.stat(fp)).size > this.maxFileSizeBytes) continue;
|
|
942
|
+
const lines = (await fs$1.readFile(fp, "utf-8")).split("\n");
|
|
943
|
+
for (let i = 0; i < lines.length; i++) {
|
|
944
|
+
const line = lines[i];
|
|
945
|
+
if (line.includes(pattern)) {
|
|
946
|
+
let virtPath;
|
|
947
|
+
if (this.virtualMode) try {
|
|
948
|
+
const relative = path.relative(this.cwd, fp);
|
|
949
|
+
if (relative.startsWith("..")) continue;
|
|
950
|
+
virtPath = "/" + relative.split(path.sep).join("/");
|
|
951
|
+
} catch {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
else virtPath = fp;
|
|
955
|
+
if (!results[virtPath]) results[virtPath] = [];
|
|
956
|
+
results[virtPath].push([i + 1, line]);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
} catch {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
return results;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Structured glob matching returning FileInfo objects.
|
|
966
|
+
*/
|
|
967
|
+
async glob(pattern, searchPath = "/") {
|
|
968
|
+
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
969
|
+
const resolvedSearchPath = searchPath === "/" ? this.cwd : this.resolvePath(searchPath);
|
|
970
|
+
try {
|
|
971
|
+
if (!(await fs$1.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
972
|
+
} catch {
|
|
973
|
+
return { files: [] };
|
|
974
|
+
}
|
|
975
|
+
const results = [];
|
|
976
|
+
try {
|
|
977
|
+
const matches = await fg(pattern, {
|
|
978
|
+
cwd: resolvedSearchPath,
|
|
979
|
+
absolute: true,
|
|
980
|
+
onlyFiles: true,
|
|
981
|
+
dot: true
|
|
982
|
+
});
|
|
983
|
+
for (const matchedPath of matches) try {
|
|
984
|
+
const stat = await fs$1.stat(matchedPath);
|
|
985
|
+
if (!stat.isFile()) continue;
|
|
986
|
+
const normalizedPath = matchedPath.split("/").join(path.sep);
|
|
987
|
+
if (!this.virtualMode) results.push({
|
|
988
|
+
path: normalizedPath,
|
|
989
|
+
is_dir: false,
|
|
990
|
+
size: stat.size,
|
|
991
|
+
modified_at: stat.mtime.toISOString()
|
|
992
|
+
});
|
|
993
|
+
else {
|
|
994
|
+
const cwdStr = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
995
|
+
let relativePath;
|
|
996
|
+
if (normalizedPath.startsWith(cwdStr)) relativePath = normalizedPath.substring(cwdStr.length);
|
|
997
|
+
else if (normalizedPath.startsWith(this.cwd)) relativePath = normalizedPath.substring(this.cwd.length).replace(/^[/\\]/, "");
|
|
998
|
+
else relativePath = normalizedPath;
|
|
999
|
+
relativePath = relativePath.split(path.sep).join("/");
|
|
1000
|
+
const virt = "/" + relativePath;
|
|
1001
|
+
results.push({
|
|
1002
|
+
path: virt,
|
|
1003
|
+
is_dir: false,
|
|
1004
|
+
size: stat.size,
|
|
1005
|
+
modified_at: stat.mtime.toISOString()
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
} catch {}
|
|
1012
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
1013
|
+
return { files: results };
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Upload multiple files to the filesystem.
|
|
1017
|
+
*
|
|
1018
|
+
* @param files - List of [path, content] tuples to upload
|
|
1019
|
+
* @returns List of FileUploadResponse objects, one per input file
|
|
1020
|
+
*/
|
|
1021
|
+
async uploadFiles(files) {
|
|
1022
|
+
const responses = [];
|
|
1023
|
+
for (const [filePath, content] of files) try {
|
|
1024
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
1025
|
+
await fs$1.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1026
|
+
await fs$1.writeFile(resolvedPath, content);
|
|
1027
|
+
responses.push({
|
|
1028
|
+
path: filePath,
|
|
1029
|
+
error: null
|
|
1030
|
+
});
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
if (e.code === "ENOENT") responses.push({
|
|
1033
|
+
path: filePath,
|
|
1034
|
+
error: "file_not_found"
|
|
1035
|
+
});
|
|
1036
|
+
else if (e.code === "EACCES") responses.push({
|
|
1037
|
+
path: filePath,
|
|
1038
|
+
error: "permission_denied"
|
|
1039
|
+
});
|
|
1040
|
+
else if (e.code === "EISDIR") responses.push({
|
|
1041
|
+
path: filePath,
|
|
1042
|
+
error: "is_directory"
|
|
1043
|
+
});
|
|
1044
|
+
else responses.push({
|
|
1045
|
+
path: filePath,
|
|
1046
|
+
error: "invalid_path"
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return responses;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Download multiple files from the filesystem.
|
|
1053
|
+
*
|
|
1054
|
+
* @param paths - List of file paths to download
|
|
1055
|
+
* @returns List of FileDownloadResponse objects, one per input path
|
|
1056
|
+
*/
|
|
1057
|
+
async downloadFiles(paths) {
|
|
1058
|
+
const responses = [];
|
|
1059
|
+
for (const filePath of paths) try {
|
|
1060
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
1061
|
+
const content = await fs$1.readFile(resolvedPath);
|
|
1062
|
+
responses.push({
|
|
1063
|
+
path: filePath,
|
|
1064
|
+
content,
|
|
1065
|
+
error: null
|
|
1066
|
+
});
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
if (e.code === "ENOENT") responses.push({
|
|
1069
|
+
path: filePath,
|
|
1070
|
+
content: null,
|
|
1071
|
+
error: "file_not_found"
|
|
1072
|
+
});
|
|
1073
|
+
else if (e.code === "EACCES") responses.push({
|
|
1074
|
+
path: filePath,
|
|
1075
|
+
content: null,
|
|
1076
|
+
error: "permission_denied"
|
|
1077
|
+
});
|
|
1078
|
+
else if (e.code === "EISDIR") responses.push({
|
|
1079
|
+
path: filePath,
|
|
1080
|
+
content: null,
|
|
1081
|
+
error: "is_directory"
|
|
1082
|
+
});
|
|
1083
|
+
else responses.push({
|
|
1084
|
+
path: filePath,
|
|
1085
|
+
content: null,
|
|
1086
|
+
error: "invalid_path"
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
return responses;
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/backends/local-shell.ts
|
|
1094
|
+
/**
|
|
1095
|
+
* LocalShellBackend: Node.js implementation of the filesystem backend with unrestricted local shell execution.
|
|
1096
|
+
*
|
|
1097
|
+
* This backend extends FilesystemBackend to add shell command execution on the local
|
|
1098
|
+
* host system. It provides NO sandboxing or isolation - all operations run directly
|
|
1099
|
+
* on the host machine with full system access.
|
|
1100
|
+
*
|
|
1101
|
+
* @module
|
|
1102
|
+
*/
|
|
1103
|
+
/**
|
|
1104
|
+
* Filesystem backend with unrestricted local shell command execution.
|
|
1105
|
+
*
|
|
1106
|
+
* This backend extends FilesystemBackend to add shell command execution
|
|
1107
|
+
* capabilities. Commands are executed directly on the host system without any
|
|
1108
|
+
* sandboxing, process isolation, or security restrictions.
|
|
1109
|
+
*
|
|
1110
|
+
* **Security Warning:**
|
|
1111
|
+
* This backend grants agents BOTH direct filesystem access AND unrestricted
|
|
1112
|
+
* shell execution on your local machine. Use with extreme caution and only in
|
|
1113
|
+
* appropriate environments.
|
|
1114
|
+
*
|
|
1115
|
+
* **Appropriate use cases:**
|
|
1116
|
+
* - Local development CLIs (coding assistants, development tools)
|
|
1117
|
+
* - Personal development environments where you trust the agent's code
|
|
1118
|
+
* - CI/CD pipelines with proper secret management
|
|
1119
|
+
*
|
|
1120
|
+
* **Inappropriate use cases:**
|
|
1121
|
+
* - Production environments (e.g., web servers, APIs, multi-tenant systems)
|
|
1122
|
+
* - Processing untrusted user input or executing untrusted code
|
|
1123
|
+
*
|
|
1124
|
+
* Use StateBackend, StoreBackend, or extend BaseSandbox for production.
|
|
1125
|
+
*
|
|
1126
|
+
* @example
|
|
1127
|
+
* ```typescript
|
|
1128
|
+
* import { LocalShellBackend } from "@langchain/deepagents";
|
|
1129
|
+
*
|
|
1130
|
+
* // Create backend with explicit environment
|
|
1131
|
+
* const backend = new LocalShellBackend({
|
|
1132
|
+
* rootDir: "/home/user/project",
|
|
1133
|
+
* virtualMode: true,
|
|
1134
|
+
* env: { PATH: "/usr/bin:/bin" },
|
|
1135
|
+
* });
|
|
1136
|
+
*
|
|
1137
|
+
* // Execute shell commands (runs directly on host)
|
|
1138
|
+
* const result = await backend.execute("ls -la");
|
|
1139
|
+
* console.log(result.output);
|
|
1140
|
+
* console.log(result.exitCode);
|
|
1141
|
+
*
|
|
1142
|
+
* // Use filesystem operations (inherited from FilesystemBackend)
|
|
1143
|
+
* const content = await backend.read("/README.md");
|
|
1144
|
+
* await backend.write("/output.txt", "Hello world");
|
|
1145
|
+
*
|
|
1146
|
+
* // Inherit all environment variables
|
|
1147
|
+
* const backend2 = new LocalShellBackend({
|
|
1148
|
+
* rootDir: "/home/user/project",
|
|
1149
|
+
* virtualMode: true,
|
|
1150
|
+
* inheritEnv: true,
|
|
1151
|
+
* });
|
|
1152
|
+
* ```
|
|
1153
|
+
*/
|
|
1154
|
+
var LocalShellBackend = class LocalShellBackend extends FilesystemBackend {
|
|
1155
|
+
#timeout;
|
|
1156
|
+
#maxOutputBytes;
|
|
1157
|
+
#env;
|
|
1158
|
+
#sandboxId;
|
|
1159
|
+
#initialized = false;
|
|
1160
|
+
constructor(options = {}) {
|
|
1161
|
+
const { rootDir, virtualMode = false, timeout = 120, maxOutputBytes = 1e5, env, inheritEnv = false } = options;
|
|
1162
|
+
super({
|
|
1163
|
+
rootDir,
|
|
1164
|
+
virtualMode,
|
|
1165
|
+
maxFileSizeMb: 10
|
|
1166
|
+
});
|
|
1167
|
+
this.#timeout = timeout;
|
|
1168
|
+
this.#maxOutputBytes = maxOutputBytes;
|
|
1169
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
1170
|
+
crypto.getRandomValues(bytes);
|
|
1171
|
+
this.#sandboxId = `local-${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1172
|
+
if (inheritEnv) {
|
|
1173
|
+
this.#env = { ...process.env };
|
|
1174
|
+
if (env) Object.assign(this.#env, env);
|
|
1175
|
+
} else this.#env = env ?? {};
|
|
1176
|
+
}
|
|
1177
|
+
/** Unique identifier for this backend instance (format: "local-{random_hex}"). */
|
|
1178
|
+
get id() {
|
|
1179
|
+
return this.#sandboxId;
|
|
1180
|
+
}
|
|
1181
|
+
/** Whether the backend has been initialized and is ready to use. */
|
|
1182
|
+
get isInitialized() {
|
|
1183
|
+
return this.#initialized;
|
|
1184
|
+
}
|
|
1185
|
+
/** Alias for `isInitialized`, matching the standard sandbox interface. */
|
|
1186
|
+
get isRunning() {
|
|
1187
|
+
return this.#initialized;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Initialize the backend by ensuring the rootDir exists.
|
|
1191
|
+
*
|
|
1192
|
+
* Creates the rootDir (and any parent directories) if it does not already
|
|
1193
|
+
* exist. Safe to call on an existing directory. Must be called before
|
|
1194
|
+
* `execute()`, or use the static `LocalShellBackend.create()` factory.
|
|
1195
|
+
*
|
|
1196
|
+
* @throws {SandboxError} If already initialized (`ALREADY_INITIALIZED`)
|
|
1197
|
+
*/
|
|
1198
|
+
async initialize() {
|
|
1199
|
+
if (this.#initialized) throw new SandboxError("Backend is already initialized. Each LocalShellBackend instance can only be initialized once.", "ALREADY_INITIALIZED");
|
|
1200
|
+
await fs$1.mkdir(this.cwd, { recursive: true });
|
|
1201
|
+
this.#initialized = true;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Mark the backend as no longer running.
|
|
1205
|
+
*
|
|
1206
|
+
* For local shell backends there is no remote resource to tear down,
|
|
1207
|
+
* so this simply flips the `isRunning` / `isInitialized` flag.
|
|
1208
|
+
*/
|
|
1209
|
+
async close() {
|
|
1210
|
+
this.#initialized = false;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Read a file, adapting error messages to the standard sandbox format.
|
|
1214
|
+
*/
|
|
1215
|
+
async read(filePath, offset = 0, limit = 500) {
|
|
1216
|
+
const result = await super.read(filePath, offset, limit);
|
|
1217
|
+
if (result.error?.includes("ENOENT")) return { error: `File '${filePath}' not found` };
|
|
1218
|
+
return result;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Edit a file, adapting error messages to the standard sandbox format.
|
|
1222
|
+
*/
|
|
1223
|
+
async edit(filePath, oldString, newString, replaceAll = false) {
|
|
1224
|
+
const result = await super.edit(filePath, oldString, newString, replaceAll);
|
|
1225
|
+
if (result.error?.includes("ENOENT")) return {
|
|
1226
|
+
...result,
|
|
1227
|
+
error: `Error: File '${filePath}' not found`
|
|
1228
|
+
};
|
|
1229
|
+
return result;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* List directory contents, returning paths relative to rootDir.
|
|
1233
|
+
*/
|
|
1234
|
+
async ls(dirPath) {
|
|
1235
|
+
const result = await super.ls(dirPath);
|
|
1236
|
+
if (result.error) return result;
|
|
1237
|
+
if (this.virtualMode) return result;
|
|
1238
|
+
const cwdPrefix = this.cwd.endsWith(path.sep) ? this.cwd : this.cwd + path.sep;
|
|
1239
|
+
return { files: (result.files || []).map((info) => ({
|
|
1240
|
+
...info,
|
|
1241
|
+
path: info.path.startsWith(cwdPrefix) ? info.path.slice(cwdPrefix.length) : info.path
|
|
1242
|
+
})) };
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Glob matching that returns relative paths and includes directories.
|
|
1246
|
+
*/
|
|
1247
|
+
async glob(pattern, searchPath = "/") {
|
|
1248
|
+
if (pattern.startsWith("/")) pattern = pattern.substring(1);
|
|
1249
|
+
const resolvedSearchPath = searchPath === "/" || searchPath === "" ? this.cwd : this.virtualMode ? path.resolve(this.cwd, searchPath.replace(/^\//, "")) : path.resolve(this.cwd, searchPath);
|
|
1250
|
+
try {
|
|
1251
|
+
if (!(await fs$1.stat(resolvedSearchPath)).isDirectory()) return { files: [] };
|
|
1252
|
+
} catch {
|
|
1253
|
+
return { files: [] };
|
|
1254
|
+
}
|
|
1255
|
+
const formatPath = (rel) => this.virtualMode ? `/${rel}` : rel;
|
|
1256
|
+
const globOpts = {
|
|
1257
|
+
cwd: resolvedSearchPath,
|
|
1258
|
+
absolute: false,
|
|
1259
|
+
dot: true
|
|
1260
|
+
};
|
|
1261
|
+
const [fileMatches, dirMatches] = await Promise.all([fg(pattern, {
|
|
1262
|
+
...globOpts,
|
|
1263
|
+
onlyFiles: true
|
|
1264
|
+
}), fg(pattern, {
|
|
1265
|
+
...globOpts,
|
|
1266
|
+
onlyDirectories: true
|
|
1267
|
+
})]);
|
|
1268
|
+
const statFile = async (match) => {
|
|
1269
|
+
try {
|
|
1270
|
+
const entryStat = await fs$1.stat(path.join(resolvedSearchPath, match));
|
|
1271
|
+
if (entryStat.isFile()) return {
|
|
1272
|
+
path: formatPath(match),
|
|
1273
|
+
is_dir: false,
|
|
1274
|
+
size: entryStat.size,
|
|
1275
|
+
modified_at: entryStat.mtime.toISOString()
|
|
1276
|
+
};
|
|
1277
|
+
} catch {}
|
|
1278
|
+
return null;
|
|
1279
|
+
};
|
|
1280
|
+
const statDir = async (match) => {
|
|
1281
|
+
try {
|
|
1282
|
+
const entryStat = await fs$1.stat(path.join(resolvedSearchPath, match));
|
|
1283
|
+
if (entryStat.isDirectory()) return {
|
|
1284
|
+
path: formatPath(match),
|
|
1285
|
+
is_dir: true,
|
|
1286
|
+
size: 0,
|
|
1287
|
+
modified_at: entryStat.mtime.toISOString()
|
|
1288
|
+
};
|
|
1289
|
+
} catch {}
|
|
1290
|
+
return null;
|
|
1291
|
+
};
|
|
1292
|
+
const [fileInfos, dirInfos] = await Promise.all([Promise.all(fileMatches.map(statFile)), Promise.all(dirMatches.map(statDir))]);
|
|
1293
|
+
const results = [...fileInfos, ...dirInfos].filter((info) => info !== null);
|
|
1294
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
1295
|
+
return { files: results };
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Execute a shell command directly on the host system.
|
|
1299
|
+
*
|
|
1300
|
+
* Commands are executed directly on your host system using `spawn()`
|
|
1301
|
+
* with `shell: true`. There is NO sandboxing, isolation, or security
|
|
1302
|
+
* restrictions. The command runs with your user's full permissions.
|
|
1303
|
+
*
|
|
1304
|
+
* The command is executed using the system shell with the working directory
|
|
1305
|
+
* set to the backend's rootDir. Stdout and stderr are combined into a single
|
|
1306
|
+
* output stream, with stderr lines prefixed with `[stderr]`.
|
|
1307
|
+
*
|
|
1308
|
+
* @param command - Shell command string to execute
|
|
1309
|
+
* @returns ExecuteResponse containing output, exit code, and truncation flag
|
|
1310
|
+
*/
|
|
1311
|
+
async execute(command) {
|
|
1312
|
+
if (!command || typeof command !== "string") return {
|
|
1313
|
+
output: "Error: Command must be a non-empty string.",
|
|
1314
|
+
exitCode: 1,
|
|
1315
|
+
truncated: false
|
|
1316
|
+
};
|
|
1317
|
+
return new Promise((resolve) => {
|
|
1318
|
+
let stdout = "";
|
|
1319
|
+
let stderr = "";
|
|
1320
|
+
let timedOut = false;
|
|
1321
|
+
const child = cp.spawn(command, {
|
|
1322
|
+
shell: true,
|
|
1323
|
+
env: this.#env,
|
|
1324
|
+
cwd: this.cwd
|
|
1325
|
+
});
|
|
1326
|
+
const timer = setTimeout(() => {
|
|
1327
|
+
timedOut = true;
|
|
1328
|
+
child.kill("SIGTERM");
|
|
1329
|
+
}, this.#timeout * 1e3);
|
|
1330
|
+
child.stdout.on("data", (data) => {
|
|
1331
|
+
stdout += data.toString();
|
|
1332
|
+
});
|
|
1333
|
+
child.stderr.on("data", (data) => {
|
|
1334
|
+
stderr += data.toString();
|
|
1335
|
+
});
|
|
1336
|
+
child.on("error", (err) => {
|
|
1337
|
+
clearTimeout(timer);
|
|
1338
|
+
resolve({
|
|
1339
|
+
output: `Error executing command: ${err.message}`,
|
|
1340
|
+
exitCode: 1,
|
|
1341
|
+
truncated: false
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
child.on("close", (code, signal) => {
|
|
1345
|
+
clearTimeout(timer);
|
|
1346
|
+
if (timedOut || signal === "SIGTERM") {
|
|
1347
|
+
resolve({
|
|
1348
|
+
output: `Error: Command timed out after ${this.#timeout.toFixed(1)} seconds.`,
|
|
1349
|
+
exitCode: 124,
|
|
1350
|
+
truncated: false
|
|
1351
|
+
});
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const outputParts = [];
|
|
1355
|
+
if (stdout) outputParts.push(stdout);
|
|
1356
|
+
if (stderr) {
|
|
1357
|
+
const stderrLines = stderr.trim().split("\n");
|
|
1358
|
+
outputParts.push(...stderrLines.map((line) => `[stderr] ${line}`));
|
|
1359
|
+
}
|
|
1360
|
+
let output = outputParts.length > 0 ? outputParts.join("\n") : "<no output>";
|
|
1361
|
+
let truncated = false;
|
|
1362
|
+
if (output.length > this.#maxOutputBytes) {
|
|
1363
|
+
output = output.slice(0, this.#maxOutputBytes);
|
|
1364
|
+
output += `\n\n... Output truncated at ${this.#maxOutputBytes} bytes.`;
|
|
1365
|
+
truncated = true;
|
|
1366
|
+
}
|
|
1367
|
+
const exitCode = code ?? 1;
|
|
1368
|
+
if (exitCode !== 0) output = `${output.trimEnd()}\n\nExit code: ${exitCode}`;
|
|
1369
|
+
resolve({
|
|
1370
|
+
output,
|
|
1371
|
+
exitCode,
|
|
1372
|
+
truncated
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Create and initialize a new LocalShellBackend in one step.
|
|
1379
|
+
*
|
|
1380
|
+
* This is the recommended way to create a backend when the rootDir may
|
|
1381
|
+
* not exist yet. It combines construction and initialization (ensuring
|
|
1382
|
+
* rootDir exists) into a single async operation.
|
|
1383
|
+
*
|
|
1384
|
+
* @param options - Configuration options for the backend
|
|
1385
|
+
* @returns An initialized and ready-to-use backend
|
|
1386
|
+
*/
|
|
1387
|
+
static async create(options = {}) {
|
|
1388
|
+
const { initialFiles, ...backendOptions } = options;
|
|
1389
|
+
const backend = new LocalShellBackend(backendOptions);
|
|
1390
|
+
await backend.initialize();
|
|
1391
|
+
if (initialFiles) {
|
|
1392
|
+
const encoder = new TextEncoder();
|
|
1393
|
+
const files = Object.entries(initialFiles).map(([filePath, content]) => [filePath, encoder.encode(content)]);
|
|
1394
|
+
await backend.uploadFiles(files);
|
|
1395
|
+
}
|
|
1396
|
+
return backend;
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
//#endregion
|
|
1400
|
+
export { createAgentMemoryMiddleware as a, parseSkillMetadata as i, FilesystemBackend as n, createSettings as o, listSkills as r, findProjectRoot as s, LocalShellBackend as t };
|
|
1401
|
+
|
|
1402
|
+
//# sourceMappingURL=src-DybKvM4T.js.map
|