n8n
Version:
n8n Workflow Automation Tool
221 lines • 9.75 kB
JavaScript
;
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