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.
- package/CHANGELOG.md +21 -0
- package/bin/natureco.js +10 -3
- package/package.json +2 -1
- package/scripts/benchmark-startup.js +26 -0
- package/src/commands/backup.js +3 -3
- package/src/commands/code.js +49 -14
- package/src/commands/code_v5.js +15 -1
- package/src/commands/gateway-server.js +128 -24
- package/src/commands/git.js +2 -2
- package/src/commands/pairing.js +2 -22
- package/src/commands/repl.js +8 -6
- package/src/tools/agentic-runner.js +41 -33
- package/src/tools/memory_write.js +16 -12
- package/src/tools/structural_patch.js +25 -0
- package/src/tools/workflow.js +10 -2
- package/src/utils/agent-core.js +51 -0
- package/src/utils/agent-workspace.js +24 -0
- package/src/utils/api.js +12 -8
- package/src/utils/channel-sdk.js +212 -0
- package/src/utils/code-intelligence.js +69 -0
- package/src/utils/coding-session.js +54 -0
- package/src/utils/delivery-store.js +34 -0
- package/src/utils/i18n.js +7 -1
- package/src/utils/json-schema.js +43 -0
- package/src/utils/lsp-client.js +129 -0
- package/src/utils/memory-record.js +49 -0
- package/src/utils/pairing-store.js +55 -0
- package/src/utils/pattern-detector.js +13 -3
- package/src/utils/plugin-registry.js +3 -3
- package/src/utils/process-errors.js +14 -7
- package/src/utils/runtime-health.js +28 -0
- package/src/utils/secret-store.js +90 -0
- package/src/utils/secure-sync.js +63 -0
- package/src/utils/skill-lifecycle.js +59 -0
- package/src/utils/structural-patch.js +68 -0
- package/src/utils/sub-agent.js +13 -2
- package/src/utils/test-failure-analyzer.js +64 -0
- package/src/utils/tool-execution-gateway.js +56 -0
- package/src/utils/tool-manifest.js +31 -0
- package/src/utils/tool-path-policy.js +49 -0
- package/src/utils/tool-result.js +17 -0
- package/src/utils/tool-runner.js +81 -53
- 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
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/commands/backup.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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'));
|
package/src/commands/code.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}`;
|
package/src/commands/code_v5.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
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 =
|
|
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
|
|
520
|
-
if (
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/commands/git.js
CHANGED
|
@@ -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
|
-
|
|
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'));
|
package/src/commands/pairing.js
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
-
const
|
|
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 =
|
|
67
|
+
const code = genCode();
|
|
88
68
|
const pairings = loadPairings();
|
|
89
69
|
|
|
90
70
|
const entry = {
|
package/src/commands/repl.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
839
|
-
const
|
|
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
|
-
|
|
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) {
|