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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to NatureCo CLI will be documented in this file.
4
4
 
5
+ ## [5.64.0] - 2026-07-13 — Secure unified agent and operations foundation
6
+
7
+ ### Added
8
+ - Unified `AgentCore`, `ToolExecutionGateway`, single tool manifest, mandatory JSON Schema validation and standardized tool results.
9
+ - Conflict-safe structural patching, atomic rollback, coding `/undo`, `/retry`, `/compact`, local code intelligence and LSP JSON-RPC client.
10
+ - Provenance/confidence/TTL memory records, conflict resolution, approved skill promotion, versioning and rollback.
11
+ - Shared channel SDK with pairing-by-default, persistent idempotent delivery queue, retries, dead letters, reconnect supervision, health and metrics.
12
+ - macOS Keychain, Windows DPAPI, Linux Secret Service and AES-256-GCM encrypted secret fallback.
13
+ - Encrypted multi-device sync primitives with authenticated envelopes and vector-clock conflict detection.
14
+ - Isolated sub-agent worktrees, test-failure analysis/repair loop, startup benchmark and TR/EN catalog snapshots.
15
+
16
+ ### Security
17
+ - Fixed bulk-write guardrail bypasses, protected sensitive paths across execution origins and enabled guardrail hard-stop.
18
+ - Removed high-risk shell string interpolation from iMessage delivery, backups, plugin installation/cloning and AI-generated Git commits.
19
+ - Added channel pairing gates, non-interactive fail-closed permission handling and process-listener cleanup.
20
+
21
+ ### Tests
22
+ - 73 test files, 711 passing tests (3 skipped), zero high-severity audit findings, CLI smoke and package checks.
23
+ - Windows `--version` median reduced from ~366 ms to ~75 ms, meeting the <100 ms target.
24
+ - Real Windows/macOS/Linux GitHub Actions matrix with lint, tests, smoke, audit and package verification.
25
+
5
26
  ## [5.63.0] - 2026-07-12 — Complete English REPL localization
6
27
 
7
28
  ### Changed
package/bin/natureco.js CHANGED
@@ -1,6 +1,13 @@
1
- #!/usr/bin/env node
2
-
3
- const { Command } = require('commander');
1
+ #!/usr/bin/env node
2
+
3
+ // Fast path: version checks are used by shells, installers and health probes.
4
+ // Avoid loading Commander and ~100 command modules for a constant-time answer.
5
+ if (process.argv.length === 3 && (process.argv[2] === '--version' || process.argv[2] === '-V')) {
6
+ process.stdout.write(require('../package.json').version + '\n');
7
+ process.exit(0);
8
+ }
9
+
10
+ const { Command } = require('commander');
4
11
  const chalk = require('chalk');
5
12
  const packageJson = require('../package.json');
6
13
  // Küresel çökme/EPIPE yakalayıcıları — ham Node stack yerine düzgün mesaj + audit log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.63.0",
3
+ "version": "5.64.0",
4
4
  "description": "Terminal-native AI agent CLI with bilingual TR/EN UI, multi-agent orchestration, persistent memory, secure tools and messaging integrations.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -22,6 +22,7 @@
22
22
  "test:unit": "vitest run",
23
23
  "test:watch": "vitest",
24
24
  "test:coverage": "vitest run --coverage",
25
+ "perf:startup": "node scripts/benchmark-startup.js",
25
26
  "lint": "eslint src/ bin/ test/",
26
27
  "lint:fix": "eslint src/ bin/ test/ --fix",
27
28
  "lint:errors-only": "eslint src/ bin/ test/ --quiet",
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const bin = path.join(__dirname, '..', 'bin', 'natureco.js');
7
+ function measure(args, runs = 7) {
8
+ const samples = [];
9
+ for (let i = 0; i < runs; i++) {
10
+ const start = process.hrtime.bigint();
11
+ const result = spawnSync(process.execPath, [bin, ...args], { encoding: 'utf8', env: { ...process.env, NATURECO_NO_UPDATE_CHECK: '1', FORCE_COLOR: '0' } });
12
+ const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
13
+ if (result.status !== 0) throw new Error(`${args.join(' ')} failed: ${result.stderr}`);
14
+ samples.push(durationMs);
15
+ }
16
+ samples.sort((a, b) => a - b);
17
+ return { min: samples[0], median: samples[Math.floor(samples.length / 2)], p95: samples[Math.min(samples.length - 1, Math.ceil(samples.length * 0.95) - 1)], samples };
18
+ }
19
+
20
+ const version = measure(['--version']);
21
+ const report = { timestamp: new Date().toISOString(), node: process.version, platform: process.platform, version };
22
+ console.log(JSON.stringify(report, null, 2));
23
+ if (version.median >= 100) {
24
+ console.error(`--version median ${version.median.toFixed(1)}ms exceeds 100ms target`);
25
+ process.exitCode = 1;
26
+ }
@@ -4,7 +4,7 @@ const { getLang: _gl } = require('../utils/i18n');
4
4
  const L = (tr, en) => (_gl() === 'en' ? en : tr);
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const { execSync } = require('child_process');
7
+ const { execFileSync } = require('child_process');
8
8
 
9
9
  const NATURECO_DIR = path.join(os.homedir(), '.natureco');
10
10
 
@@ -32,7 +32,7 @@ function createBackup() {
32
32
 
33
33
  if (fs.existsSync(NATURECO_DIR)) {
34
34
  try {
35
- execSync(`tar -czf "${backupFile}" -C "${path.dirname(NATURECO_DIR)}" "${path.basename(NATURECO_DIR)}"`, {
35
+ execFileSync('tar', ['-czf', backupFile, '-C', path.dirname(NATURECO_DIR), path.basename(NATURECO_DIR)], {
36
36
  stdio: 'pipe',
37
37
  encoding: 'utf8',
38
38
  timeout: 30000
@@ -97,7 +97,7 @@ function restoreBackup(file) {
97
97
 
98
98
  console.log(chalk.yellow(`\n ⚠️ Restoring ${file}...\n`));
99
99
  try {
100
- execSync(`tar -xzf "${backupFile}" -C "${os.homedir()}"`, { stdio: 'pipe', timeout: 15000 });
100
+ execFileSync('tar', ['-xzf', backupFile, '-C', os.homedir()], { stdio: 'pipe', timeout: 15000 });
101
101
  console.log(chalk.green(' ✅ Restore complete\n'));
102
102
  } catch {
103
103
  console.log(chalk.red(' ❌ Restore failed\n'));
@@ -2,7 +2,7 @@ const path = require('path');
2
2
  const os = require('os');
3
3
  const fs = require('fs');
4
4
  const readline = require('readline');
5
- const { execSync } = require('child_process');
5
+ const { execSync, execFileSync } = require('child_process');
6
6
  const inquirer = require('../utils/inquirer-wrapper');
7
7
  const TB = require('../utils/token-budget');
8
8
  const chalk = require('chalk');
@@ -14,7 +14,11 @@ const { getMemoryPrompt, loadMemory } = require('../utils/memory');
14
14
  const { getAgentsPrompt } = require('../utils/agents');
15
15
  const { createSession, addMessageToSession } = require('../utils/sessions');
16
16
  const { addToHistory } = require('../utils/history');
17
- const { getToolDefinitions, executeTool } = require('../utils/tool-runner');
17
+ const { getToolDefinitions, executeTool } = require('../utils/tool-runner');
18
+ const { AgentCore } = require('../utils/agent-core');
19
+ const { CodingSession } = require('../utils/coding-session');
20
+
21
+ const agentCore = new AgentCore({ maxIterations: 10 });
18
22
 
19
23
  let rl = null;
20
24
 
@@ -224,12 +228,17 @@ async function streamMessage(providerConfig, messages, tools) {
224
228
  // ── Tool execution ────────────────────────────────────────────────────────────
225
229
  const DANGEROUS = [/\brm\b/, /\brmdir\b/, /\bdelete\b/i, /\bdrop\b/i, /\btruncate\b/i];
226
230
 
227
- async function runToolCall(toolCall, stats, dryRun = false) {
231
+ async function runToolCall(toolCall, stats, dryRun = false, codingSession = null) {
232
+ const guard = agentCore.assess({ name: toolCall.name, input: toolCall.input });
233
+ if (guard.blocked) return { success: false, error: guard.reason || `blocked_by_guardrails: ${toolCall.name}` };
228
234
  const inputPreview = JSON.stringify(toolCall.input).slice(0, 60);
229
235
 
230
- const needsConfirm =
236
+ const needsConfirm =
231
237
  toolCall.name === 'write_file' ||
232
- (toolCall.name === 'bash' && DANGEROUS.some(re => re.test(toolCall.input.command || '')));
238
+ (toolCall.name === 'bash' && DANGEROUS.some(re => re.test(toolCall.input.command || '')));
239
+
240
+ const risk = codingSession?.riskSummary(toolCall);
241
+ if (risk && risk.level !== 'low') console.log(chalk.gray(` Risk: ${risk.level} (${risk.risks.join(', ')})`));
233
242
 
234
243
  // Dry-run: write_file'ı engelle, diff göster
235
244
  if (dryRun && toolCall.name === 'write_file') {
@@ -261,8 +270,12 @@ async function runToolCall(toolCall, stats, dryRun = false) {
261
270
  }
262
271
  }
263
272
 
264
- const spinner = startSpinner(`${toolCall.name} ${inputPreview}`);
265
- const result = await executeTool(toolCall.name, toolCall.input);
273
+ const spinner = startSpinner(`${toolCall.name} ${inputPreview}`);
274
+ if (!dryRun && codingSession && (toolCall.name === 'write_file' || toolCall.name === 'edit_file')) {
275
+ codingSession.capture(toolCall.input.path);
276
+ }
277
+ const result = await executeTool(toolCall.name, toolCall.input);
278
+ agentCore.record({ name: toolCall.name, input: toolCall.input }, result);
266
279
  stopSpinner(spinner, toolCall.name, result.success !== false);
267
280
 
268
281
  if (result.success !== false) {
@@ -310,7 +323,8 @@ async function runTests(projectIndex, conversationMessages, tools, providerConfi
310
323
  }
311
324
 
312
325
  // ── Main ──────────────────────────────────────────────────────────────────────
313
- async function code(targetFile, options = {}) {
326
+ async function code(targetFile, options = {}) {
327
+ const codingSession = new CodingSession();
314
328
  const workDir = options.dir || process.cwd();
315
329
  const apiKey = getApiKey();
316
330
  const config = getConfig();
@@ -507,15 +521,35 @@ ${indexPrompt}`);
507
521
  ['/run <cmd>',L('Komutu çalıştır, hata varsa düzelt', 'Run command, fix errors if any')],
508
522
  ['/test', L('Testleri çalıştır, hata varsa düzelt', 'Run tests, fix errors if any')],
509
523
  ['/git', L('Git durumu ve son commitler', 'Git status and recent commits')],
510
- ['/commit', L('Staged değişiklikleri AI ile commit et', 'Commit staged changes with AI')],
524
+ ['/commit', L('Staged değişiklikleri AI ile commit et', 'Commit staged changes with AI')],
525
+ ['/undo', L('Son dosya değişikliğini geri al', 'Undo the last file change')],
526
+ ['/retry', L('Son isteği yeniden çalıştır', 'Retry the last request')],
527
+ ['/compact', L('Konuşma bağlamını sıkıştır', 'Compact conversation context')],
511
528
  ['/help', L('Bu yardım', 'This help')],
512
529
  ].forEach(([c, d]) => console.log(' ' + chalk.cyan(c.padEnd(12)) + chalk.gray(d)));
513
530
  console.log(chalk.gray(' Ctrl+C'.padEnd(14) + L('Çıkış', 'Exit')));
514
531
  console.log();
515
532
  return;
516
- case 'clear':
533
+ case 'clear':
517
534
  console.clear();
518
- return;
535
+ return;
536
+ case 'undo': {
537
+ const undone = codingSession.undo();
538
+ console.log(undone.ok ? chalk.green(` ✓ ${undone.path}`) : chalk.yellow(` ${undone.error}`));
539
+ return;
540
+ }
541
+ case 'compact': {
542
+ const compacted = codingSession.compact(conversationMessages);
543
+ conversationMessages.splice(0, conversationMessages.length, ...compacted.messages);
544
+ console.log(chalk.green(` ✓ Context compacted: ${compacted.before} → ${compacted.after}`));
545
+ return;
546
+ }
547
+ case 'retry': {
548
+ const previous = codingSession.retryMessage();
549
+ if (!previous) console.log(chalk.yellow(L(' Tekrarlanacak istek yok.', ' No request to retry.')));
550
+ else await handleMessage(previous);
551
+ return;
552
+ }
519
553
  case 'summary':
520
554
  case 'done': {
521
555
  const sum = await generateSummary(conversationMessages, providerConfig);
@@ -591,7 +625,7 @@ ${indexPrompt}`);
591
625
  console.log(chalk.cyan(`\n ${L('Önerilen', 'Suggested')}: ${chalk.white(commitMsg)}\n`));
592
626
  const ok = await confirmAction(L('Commit edilsin mi?', 'Commit?'));
593
627
  if (ok) {
594
- execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { cwd: workDir, stdio: 'pipe' });
628
+ execFileSync('git', ['commit', '-m', commitMsg], { cwd: workDir, stdio: 'pipe' });
595
629
  console.log(chalk.green(L(' ✓ Commit yapıldı!\n', ' ✓ Committed!\n')));
596
630
  } else {
597
631
  console.log(chalk.gray(L(' İptal edildi.\n', ' Cancelled.\n')));
@@ -607,7 +641,8 @@ ${indexPrompt}`);
607
641
  }
608
642
  }
609
643
 
610
- stats.messageCount++;
644
+ stats.messageCount++;
645
+ codingSession.rememberUserMessage(userMessage);
611
646
  console.log(chalk.white('You ') + userMessage);
612
647
  conversationMessages.push({ role: 'user', content: userMessage });
613
648
 
@@ -655,7 +690,7 @@ ${indexPrompt}`);
655
690
 
656
691
  for (let ti = 0; ti < streamResult.toolCalls.length; ti++) {
657
692
  const toolCall = streamResult.toolCalls[ti];
658
- const result = await runToolCall(toolCall, stats, options.dryRun);
693
+ const result = await runToolCall(toolCall, stats, options.dryRun, codingSession);
659
694
  const resultStr = result.success !== false
660
695
  ? (result.output || JSON.stringify(result))
661
696
  : `${L('Hata', 'Error')}: ${result.error}`;
@@ -33,6 +33,9 @@ const { getFallbackChain } = require("../utils/fallback-chain");
33
33
  const { getTaskManager } = require("../utils/tasks");
34
34
  const { buildTiers, assemble, discoverProjectRules } = require("../utils/system-prompt");
35
35
  const { buildSkillIndex } = require("../utils/skill-index");
36
+ const { AgentCore } = require("../utils/agent-core");
37
+
38
+ const agentCore = new AgentCore({ maxIterations: 10 });
36
39
 
37
40
  const IS_MAC = os.platform() === "darwin";
38
41
  const MAX_ITERATIONS = 10;
@@ -624,9 +627,16 @@ async function codeV5(targetPath) {
624
627
  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']);
625
628
 
626
629
  async function processToolCalls(reply, config, toolDefs, messages, onToolResult) {
630
+ agentCore.startIteration();
631
+ const coreBlocked = new Map();
627
632
  for (const tc of reply.tool_calls) {
628
633
  let args = {};
629
634
  try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
635
+ const guard = agentCore.assess({ name: tc.function.name, input: args });
636
+ if (guard.blocked) {
637
+ coreBlocked.set(tc.id, guard.reason || "blocked_by_guardrails");
638
+ continue;
639
+ }
630
640
  const risk = assessRisk(tc.function.name, args);
631
641
 
632
642
  // Built-in risk assessment
@@ -687,7 +697,11 @@ async function processToolCalls(reply, config, toolDefs, messages, onToolResult)
687
697
 
688
698
  messages.push({ role: "assistant", content: reply.content || null, tool_calls: reply.tool_calls });
689
699
 
690
- const parsed = reply.tool_calls.map(tc => {
700
+ for (const [id, reason] of coreBlocked) {
701
+ messages.push({ role: "tool", tool_call_id: id, content: "ERROR: " + reason });
702
+ }
703
+
704
+ const parsed = reply.tool_calls.filter(tc => !coreBlocked.has(tc.id)).map(tc => {
691
705
  let args = {};
692
706
  try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
693
707
  return { name: tc.function.name, args, id: tc.id };
@@ -2,13 +2,75 @@ const chalk = require('chalk');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
- const { spawn, execSync } = require('child_process');
5
+ const { spawn, execSync, execFileSync } = require('child_process');
6
6
  const pino = require('pino');
7
7
  const { loadBaileys } = require('../utils/baileys');
8
8
  const { ApiError } = require('../utils/errors');
9
+ const { ensurePendingPairing, isPaired } = require('../utils/pairing-store');
10
+ const { ChannelAdapter, ChannelDeliveryManager } = require('../utils/channel-sdk');
11
+ const { DeliveryStore } = require('../utils/delivery-store');
9
12
 
10
13
  const PID_FILE = path.join(os.homedir(), '.natureco', 'gateway.pid');
11
14
  const LOG_FILE = path.join(os.homedir(), '.natureco', 'gateway.log');
15
+ const gatewayStartedAt = Date.now();
16
+ const gatewayDeliveryManager = new ChannelDeliveryManager({ store: new DeliveryStore() });
17
+
18
+ async function buildGatewayHealth(config = {}, manager = gatewayDeliveryManager) {
19
+ const configuredChannels = ['whatsapp', 'telegram', 'signal', 'discord', 'slack', 'irc', 'mattermost', 'imessage', 'sms']
20
+ .filter(name => Object.keys(config).some(key => key.toLowerCase().startsWith(name) && !!config[key]));
21
+ const adapterHealth = await manager.health();
22
+ const channelStates = Object.fromEntries(configuredChannels.map(name => {
23
+ const adapter = adapterHealth.find(item => item.channel === name);
24
+ return [name, adapter || { channel: name, state: 'configured', ok: null }];
25
+ }));
26
+ const metrics = manager.snapshotMetrics();
27
+ return {
28
+ ok: metrics.failed === 0 || metrics.delivered > 0,
29
+ status: metrics.failed > 0 ? 'degraded' : 'healthy',
30
+ uptimeSeconds: Math.floor((Date.now() - gatewayStartedAt) / 1000),
31
+ pid: process.pid,
32
+ channels: channelStates,
33
+ delivery: { ...metrics, deadLetters: manager.deadLetters.length },
34
+ timestamp: new Date().toISOString(),
35
+ };
36
+ }
37
+
38
+ function registerGatewayDeliveryAdapters(config, manager = gatewayDeliveryManager) {
39
+ const register = (name, send, health) => {
40
+ if (manager.adapters.has(name)) return;
41
+ manager.register(new ChannelAdapter({ name, send, health }));
42
+ };
43
+ register('whatsapp', async item => {
44
+ if (!global.whatsappSock) throw new Error('WhatsApp not connected');
45
+ const jid = `${String(item.target).replace(/[^\d]/g, '')}@s.whatsapp.net`;
46
+ return global.whatsappSock.sendMessage(jid, { text: String(item.payload.message) });
47
+ }, async () => ({ ok: !!global.whatsappSock }));
48
+ register('telegram', async item => {
49
+ if (!global.telegramBot) throw new Error('Telegram not connected');
50
+ return global.telegramBot.sendMessage(String(item.target), String(item.payload.message));
51
+ }, async () => ({ ok: !!global.telegramBot }));
52
+ register('signal', item => sendSignalMessage(config, item.target, item.payload.message), async () => ({ ok: !!global.signalProvider || !!config.signalHttpUrl }));
53
+ register('irc', async item => {
54
+ if (!global.ircClient?.isReady()) throw new Error('IRC not connected');
55
+ return sendIrcMessage(global.ircClient, item.target, item.payload.message);
56
+ }, async () => ({ ok: !!global.ircClient?.isReady() }));
57
+ register('mattermost', item => sendMattermostMessage(config, item.target, item.payload.message), async () => ({ ok: !!global.mattermostProvider }));
58
+ register('imessage', async item => {
59
+ if (!global.imessageProvider) throw new Error('iMessage not connected');
60
+ return sendImessage(global.imessageProvider.imsgPath, item.target, item.payload.message);
61
+ }, async () => ({ ok: !!global.imessageProvider }));
62
+ register('sms', item => sendSmsMessage(config, item.target, item.payload.message), async () => ({ ok: !!global.smsProvider }));
63
+ register('discord', async item => {
64
+ if (!global.discordClient) throw new Error('Discord not connected');
65
+ const channel = await global.discordClient.channels.fetch(String(item.target));
66
+ return channel.send(String(item.payload.message));
67
+ }, async () => ({ ok: !!global.discordClient }));
68
+ register('slack', async item => {
69
+ if (!global.slackClient) throw new Error('Slack not connected');
70
+ return global.slackClient.chat.postMessage({ channel: String(item.target), text: String(item.payload.message) });
71
+ }, async () => ({ ok: !!global.slackClient }));
72
+ return manager;
73
+ }
12
74
 
13
75
  // `https` is used in the webhook delivery path (~line 570). The require was
14
76
  // missing; the module only worked because Node's CommonJS cache happens to
@@ -408,9 +470,14 @@ async function startWhatsAppProvider(sessionDir, config) {
408
470
  // Log incoming number
409
471
  log('whatsapp', `Incoming from: +${sender}, allowed: ${JSON.stringify(allowedNumbers)}`, 'gray');
410
472
 
411
- // Access control - skip if fromMe + LID (own conversation)
412
- if (!(msg.key.fromMe && isLID) && allowedNumbers.length > 0 && !allowedNumbers.some(n => numberMatches(n, sender))) {
413
- log('whatsapp', `blocked message from +${sender} (not in allowed list)`, 'yellow');
473
+ // Pairing is the default. The owner's own LID conversation is trusted;
474
+ // every other sender must be allowlisted or explicitly paired.
475
+ const ownConversation = msg.key.fromMe && isLID;
476
+ const gate = ownConversation
477
+ ? { allowed: true, trusted: true, reason: 'owner' }
478
+ : channelGate(config, 'whatsapp', sender);
479
+ if (!gate.allowed) {
480
+ log('whatsapp', `blocked message from +${sender} (${gate.reason})`, 'yellow');
414
481
  continue;
415
482
  }
416
483
 
@@ -437,8 +504,7 @@ async function startWhatsAppProvider(sessionDir, config) {
437
504
  try {
438
505
  // v5.47 TEK BEYIN: allow-list'teki gonderen (veya sahibin kendi cihazi) =
439
506
  // guvenilir → terminaldekiyle ayni ajan. Aksi halde hafizasiz hafif yol.
440
- const trusted = (msg.key.fromMe && isLID) ||
441
- (allowedNumbers.length > 0 && allowedNumbers.some(n => numberMatches(n, sender)));
507
+ const trusted = gate.trusted;
442
508
  let reply = '';
443
509
 
444
510
  if (trusted) {
@@ -516,8 +582,11 @@ async function startDiscordProvider(config) {
516
582
  client.on(Events.MessageCreate, async (message) => {
517
583
  if (message.author.bot) return;
518
584
  const chatId = message.channel.id;
519
- const allowedChats = config.discordAllowedChats || [];
520
- if (allowedChats.length > 0 && !allowedChats.includes(chatId)) return;
585
+ const gate = channelGate(config, 'discord', String(message.author.id || chatId));
586
+ if (!gate.allowed) {
587
+ log('discord', `blocked message from ${message.author.id} (${gate.reason})`, 'yellow');
588
+ return;
589
+ }
521
590
 
522
591
  try {
523
592
  const response = await callProviderForGateway(config, message.content, {
@@ -546,13 +615,19 @@ async function startDiscordProvider(config) {
546
615
  // yetkisiz/yabancı gönderene kişisel hafıza sızabiliyordu. channelGate: (1) allow-list
547
616
  // kuruluysa yetkisiz göndereni ENGELLE; (2) allow-list kurulu DEĞİLSE yanıt ver ama
548
617
  // kişisel hafızayı ENJEKTE ETME (trusted=false) — böylece anonim kanaldan hafıza sızmaz.
549
- function channelGate(config, channel, senderId) {
618
+ function channelGate(config, channel, senderId, pairing = { isPaired, ensurePendingPairing }) {
550
619
  const allow = config[`${channel}AllowedChats`] || config[`${channel}AllowedNumbers`] || config[`${channel}AllowedUsers`] || [];
551
- if (!Array.isArray(allow) || allow.length === 0) {
552
- return { allowed: true, trusted: false };
620
+ const allowlisted = Array.isArray(allow) && allow.map(String).includes(String(senderId));
621
+ if (allowlisted) return { allowed: true, trusted: true, reason: 'allowlist' };
622
+
623
+ const policy = config[`${channel}DmPolicy`] || 'pairing';
624
+ if (policy === 'open') return { allowed: true, trusted: false, reason: 'open' };
625
+ if (policy === 'disabled' || policy === 'allowlist') {
626
+ return { allowed: false, trusted: false, reason: policy };
553
627
  }
554
- const ok = allow.map(String).includes(String(senderId));
555
- return { allowed: ok, trusted: ok };
628
+ if (pairing.isPaired(channel, senderId)) return { allowed: true, trusted: true, reason: 'paired' };
629
+ const pending = pairing.ensurePendingPairing(channel, senderId);
630
+ return { allowed: false, trusted: false, reason: 'pairing-required', pairingId: pending.id };
556
631
  }
557
632
 
558
633
  async function startSlackProvider(config) {
@@ -1500,7 +1575,7 @@ function startImessagePollingFallback(imsgPath, config) {
1500
1575
  try {
1501
1576
  const args = ['history', '--format', 'json', '--limit', '10'];
1502
1577
  if (lastRowId) args.push('--since-rowid', String(lastRowId));
1503
- const result = execSync(`"${imsgPath}" ${args.join(' ')} 2>/dev/null`, {
1578
+ const result = execFileSync(imsgPath, args, {
1504
1579
  encoding: 'utf-8', timeout: 10000, stdio: 'pipe',
1505
1580
  });
1506
1581
  const lines = result.trim().split('\n').filter(Boolean);
@@ -1519,6 +1594,13 @@ function startImessagePollingFallback(imsgPath, config) {
1519
1594
  log('imessage', 'polling started (15s interval, history fallback)', 'green');
1520
1595
  }
1521
1596
 
1597
+ function sendImessage(imsgPath, target, text) {
1598
+ return execFileSync(imsgPath, ['send', '--to', String(target), '--text', String(text)], {
1599
+ timeout: 15000,
1600
+ stdio: 'pipe',
1601
+ });
1602
+ }
1603
+
1522
1604
  function findImsgBin() {
1523
1605
  const config = require('../utils/config').getConfig();
1524
1606
  if (config.imessageCliPath && fs.existsSync(config.imessageCliPath)) return config.imessageCliPath;
@@ -1601,9 +1683,7 @@ async function processImessageMessage(msg, config) {
1601
1683
  }
1602
1684
 
1603
1685
  if (reply) {
1604
- execSync(`${global.imessageProvider.imsgPath} send --to "${sender}" --text "${reply.replace(/"/g, '\\"')}" 2>/dev/null`, {
1605
- timeout: 15000, stdio: 'pipe',
1606
- });
1686
+ sendImessage(global.imessageProvider.imsgPath, sender, reply);
1607
1687
  log('imessage', `Reply sent to ${sender} (${reply.length} chars)`, 'green');
1608
1688
 
1609
1689
  // v5.6.40: Track outgoing message for loop prevention
@@ -1805,7 +1885,7 @@ function startHttpServer() {
1805
1885
  const server = http.createServer(async (req, res) => {
1806
1886
  // CORS headers
1807
1887
  res.setHeader('Access-Control-Allow-Origin', '*');
1808
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
1888
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
1809
1889
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
1810
1890
 
1811
1891
  if (req.method === 'OPTIONS') {
@@ -1813,6 +1893,14 @@ function startHttpServer() {
1813
1893
  res.end();
1814
1894
  return;
1815
1895
  }
1896
+
1897
+ if (req.method === 'GET' && (req.url === '/health' || req.url === '/metrics')) {
1898
+ const { getConfig } = require('../utils/config');
1899
+ const snapshot = await buildGatewayHealth(getConfig(), gatewayDeliveryManager);
1900
+ res.writeHead(snapshot.status === 'healthy' ? 200 : 503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
1901
+ res.end(JSON.stringify(req.url === '/health' ? snapshot : snapshot.delivery));
1902
+ return;
1903
+ }
1816
1904
 
1817
1905
  if (req.method === 'POST' && req.url === '/webhooks/sms') {
1818
1906
  // Twilio SMS webhook handler
@@ -1857,6 +1945,23 @@ function startHttpServer() {
1857
1945
  res.end(JSON.stringify({ error: 'Missing required fields: channel, target, message' }));
1858
1946
  return;
1859
1947
  }
1948
+
1949
+ const cfg = require('../utils/config').getConfig();
1950
+ registerGatewayDeliveryAdapters(cfg, gatewayDeliveryManager);
1951
+ if (gatewayDeliveryManager.adapters.has(channel)) {
1952
+ const queued = gatewayDeliveryManager.enqueue(channel, target, { message }, { idempotencyKey: req.headers['idempotency-key'] });
1953
+ if (queued.duplicate) {
1954
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1955
+ res.end(JSON.stringify({ success: true, duplicate: true, deliveryId: queued.id }));
1956
+ return;
1957
+ }
1958
+ const [delivery] = await gatewayDeliveryManager.drain();
1959
+ res.writeHead(delivery.ok ? 200 : 503, { 'Content-Type': 'application/json' });
1960
+ res.end(JSON.stringify(delivery.ok
1961
+ ? { success: true, channel, target, deliveryId: delivery.id, attempts: delivery.attempts }
1962
+ : { success: false, channel, target, deliveryId: delivery.id, error: delivery.error }));
1963
+ return;
1964
+ }
1860
1965
 
1861
1966
  if (channel === 'whatsapp') {
1862
1967
  if (!global.whatsappSock) {
@@ -1938,9 +2043,7 @@ function startHttpServer() {
1938
2043
  return;
1939
2044
  }
1940
2045
  try {
1941
- execSync(`${global.imessageProvider.imsgPath} send --to "${target.replace(/"/g, '\\"')}" --text "${message.replace(/"/g, '\\"')}" 2>/dev/null`, {
1942
- timeout: 15000, stdio: 'pipe',
1943
- });
2046
+ sendImessage(global.imessageProvider.imsgPath, target, message);
1944
2047
  log('http', `iMessage sent to ${target}`, 'green');
1945
2048
  res.writeHead(200, { 'Content-Type': 'application/json' });
1946
2049
  res.end(JSON.stringify({ success: true, channel: 'imessage', target }));
@@ -2133,9 +2236,7 @@ function startCronJobs(config) {
2133
2236
  log('cron', 'iMessage not connected, skipping', 'red');
2134
2237
  return;
2135
2238
  }
2136
- execSync(`${global.imessageProvider.imsgPath} send --to "${cronJob.target.replace(/"/g, '\\"')}" --text "${reply.replace(/"/g, '\\"')}" 2>/dev/null`, {
2137
- timeout: 15000, stdio: 'pipe',
2138
- });
2239
+ sendImessage(global.imessageProvider.imsgPath, cronJob.target, reply);
2139
2240
  log('cron', `Sent to iMessage: ${cronJob.target}`, 'green');
2140
2241
  } else if (cronJob.action === 'sms') {
2141
2242
  if (!global.smsProvider) {
@@ -2267,3 +2368,6 @@ if (require.main === module || process.argv.includes('--gateway-worker')) {
2267
2368
  module.exports = gatewayServer;
2268
2369
  // v5.43: test için — kanal gönderen doğrulaması + hafıza izolasyonu (Madde 7)
2269
2370
  module.exports.channelGate = channelGate;
2371
+ module.exports.buildGatewayHealth = buildGatewayHealth;
2372
+ module.exports.gatewayDeliveryManager = gatewayDeliveryManager;
2373
+ module.exports.registerGatewayDeliveryAdapters = registerGatewayDeliveryAdapters;
@@ -1,4 +1,4 @@
1
- const { execSync } = require('child_process');
1
+ const { execSync, execFileSync } = require('child_process');
2
2
  const inquirer = require('../utils/inquirer-wrapper');
3
3
  const chalk = require('chalk');
4
4
  const { getApiKey } = require('../utils/config');
@@ -111,7 +111,7 @@ async function gitCommit() {
111
111
 
112
112
  if (answer.confirm) {
113
113
  try {
114
- execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
114
+ execFileSync('git', ['commit', '-m', commitMessage], { stdio: 'inherit' });
115
115
  console.log(chalk.green('\n✅ Committed successfully\n'));
116
116
  } catch (err) {
117
117
  console.log(chalk.red('\n❌ Commit failed\n'));
@@ -1,25 +1,5 @@
1
1
  const chalk = require('chalk');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const os = require('os');
5
-
6
- const PAIRINGS_FILE = path.join(os.homedir(), '.natureco', 'pairings.json');
7
-
8
- function loadPairings() {
9
- if (!fs.existsSync(PAIRINGS_FILE)) return [];
10
- try { return JSON.parse(fs.readFileSync(PAIRINGS_FILE, 'utf8')); }
11
- catch { return []; }
12
- }
13
-
14
- function savePairings(pairings) {
15
- const dir = path.dirname(PAIRINGS_FILE);
16
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
- fs.writeFileSync(PAIRINGS_FILE, JSON.stringify(pairings, null, 2), 'utf8');
18
- }
19
-
20
- function genId() {
21
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 8).toUpperCase();
22
- }
2
+ const { loadPairings, savePairings, genCode } = require('../utils/pairing-store');
23
3
 
24
4
  function pairing(args) {
25
5
  const [action, ...params] = args || [];
@@ -84,7 +64,7 @@ function cmdReject(id) {
84
64
  }
85
65
 
86
66
  function cmdGenerate() {
87
- const code = genId();
67
+ const code = genCode();
88
68
  const pairings = loadPairings();
89
69
 
90
70
  const entry = {
@@ -56,7 +56,7 @@ const { createPasteSafeInput, createOutputFilter, enableBracketedPaste, disableB
56
56
  const { getMemoryStore } = require('../utils/memory-store');
57
57
  const { buildSkillIndex } = require('../utils/skill-index');
58
58
  const { buildTiers, assemble, discoverProjectRules } = require('../utils/system-prompt');
59
- const { ToolGuardrails } = require('../utils/tool-guardrails');
59
+ const { AgentCore } = require('../utils/agent-core');
60
60
 
61
61
  // v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
62
62
  const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
@@ -128,7 +128,8 @@ function rebuildSystemPrompt(opts) {
128
128
  }
129
129
 
130
130
  // ── Tool Guardrails instance (Hermes-style) ─────────────────────────────
131
- const guardrails = new ToolGuardrails();
131
+ const agentCore = new AgentCore({ maxIterations: 10 });
132
+ const guardrails = agentCore.guardrails;
132
133
 
133
134
  // CLI komutları (REPL içinden çalıştırılabilir)
134
135
  const CLI_COMMANDS = {
@@ -835,8 +836,9 @@ async function processToolCalls(toolCalls, onToolCall, onAsk) {
835
836
  const results = [];
836
837
 
837
838
  // Parse all tool calls first
838
- const parsed = toolCalls.map(tc => {
839
- const name = tc.function?.name || tc.name;
839
+ const parsed = agentCore.parseToolCalls(toolCalls).map(call => {
840
+ const tc = call.original;
841
+ const name = tc.function?.name || tc.name;
840
842
  const argsStr = tc.function?.arguments || tc.args || '{}';
841
843
  const id = tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
842
844
  let args;
@@ -845,11 +847,11 @@ async function processToolCalls(toolCalls, onToolCall, onAsk) {
845
847
  } catch (e) {
846
848
  args = { _parse_error: e.message, _raw: argsStr };
847
849
  }
848
- return { name, args, id };
850
+ return { name, args, id, parseError: call.parseError };
849
851
  });
850
852
 
851
853
  // Filter out blocked tools via guardrails
852
- guardrails.startIteration();
854
+ agentCore.startIteration();
853
855
  const blocked = parsed.filter(p => {
854
856
  const check = guardrails.check(p.name, p.args);
855
857
  if (check.blocked) {