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.
@@ -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