UNPKG

n8n

Version:

n8n Workflow Automation Tool

221 lines • 9.75 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentKnowledgeCommandService = exports.AGENT_KNOWLEDGE_COMMANDS = void 0; const di_1 = require("@n8n/di"); const node_child_process_1 = require("node:child_process"); const promises_1 = require("node:fs/promises"); const node_os_1 = require("node:os"); const node_path_1 = __importDefault(require("node:path")); const p_limit_1 = __importDefault(require("p-limit")); const MAX_OUTPUT_BYTES = 64 * 1024; const COMMAND_TIMEOUT_MS = 5_000; const MAX_CONCURRENT_WORKSPACES = 4; const WORKSPACE_CACHE_TTL_MS = 10 * 60_000; const MAX_CACHED_WORKSPACES = 25; exports.AGENT_KNOWLEDGE_COMMANDS = ['git_grep', 'cat', 'sed']; const workspaceLimit = (0, p_limit_1.default)(MAX_CONCURRENT_WORKSPACES); let AgentKnowledgeCommandService = class AgentKnowledgeCommandService { constructor() { this.cachedWorkspaces = new Map(); this.workspaceLocks = new Map(); } async run(workspaceRoot, request) { const root = await (0, promises_1.realpath)(workspaceRoot); const { executable, args } = await this.toSpawnArgs(root, request); return await this.spawnCommand(root, executable, args, request.command); } async withCachedWorkspace(cacheKey, materialize, operation) { return await this.serializeByKey(cacheKey, async () => await workspaceLimit(async () => { const workspaceRoot = await this.ensureCachedWorkspace(cacheKey, materialize); return await operation(workspaceRoot); })); } async serializeByKey(key, fn) { const previous = this.workspaceLocks.get(key) ?? Promise.resolve(); const run = previous.then(fn, fn); const tail = run.then(() => undefined, () => undefined); this.workspaceLocks.set(key, tail); try { return await run; } finally { if (this.workspaceLocks.get(key) === tail) this.workspaceLocks.delete(key); } } async ensureCachedWorkspace(cacheKey, materialize) { const existing = this.cachedWorkspaces.get(cacheKey); if (existing && (await this.directoryExists(existing.root))) { existing.lastUsedAt = Date.now(); return existing.root; } if (existing) this.cachedWorkspaces.delete(cacheKey); const workspaceRoot = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), 'n8n-agent-knowledge-')); try { await materialize(workspaceRoot); } catch (error) { await (0, promises_1.rm)(workspaceRoot, { recursive: true, force: true }).catch(() => { }); throw error; } this.cachedWorkspaces.set(cacheKey, { root: workspaceRoot, lastUsedAt: Date.now() }); await this.evictStaleWorkspaces(); return workspaceRoot; } async evictStaleWorkspaces() { const now = Date.now(); const evictable = []; const fresh = []; for (const entry of this.cachedWorkspaces) { (now - entry[1].lastUsedAt > WORKSPACE_CACHE_TTL_MS ? evictable : fresh).push(entry); } if (fresh.length > MAX_CACHED_WORKSPACES) { fresh.sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt); evictable.push(...fresh.slice(0, fresh.length - MAX_CACHED_WORKSPACES)); } for (const [key, workspace] of evictable) { this.cachedWorkspaces.delete(key); await (0, promises_1.rm)(workspace.root, { recursive: true, force: true }).catch(() => { }); } } async directoryExists(directory) { try { await (0, promises_1.realpath)(directory); return true; } catch { return false; } } async toSpawnArgs(root, request) { switch (request.command) { case 'git_grep': { if (request.pattern.trim() === '') throw new Error('Search pattern is required'); const args = ['grep', '--no-index', '-n', '-I']; if (request.caseInsensitive) args.push('-i'); if (request.fixedStrings) args.push('-F'); if (request.fixedStrings === false) args.push('-E'); if (request.outputMode === 'count') args.push('-c'); if (request.context !== undefined) { args.push('-C', String(Math.min(Math.max(request.context, 0), 5))); } args.push('--', request.pattern); const files = await Promise.all((request.files ?? ['.']).map(async (file) => await this.safePath(root, file, { allowRoot: true }))); args.push(...files.map((file) => node_path_1.default.relative(root, file) || '.')); return { executable: 'git', args }; } case 'cat': { const file = await this.safePath(root, request.file); return { executable: 'cat', args: [node_path_1.default.relative(root, file)] }; } case 'sed': { const file = await this.safePath(root, request.file); const startLine = Math.max(1, request.startLine); const endLine = Math.max(startLine, request.endLine); return { executable: 'sed', args: [ '-n', `${startLine},${Math.min(endLine, startLine + 500)}p`, node_path_1.default.relative(root, file), ], }; } } } async safePath(root, requestedPath, options = {}) { if (this.hasControlCharacters(requestedPath)) throw new Error('Invalid path'); if (node_path_1.default.isAbsolute(requestedPath)) throw new Error('Absolute paths are not allowed'); if (requestedPath.split(/[\\/]/).includes('..')) { throw new Error('Parent path segments are not allowed'); } const resolved = node_path_1.default.resolve(root, requestedPath); const actual = await (0, promises_1.realpath)(resolved); const relative = node_path_1.default.relative(root, actual); if ((!options.allowRoot && relative === '') || relative.startsWith('..') || node_path_1.default.isAbsolute(relative)) { throw new Error('Path escapes the knowledge workspace'); } return actual; } hasControlCharacters(value) { for (const character of value) { const code = character.charCodeAt(0); if (code <= 0x1f || code === 0x7f) return true; } return false; } async spawnCommand(cwd, executable, args, command) { return await new Promise((resolve, reject) => { const child = (0, node_child_process_1.spawn)(executable, args, { cwd, shell: false, env: { PATH: process.env.PATH, HOME: cwd, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_TERMINAL_PROMPT: '0', }, }); let stdout = ''; let stderr = ''; let truncated = false; const timer = setTimeout(() => { child.kill('SIGKILL'); truncated = true; }, COMMAND_TIMEOUT_MS); const append = (current, chunk) => { const next = Buffer.concat([Buffer.from(current, 'utf8'), chunk]); if (next.length > MAX_OUTPUT_BYTES) { truncated = true; return truncateBufferToUtf8String(next, MAX_OUTPUT_BYTES); } return next.toString('utf8'); }; child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk); }); child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk); }); child.on('error', reject); child.on('close', (exitCode) => { clearTimeout(timer); resolve({ command, exitCode, stdout, stderr, truncated }); }); }); } }; exports.AgentKnowledgeCommandService = AgentKnowledgeCommandService; exports.AgentKnowledgeCommandService = AgentKnowledgeCommandService = __decorate([ (0, di_1.Service)() ], AgentKnowledgeCommandService); function truncateBufferToUtf8String(buffer, maxBytes) { for (let end = maxBytes; end >= 0; end--) { const output = buffer.subarray(0, end).toString('utf8'); if (Buffer.byteLength(output) <= maxBytes) return output; } return ''; } //# sourceMappingURL=agent-knowledge-command.service.js.map