natureco-cli 5.63.0 → 5.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/natureco.js +10 -3
  3. package/package.json +2 -1
  4. package/scripts/benchmark-startup.js +26 -0
  5. package/src/commands/backup.js +3 -3
  6. package/src/commands/code.js +49 -14
  7. package/src/commands/code_v5.js +15 -1
  8. package/src/commands/gateway-server.js +128 -24
  9. package/src/commands/git.js +2 -2
  10. package/src/commands/pairing.js +2 -22
  11. package/src/commands/repl.js +8 -6
  12. package/src/tools/agentic-runner.js +41 -33
  13. package/src/tools/memory_write.js +16 -12
  14. package/src/tools/structural_patch.js +25 -0
  15. package/src/tools/workflow.js +10 -2
  16. package/src/utils/agent-core.js +51 -0
  17. package/src/utils/agent-workspace.js +24 -0
  18. package/src/utils/api.js +12 -8
  19. package/src/utils/channel-sdk.js +212 -0
  20. package/src/utils/code-intelligence.js +69 -0
  21. package/src/utils/coding-session.js +54 -0
  22. package/src/utils/delivery-store.js +34 -0
  23. package/src/utils/i18n.js +7 -1
  24. package/src/utils/json-schema.js +43 -0
  25. package/src/utils/lsp-client.js +129 -0
  26. package/src/utils/memory-record.js +49 -0
  27. package/src/utils/pairing-store.js +55 -0
  28. package/src/utils/pattern-detector.js +13 -3
  29. package/src/utils/plugin-registry.js +3 -3
  30. package/src/utils/process-errors.js +14 -7
  31. package/src/utils/runtime-health.js +28 -0
  32. package/src/utils/secret-store.js +90 -0
  33. package/src/utils/secure-sync.js +63 -0
  34. package/src/utils/skill-lifecycle.js +59 -0
  35. package/src/utils/structural-patch.js +68 -0
  36. package/src/utils/sub-agent.js +13 -2
  37. package/src/utils/test-failure-analyzer.js +64 -0
  38. package/src/utils/tool-execution-gateway.js +56 -0
  39. package/src/utils/tool-manifest.js +31 -0
  40. package/src/utils/tool-path-policy.js +49 -0
  41. package/src/utils/tool-result.js +17 -0
  42. package/src/utils/tool-runner.js +81 -53
  43. package/src/utils/tools.js +30 -42
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function deriveKey(syncKey, salt) { return crypto.scryptSync(String(syncKey), salt, 32); }
6
+
7
+ function encryptSyncPayload(payload, options = {}) {
8
+ if (!options.syncKey) throw new Error('syncKey is required');
9
+ if (!options.deviceId) throw new Error('deviceId is required');
10
+ const salt = crypto.randomBytes(16); const iv = crypto.randomBytes(12);
11
+ const key = deriveKey(options.syncKey, salt);
12
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
13
+ const header = { version: 1, deviceId: options.deviceId, revision: options.revision || 1, clock: options.clock || { [options.deviceId]: options.revision || 1 }, createdAt: new Date().toISOString() };
14
+ cipher.setAAD(Buffer.from(JSON.stringify(header)));
15
+ const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final()]);
16
+ return { ...header, algorithm: 'aes-256-gcm', salt: salt.toString('base64'), iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), ciphertext: encrypted.toString('base64') };
17
+ }
18
+
19
+ function decryptSyncPayload(envelope, options = {}) {
20
+ if (!options.syncKey) throw new Error('syncKey is required');
21
+ if (!envelope || envelope.version !== 1 || envelope.algorithm !== 'aes-256-gcm') throw new Error('unsupported sync envelope');
22
+ const header = { version: envelope.version, deviceId: envelope.deviceId, revision: envelope.revision, clock: envelope.clock, createdAt: envelope.createdAt };
23
+ try {
24
+ const key = deriveKey(options.syncKey, Buffer.from(envelope.salt, 'base64'));
25
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(envelope.iv, 'base64'));
26
+ decipher.setAAD(Buffer.from(JSON.stringify(header))); decipher.setAuthTag(Buffer.from(envelope.tag, 'base64'));
27
+ return { header, payload: JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, 'base64')), decipher.final()]).toString('utf8')) };
28
+ } catch { throw new Error('sync payload authentication failed'); }
29
+ }
30
+
31
+ function compareClocks(left = {}, right = {}) {
32
+ const devices = new Set([...Object.keys(left), ...Object.keys(right)]);
33
+ let leftAhead = false, rightAhead = false;
34
+ for (const device of devices) {
35
+ const l = left[device] || 0, r = right[device] || 0;
36
+ if (l > r) leftAhead = true; if (r > l) rightAhead = true;
37
+ }
38
+ if (leftAhead && rightAhead) return 'concurrent';
39
+ if (leftAhead) return 'after';
40
+ if (rightAhead) return 'before';
41
+ return 'equal';
42
+ }
43
+
44
+ function mergeSyncRecords(local = [], remote = []) {
45
+ const records = new Map(); const conflicts = [];
46
+ for (const item of [...local, ...remote]) {
47
+ if (!item?.id) continue;
48
+ const existing = records.get(item.id);
49
+ if (!existing) { records.set(item.id, item); continue; }
50
+ const relation = compareClocks(existing.clock, item.clock);
51
+ if (relation === 'before') records.set(item.id, item);
52
+ else if (relation === 'concurrent' && JSON.stringify(existing.value) !== JSON.stringify(item.value)) {
53
+ conflicts.push({ id: item.id, local: existing, remote: item });
54
+ const winner = existing.userConfirmed !== item.userConfirmed
55
+ ? (item.userConfirmed ? item : existing)
56
+ : (String(item.updatedAt || '') > String(existing.updatedAt || '') ? item : existing);
57
+ records.set(item.id, winner);
58
+ }
59
+ }
60
+ return { records: [...records.values()], conflicts };
61
+ }
62
+
63
+ module.exports = { encryptSyncPayload, decryptSyncPayload, compareClocks, mergeSyncRecords };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { writeFileAtomicSync, writeJsonAtomicSync, readJsonSafeSync } = require('./atomic-file');
7
+
8
+ function validSkill(content) {
9
+ return typeof content === 'string' && content.startsWith('---') && /\nname:\s*[^\n]+/.test(content) && /\ndescription:\s*[^\n]+/.test(content);
10
+ }
11
+
12
+ class SkillLifecycle {
13
+ constructor(root) { this.root = root; }
14
+ _dir(name) { return path.join(this.root, name); }
15
+ _meta(name) { return path.join(this._dir(name), 'lifecycle.json'); }
16
+
17
+ stage({ name, content, source = 'pattern', evidence = {} }) {
18
+ if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, error: 'invalid skill name' };
19
+ if (!validSkill(content)) return { ok: false, error: 'invalid SKILL.md frontmatter' };
20
+ const id = `candidate_${crypto.randomBytes(6).toString('hex')}`;
21
+ return { ok: true, candidate: { id, name, content, source, evidence, status: 'candidate', createdAt: new Date().toISOString() } };
22
+ }
23
+
24
+ async promote(candidate, approval, validate = async () => ({ ok: true })) {
25
+ if (!approval?.approved || !approval.userId) return { ok: false, error: 'explicit user approval required' };
26
+ const validation = await validate(candidate);
27
+ if (!validation?.ok) return { ok: false, error: 'skill validation failed', validation };
28
+ const dir = this._dir(candidate.name);
29
+ fs.mkdirSync(path.join(dir, 'versions'), { recursive: true });
30
+ const meta = readJsonSafeSync(this._meta(candidate.name), { currentVersion: 0, versions: [] });
31
+ const version = (meta.currentVersion || 0) + 1;
32
+ const currentPath = path.join(dir, 'SKILL.md');
33
+ if (fs.existsSync(currentPath) && meta.currentVersion) {
34
+ writeFileAtomicSync(path.join(dir, 'versions', `${meta.currentVersion}.md`), fs.readFileSync(currentPath));
35
+ }
36
+ writeFileAtomicSync(currentPath, candidate.content);
37
+ meta.currentVersion = version;
38
+ meta.versions.push({ version, candidateId: candidate.id, source: candidate.source, evidence: candidate.evidence, approvedBy: approval.userId, approvedAt: new Date().toISOString(), validation });
39
+ writeJsonAtomicSync(this._meta(candidate.name), meta, { mode: 0o600 });
40
+ return { ok: true, name: candidate.name, version, path: currentPath, validation };
41
+ }
42
+
43
+ rollback(name, version, approval) {
44
+ if (!approval?.approved || !approval.userId) return { ok: false, error: 'explicit user approval required' };
45
+ const dir = this._dir(name);
46
+ const source = path.join(dir, 'versions', `${version}.md`);
47
+ if (!fs.existsSync(source)) return { ok: false, error: `version not found: ${version}` };
48
+ const current = path.join(dir, 'SKILL.md');
49
+ const meta = readJsonSafeSync(this._meta(name), { currentVersion: 0, versions: [] });
50
+ if (fs.existsSync(current) && meta.currentVersion) writeFileAtomicSync(path.join(dir, 'versions', `${meta.currentVersion}.md`), fs.readFileSync(current));
51
+ writeFileAtomicSync(current, fs.readFileSync(source));
52
+ meta.currentVersion = version;
53
+ meta.rollback = { toVersion: version, approvedBy: approval.userId, at: new Date().toISOString() };
54
+ writeJsonAtomicSync(this._meta(name), meta, { mode: 0o600 });
55
+ return { ok: true, name, version, path: current };
56
+ }
57
+ }
58
+
59
+ module.exports = { SkillLifecycle, validSkill };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { writeFileAtomicSync } = require('./atomic-file');
7
+
8
+ function hashContent(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
9
+
10
+ class StructuralPatchEngine {
11
+ constructor(options = {}) {
12
+ this.history = new Map();
13
+ this.maxHistory = options.maxHistory || 50;
14
+ }
15
+
16
+ inspect(filePath) {
17
+ const target = path.resolve(filePath);
18
+ if (!fs.existsSync(target)) return { exists: false, path: target, hash: null, content: '' };
19
+ const content = fs.readFileSync(target, 'utf8');
20
+ return { exists: true, path: target, hash: hashContent(content), content };
21
+ }
22
+
23
+ preview(filePath, operations, options = {}) {
24
+ const state = this.inspect(filePath);
25
+ if (options.expectedHash && options.expectedHash !== state.hash) {
26
+ return { ok: false, error: 'conflict: file changed since it was read', expectedHash: options.expectedHash, actualHash: state.hash };
27
+ }
28
+ let updated = state.content;
29
+ const changes = [];
30
+ for (const [index, operation] of (operations || []).entries()) {
31
+ const search = operation.search;
32
+ const replacement = operation.replace ?? '';
33
+ if (typeof search !== 'string' || search.length === 0) return { ok: false, error: `operation ${index}: search is required` };
34
+ const occurrences = updated.split(search).length - 1;
35
+ if (occurrences === 0) return { ok: false, error: `operation ${index}: search text not found` };
36
+ if (occurrences > 1 && !operation.replaceAll) return { ok: false, error: `operation ${index}: search is ambiguous (${occurrences} matches)` };
37
+ updated = operation.replaceAll ? updated.split(search).join(replacement) : updated.replace(search, replacement);
38
+ changes.push({ index, occurrences: operation.replaceAll ? occurrences : 1, removedChars: search.length, addedChars: replacement.length });
39
+ }
40
+ return {
41
+ ok: true, path: state.path, beforeHash: state.hash, afterHash: hashContent(updated),
42
+ before: state.content, after: updated, changes,
43
+ risk: changes.length > 10 || Math.abs(updated.length - state.content.length) > 10000 ? 'high' : changes.length > 3 ? 'medium' : 'low',
44
+ };
45
+ }
46
+
47
+ apply(filePath, operations, options = {}) {
48
+ const preview = this.preview(filePath, operations, options);
49
+ if (!preview.ok || options.dryRun) return preview;
50
+ const id = `patch_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`;
51
+ writeFileAtomicSync(preview.path, preview.after);
52
+ this.history.set(id, { path: preview.path, content: preview.before, expectedHash: preview.afterHash });
53
+ while (this.history.size > this.maxHistory) this.history.delete(this.history.keys().next().value);
54
+ return { ok: true, id, path: preview.path, beforeHash: preview.beforeHash, afterHash: preview.afterHash, changes: preview.changes, risk: preview.risk };
55
+ }
56
+
57
+ rollback(id) {
58
+ const entry = this.history.get(id);
59
+ if (!entry) return { ok: false, error: `unknown patch: ${id}` };
60
+ const current = this.inspect(entry.path);
61
+ if (current.hash !== entry.expectedHash) return { ok: false, error: 'rollback conflict: file changed after patch', actualHash: current.hash };
62
+ writeFileAtomicSync(entry.path, entry.content);
63
+ this.history.delete(id);
64
+ return { ok: true, id, path: entry.path, hash: hashContent(entry.content) };
65
+ }
66
+ }
67
+
68
+ module.exports = { StructuralPatchEngine, hashContent };
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { getProviderConfig } = require('./api');
5
+ const { AgentWorkspaceManager } = require('./agent-workspace');
5
6
 
6
7
  const SUB_AGENTS_FILE = path.join(os.homedir(), '.natureco', 'sub-agents.json');
7
8
 
@@ -64,12 +65,15 @@ async function spawnSubAgent(type, task, options = {}) {
64
65
  saveAgents(agents);
65
66
 
66
67
  try {
68
+ const workspaceManager = options.workspaceManager || new AgentWorkspaceManager();
69
+ const useWorkspace = options.isolatedWorktree !== false && ['general', 'debugger'].includes(type);
70
+ const executeAgent = async (workspace = null) => {
67
71
  const providerConfig = getProviderConfig();
68
72
  if (!providerConfig) {
69
73
  throw new Error('Provider not configured. Run: natureco configure');
70
74
  }
71
75
 
72
- const systemPrompt = options.systemPrompt || SYSTEM_PROMPTS[type];
76
+ const systemPrompt = (options.systemPrompt || SYSTEM_PROMPTS[type]) + (workspace ? `\n\nIsolated workspace: ${workspace.cwd}\nOnly operate inside this workspace.` : '');
73
77
 
74
78
  const baseUrl = providerConfig.url.replace(/\/+$/, '');
75
79
  const endpoint = `${baseUrl}/chat/completions`;
@@ -109,7 +113,14 @@ async function spawnSubAgent(type, task, options = {}) {
109
113
  const usage = data.usage || {};
110
114
  const duration = new Date(entry.completedAt) - new Date(entry.startedAt);
111
115
 
112
- return { result: content, usage, duration };
116
+ return { result: content, usage, duration, workspace: workspace?.cwd || null };
117
+ };
118
+ if (useWorkspace) {
119
+ const isolated = await workspaceManager.run(entry.id, executeAgent, { merge: options.mergeWorktree === true });
120
+ if (!isolated.ok) throw new Error(isolated.error);
121
+ return isolated.result;
122
+ }
123
+ return await executeAgent();
113
124
  } catch (err) {
114
125
  entry.status = 'failed';
115
126
  entry.result = err.message;
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function fingerprint(text) {
6
+ return crypto.createHash('sha256').update(String(text || '').replace(/\d+ms|\d+\.\d+s|\d+/g, '#')).digest('hex').slice(0, 16);
7
+ }
8
+
9
+ function analyzeTestFailure(output, exitCode = 1) {
10
+ const text = String(output || '');
11
+ const findings = [];
12
+ const patterns = [
13
+ { type: 'assertion', re: /(?:AssertionError|expected .* (?:to|but)|FAIL\s+)([^\n]*)/gi },
14
+ { type: 'syntax', re: /(?:SyntaxError|ParseError)[:\s]+([^\n]*)/gi },
15
+ { type: 'type', re: /(?:TypeError|TS\d{4}:)[:\s]*([^\n]*)/gi },
16
+ { type: 'module', re: /(?:Cannot find module|ModuleNotFoundError)[:\s]*([^\n]*)/gi },
17
+ { type: 'timeout', re: /(?:timed out|timeout of \d+ms exceeded|Test timeout)/gi },
18
+ ];
19
+ for (const pattern of patterns) {
20
+ pattern.re.lastIndex = 0;
21
+ let match;
22
+ while ((match = pattern.re.exec(text)) && findings.length < 50) {
23
+ findings.push({ type: pattern.type, message: (match[1] || match[0]).trim().slice(0, 500) });
24
+ }
25
+ }
26
+ const locations = [...text.matchAll(/(?:\(|\s)([^\s():]+\.(?:js|ts|tsx|jsx|py|go|rs|java)):(\d+)(?::(\d+))?/g)]
27
+ .slice(0, 50).map(match => ({ file: match[1], line: Number(match[2]), column: match[3] ? Number(match[3]) : null }));
28
+ const summaryMatch = text.match(/(\d+)\s+(?:failed|failing|failures?)/i);
29
+ return {
30
+ ok: exitCode === 0,
31
+ exitCode,
32
+ framework: /vitest/i.test(text) ? 'vitest' : /jest/i.test(text) ? 'jest' : /pytest|FAILED .*\.py/i.test(text) ? 'pytest' : /TS\d{4}/.test(text) ? 'typescript' : 'unknown',
33
+ failedCount: summaryMatch ? Number(summaryMatch[1]) : (exitCode === 0 ? 0 : Math.max(1, findings.length)),
34
+ findings, locations, fingerprint: fingerprint(text), rawTail: text.slice(-6000),
35
+ };
36
+ }
37
+
38
+ class AutoFixLoop {
39
+ constructor(options = {}) { this.maxAttempts = options.maxAttempts || 3; }
40
+
41
+ async run({ runTests, proposeFix, onAttempt }) {
42
+ const attempts = [];
43
+ let previousFingerprint = null;
44
+ for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
45
+ const startedAt = Date.now();
46
+ const testResult = await runTests({ attempt });
47
+ const analysis = analyzeTestFailure(testResult.output, testResult.exitCode);
48
+ const record = { attempt, analysis, durationMs: Date.now() - startedAt };
49
+ attempts.push(record);
50
+ if (onAttempt) await onAttempt(record);
51
+ if (analysis.ok) return { ok: true, attempts };
52
+ if (analysis.fingerprint === previousFingerprint) {
53
+ return { ok: false, stopped: 'no-progress', attempts, analysis };
54
+ }
55
+ previousFingerprint = analysis.fingerprint;
56
+ const fix = await proposeFix({ attempt, analysis, attempts });
57
+ record.fix = fix || null;
58
+ if (!fix || fix.applied === false) return { ok: false, stopped: 'no-fix', attempts, analysis };
59
+ }
60
+ return { ok: false, stopped: 'max-attempts', attempts, analysis: attempts.at(-1)?.analysis };
61
+ }
62
+ }
63
+
64
+ module.exports = { analyzeTestFailure, AutoFixLoop, fingerprint };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const { assessToolPath } = require('./tool-path-policy');
4
+ const { validateJsonSchema } = require('./json-schema');
5
+ const { standardToolResult } = require('./tool-result');
6
+
7
+ // Transport-agnostic pipeline: resolve -> availability -> policy -> execute -> post-process.
8
+ async function executeThroughGateway(options) {
9
+ const {
10
+ toolName, args = {}, resolveTool, checkAvailability, policyChecks = [],
11
+ execute, postProcess, allowSensitivePaths = false, standardResult = false,
12
+ normalizeSuccess = result => ({ result }),
13
+ normalizeError = error => ({ error }),
14
+ } = options || {};
15
+
16
+ if (!toolName || typeof resolveTool !== 'function') {
17
+ return normalizeError('Geçersiz araç çalıştırma isteği');
18
+ }
19
+
20
+ let tool;
21
+ try { tool = await resolveTool(toolName); }
22
+ catch (error) { return normalizeError(error?.message || String(error)); }
23
+
24
+ if (!tool) return normalizeError(`Tool bulunamadı: ${toolName}`);
25
+ const executor = execute || tool.execute;
26
+ if (typeof executor !== 'function') return normalizeError(`Tool execute fonksiyonu yok: ${toolName}`);
27
+
28
+ try {
29
+ const schema = tool.inputSchema || tool.parameters || tool.schema?.parameters;
30
+ const validation = validateJsonSchema(schema, args);
31
+ if (!validation.valid) return normalizeError(`Geçersiz araç argümanları: ${validation.errors.join('; ')}`);
32
+ if (!allowSensitivePaths) {
33
+ const pathDecision = assessToolPath(toolName, args);
34
+ if (!pathDecision.allowed) return normalizeError(pathDecision.reason);
35
+ }
36
+ if (checkAvailability) {
37
+ const decision = await checkAvailability({ toolName, args, tool });
38
+ if (decision === false) return normalizeError(`${toolName} şu anda kullanılamıyor`);
39
+ if (decision?.allowed === false) return normalizeError(decision.reason || `${toolName} şu anda kullanılamıyor`);
40
+ }
41
+ for (const check of policyChecks) {
42
+ if (typeof check !== 'function') continue;
43
+ const decision = await check({ toolName, args, tool });
44
+ if (decision === false) return normalizeError('Araç çalıştırma politikası engelledi');
45
+ if (decision?.allowed === false) return normalizeError(decision.reason || 'Araç çalıştırma politikası engelledi');
46
+ }
47
+ let result = await executor.call(tool, args);
48
+ if (postProcess) result = await postProcess({ toolName, args, tool, result });
49
+ if (standardResult) return standardToolResult(result, { tool: toolName });
50
+ return normalizeSuccess(result);
51
+ } catch (error) {
52
+ return normalizeError(error?.message || String(error));
53
+ }
54
+ }
55
+
56
+ module.exports = { executeThroughGateway };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ let cache = null;
7
+
8
+ function loadToolManifest({ toolsDir = path.join(__dirname, '..', 'tools'), refresh = false } = {}) {
9
+ if (cache && !refresh) return cache;
10
+ const entries = new Map();
11
+ if (!fs.existsSync(toolsDir)) return entries;
12
+ for (const file of fs.readdirSync(toolsDir).filter(name => name.endsWith('.js')).sort()) {
13
+ try {
14
+ const mod = require(path.join(toolsDir, file));
15
+ const name = mod.name || path.basename(file, '.js');
16
+ const execute = mod.execute || mod.default?.execute;
17
+ if (!name || typeof execute !== 'function') continue;
18
+ entries.set(name, {
19
+ name, description: mod.description || `${name} tool`,
20
+ inputSchema: mod.inputSchema || mod.parameters || { type: 'object', properties: {} },
21
+ execute, module: mod, source: file,
22
+ });
23
+ } catch { /* unavailable optional tool */ }
24
+ }
25
+ cache = entries;
26
+ return entries;
27
+ }
28
+
29
+ function clearToolManifestCache() { cache = null; }
30
+
31
+ module.exports = { loadToolManifest, clearToolManifestCache };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const SENSITIVE_READ = [
7
+ /(^|[\\/])\.ssh[\\/]/i, /id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
8
+ /\.pem$|\.ppk$|\.key$/i, /(^|[\\/])\.aws[\\/]/i,
9
+ /gcloud[\\/].*(credential|token)/i, /(^|[\\/])\.npmrc$/i,
10
+ /(^|[\\/])\.git-credentials$/i, /\.natureco[\\/]config\.json$/i,
11
+ /(^|[\\/])\.netrc$/i,
12
+ ];
13
+ const SENSITIVE_WRITE = [
14
+ ...SENSITIVE_READ,
15
+ /(^|[\\/])authorized_keys$/i, /System32[\\/]drivers[\\/]etc/i,
16
+ /\.aws[\\/]credentials/i,
17
+ /^[/\\](etc|usr|bin|sbin|boot|sys)[\\/]/i,
18
+ /(^|[\\/])etc[\\/](passwd|shadow|sudoers|hosts|crontab|ssh)/i,
19
+ ];
20
+
21
+ function expandPath(value) {
22
+ if (typeof value !== 'string' || !value.trim()) return value;
23
+ const trimmed = value.trim();
24
+ if (trimmed === '~') return os.homedir();
25
+ if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
26
+ return path.join(os.homedir(), trimmed.slice(2));
27
+ }
28
+ return path.resolve(trimmed);
29
+ }
30
+
31
+ function assessToolPath(toolName, args = {}) {
32
+ const rawPath = args.path || args.filePath || args.file_path;
33
+ if (!rawPath) return { allowed: true };
34
+ const targetPath = expandPath(rawPath);
35
+ const write = toolName === 'write_file' || toolName === 'edit_file' || toolName === 'structural_patch';
36
+ const read = toolName === 'read_file';
37
+ if (!write && !read) return { allowed: true, path: targetPath };
38
+ const patterns = write ? SENSITIVE_WRITE : SENSITIVE_READ;
39
+ if (patterns.some(pattern => pattern.test(targetPath))) {
40
+ return {
41
+ allowed: false,
42
+ path: targetPath,
43
+ reason: `Hassas dosya yolu güvenlik politikasıyla engellendi: ${targetPath}`,
44
+ };
45
+ }
46
+ return { allowed: true, path: targetPath };
47
+ }
48
+
49
+ module.exports = { assessToolPath, expandPath, SENSITIVE_READ, SENSITIVE_WRITE };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ function standardToolResult(value, metrics = {}) {
4
+ if (value && typeof value === 'object' && typeof value.ok === 'boolean' && 'data' in value) return value;
5
+ const failed = value && typeof value === 'object' && (value.success === false || value.error);
6
+ const error = failed ? String(value.error || 'Tool execution failed') : null;
7
+ return {
8
+ ok: !failed,
9
+ data: failed ? null : value,
10
+ error,
11
+ warnings: Array.isArray(value?.warnings) ? value.warnings : [],
12
+ artifacts: Array.isArray(value?.artifacts) ? value.artifacts : [],
13
+ metrics: { ...metrics, ...(value?.metrics || {}) },
14
+ };
15
+ }
16
+
17
+ module.exports = { standardToolResult };
@@ -1,7 +1,11 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const chalk = require('chalk');
4
- const inquirer = require('./inquirer-wrapper');
3
+ const chalk = require('chalk');
4
+ const inquirer = require('./inquirer-wrapper');
5
+ const { executeThroughGateway } = require('./tool-execution-gateway');
6
+ const { checkPermission } = require('./permissions');
7
+ const { checkPreHooks, runPostHooks } = require('./tool-hooks');
8
+ const { loadToolManifest } = require('./tool-manifest');
5
9
 
6
10
  // ── Spinner ───────────────────────────────────────────────────────────────────
7
11
  const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
@@ -12,8 +16,8 @@ function startSpinner(label) {
12
16
  process.stdout.write(`\r${chalk.cyan(SPINNER_FRAMES[i++ % SPINNER_FRAMES.length])} ${chalk.gray(label)}`);
13
17
  }, 80);
14
18
  return timer;
15
- }
16
-
19
+ }
20
+
17
21
  function stopSpinner(timer, label, success = true) {
18
22
  clearInterval(timer);
19
23
  if (success) {
@@ -55,23 +59,15 @@ function resetSessionStats() {
55
59
  }
56
60
 
57
61
  // ── Load tools ────────────────────────────────────────────────────────────────
58
- function loadTools() {
59
- const toolsDir = path.join(__dirname, '..', 'tools');
60
- const tools = {};
61
-
62
- if (!fs.existsSync(toolsDir)) return tools;
63
-
64
- const files = fs.readdirSync(toolsDir).filter(f => f.endsWith('.js'));
65
- for (const file of files) {
66
- try {
67
- const tool = require(path.join(toolsDir, file));
68
- if (tool.name && tool.execute) tools[tool.name] = tool;
69
- } catch (err) {
70
- console.error(chalk.red(`Failed to load tool ${file}:`, err.message));
71
- }
72
- }
73
- return tools;
74
- }
62
+ function loadTools() {
63
+ return Object.fromEntries([...loadToolManifest()].map(([name, entry]) => [name, {
64
+ ...entry.module,
65
+ name: entry.name,
66
+ description: entry.description,
67
+ inputSchema: entry.inputSchema,
68
+ execute: entry.execute,
69
+ }]));
70
+ }
75
71
 
76
72
  function getToolDefinitions() {
77
73
  const tools = loadTools();
@@ -90,11 +86,12 @@ function getToolDefinitions() {
90
86
  // v5.51.1 GÜVENLİK: edit_file eklendi — write_file diff+onay isterken, aynı riski
91
87
  // taşıyan hedefli değişiklik (edit_file) onaysız geçiyordu (SELF.md kendini-onarma
92
88
  // + Tek Beyin kanal erişimiyle birleşince kritik).
93
- function needsConfirmation(toolName, params) {
89
+ function needsConfirmation(toolName, params) {
94
90
  const p = params || {};
95
91
  return (
96
- toolName === 'write_file' ||
97
- toolName === 'edit_file' ||
92
+ toolName === 'write_file' ||
93
+ toolName === 'edit_file' ||
94
+ toolName === 'structural_patch' ||
98
95
  ((toolName === 'bash' || toolName === 'shell_command') && /\b(rm|mv|cp|chmod|chown|dd|mkfs|truncate)\b/.test(p.command || ''))
99
96
  );
100
97
  }
@@ -103,18 +100,33 @@ function needsConfirmation(toolName, params) {
103
100
  async function executeTool(toolName, params, opts = {}) {
104
101
  const safeParams = params ?? {};
105
102
  const tools = loadTools();
106
- const tool = tools[toolName];
107
- const agentMode = opts.agentMode || false;
108
-
109
- if (!tool) {
110
- return { success: false, error: `Tool '${toolName}' not found` };
111
- }
103
+ const tool = tools[toolName];
104
+ const agentMode = opts.agentMode || false;
105
+ if (!tool) {
106
+ return { success: false, error: `Tool '${toolName}' not found` };
107
+ }
108
+
109
+ // Configured permission rules and pre-hooks are mandatory in every origin.
110
+ // Interactive callers may approve `ask`; API/headless/channel callers must
111
+ // fail closed because they cannot prove that a human approved the action.
112
+ const permission = checkPermission(toolName, safeParams);
113
+ const hook = checkPreHooks(toolName, safeParams);
114
+ const policy = evaluatePolicyDecision(permission, hook, opts);
115
+ const policyAsk = policy.needsApproval ? { reason: policy.reason } : null;
116
+ if (!policy.allowed) {
117
+ return {
118
+ success: false,
119
+ error: policy.needsApproval
120
+ ? `Etkileşimsiz çağrıda kullanıcı onayı gerekiyor: ${policy.reason || toolName}`
121
+ : policy.reason || 'Araç güvenlik politikasıyla engellendi.',
122
+ };
123
+ }
112
124
 
113
125
  const label = `${toolName}${safeParams.path ? ' — ' + safeParams.path : safeParams.command ? ' — ' + safeParams.command : ''}`;
114
126
 
115
127
  // ── Onay mekanizması (dosya değiştiren araçlar ve tehlikeli bash) ──────────
116
- if (agentMode) {
117
- if (needsConfirmation(toolName, safeParams)) {
128
+ if (agentMode) {
129
+ if (policyAsk || needsConfirmation(toolName, safeParams)) {
118
130
  if (toolName === 'write_file') {
119
131
  // Diff göster
120
132
  let oldContent = '';
@@ -136,8 +148,9 @@ async function executeTool(toolName, params, opts = {}) {
136
148
  console.log(chalk.yellow(`\n 🖥️ Komut: ${chalk.white(safeParams.command)}\n`));
137
149
  }
138
150
 
139
- const confirmMsg =
140
- toolName === 'write_file' ? `✏️ ${safeParams.path} dosyası değiştirilecek` :
151
+ const confirmMsg =
152
+ policyAsk ? `🛡️ ${policyAsk.reason || toolName + ' için izin gerekli'}` :
153
+ toolName === 'write_file' ? `✏️ ${safeParams.path} dosyası değiştirilecek` :
141
154
  toolName === 'edit_file' ? `✏️ ${safeParams.path} dosyasında değişiklik yapılacak` :
142
155
  '⚠️ Bu komut çalıştırılacak';
143
156
  const { confirm } = await inquirer.prompt([{
@@ -155,23 +168,37 @@ async function executeTool(toolName, params, opts = {}) {
155
168
  }
156
169
 
157
170
  // ── Spinner ile çalıştır ──────────────────────────────────────────────────
158
- const spinner = startSpinner(label);
159
- try {
160
- const result = await tool.execute(safeParams);
161
- stopSpinner(spinner, label, result.success !== false);
162
-
163
- // İstatistik güncelle
164
- if (result.success !== false) {
165
- if (toolName === 'write_file' || toolName === 'edit_file') filesChanged++;
166
- if (toolName === 'bash') commandsRun++;
167
- }
168
-
169
- return result;
170
- } catch (error) {
171
- stopSpinner(spinner, label, false);
172
- return { success: false, error: error.message };
173
- }
174
- }
171
+ const spinner = startSpinner(label);
172
+ const result = await executeThroughGateway({
173
+ toolName,
174
+ args: safeParams,
175
+ resolveTool: () => tool,
176
+ postProcess: ({ result: value }) => runPostHooks(toolName, safeParams, value),
177
+ normalizeSuccess: value => value,
178
+ normalizeError: error => ({ success: false, error }),
179
+ allowSensitivePaths: !!opts.allowSensitivePaths,
180
+ });
181
+ stopSpinner(spinner, label, result.success !== false);
182
+
183
+ // İstatistik güncelle
184
+ if (result.success !== false) {
185
+ if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'structural_patch') filesChanged++;
186
+ if (toolName === 'bash' || toolName === 'shell_command') commandsRun++;
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ function evaluatePolicyDecision(permission, hook, opts = {}) {
193
+ const decisions = [permission, hook].filter(Boolean);
194
+ const denied = decisions.find(decision => decision.action === 'deny');
195
+ if (denied) return { allowed: false, needsApproval: false, reason: denied.reason };
196
+ const asked = decisions.find(decision => decision.action === 'ask');
197
+ if (!asked) return { allowed: true, needsApproval: false };
198
+ if (opts.agentMode) return { allowed: true, needsApproval: true, reason: asked.reason };
199
+ if (opts.approvalMode === 'preapproved') return { allowed: true, needsApproval: false };
200
+ return { allowed: false, needsApproval: true, reason: asked.reason };
201
+ }
175
202
 
176
203
  // ── Execute multiple tool calls (parallel for independent, sequential for others) ──
177
204
  const PARALLEL_SAFE_TOOLS = new Set(['read_file', 'file_search', 'grep_search', 'web_search', 'web_readability', 'duckduckgo_search', 'exa_search', 'searxng_search', 'firecrawl', 'memory_search', 'memory']);
@@ -210,5 +237,6 @@ module.exports = {
210
237
  executeToolCalls,
211
238
  getSessionStats,
212
239
  resetSessionStats,
213
- needsConfirmation,
214
- };
240
+ needsConfirmation,
241
+ evaluatePolicyDecision,
242
+ };