natureco-cli 5.63.0 → 5.64.1
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 +37 -0
- package/README.md +20 -3
- package/bin/natureco.js +10 -3
- package/package.json +3 -1
- package/scripts/benchmark-startup.js +26 -0
- package/scripts/e2e-context-smoke.js +33 -0
- package/src/commands/backup.js +3 -3
- package/src/commands/code.js +49 -14
- package/src/commands/code_v5.js +25 -2
- package/src/commands/gateway-server.js +128 -24
- package/src/commands/git.js +2 -2
- package/src/commands/help.js +11 -1
- 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/conversation-context.js +52 -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/token-budget.js +3 -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
|
@@ -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/help.js
CHANGED
|
@@ -12,6 +12,15 @@ const { getLang } = require('../utils/i18n');
|
|
|
12
12
|
|
|
13
13
|
const L = (tr, en) => (getLang() === 'en' ? en : tr);
|
|
14
14
|
|
|
15
|
+
function providerHostname(providerUrl) {
|
|
16
|
+
if (!providerUrl) return '';
|
|
17
|
+
try {
|
|
18
|
+
return new URL(providerUrl).hostname;
|
|
19
|
+
} catch {
|
|
20
|
+
return String(providerUrl).replace(/^https?:\/\//i, '').split('/')[0];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
function help() {
|
|
16
25
|
const config = getConfig() || {};
|
|
17
26
|
const version = require('../../package.json').version;
|
|
@@ -203,7 +212,7 @@ function help() {
|
|
|
203
212
|
tui.styled(' ╭' + '─'.repeat(cardW) + '╮', { color: tui.PALETTE.border }),
|
|
204
213
|
];
|
|
205
214
|
if (config.providerUrl) {
|
|
206
|
-
const provider = config.providerUrl
|
|
215
|
+
const provider = providerHostname(config.providerUrl);
|
|
207
216
|
cardLines.push(tui.styled(' │ ', { color: tui.PALETTE.border }) + tui.C.muted('Provider ') + tui.styled(provider.padEnd(38), { color: tui.PALETTE.text, bold: true }) + tui.styled(' │', { color: tui.PALETTE.border }));
|
|
208
217
|
}
|
|
209
218
|
if (config.providerModel) {
|
|
@@ -228,3 +237,4 @@ function help() {
|
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
module.exports = help;
|
|
240
|
+
module.exports.providerHostname = providerHostname;
|
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) {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* bu modda KAPALIDIR (onay katmanini atlamamak icin).
|
|
16
16
|
*/
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const { executeThroughGateway } = require('../utils/tool-execution-gateway');
|
|
19
|
+
const { assessToolPath } = require('../utils/tool-path-policy');
|
|
18
20
|
const os = require('os');
|
|
19
21
|
|
|
20
22
|
// Agentic dongude izin verilen araclar. bash BURADA ama guvenli: bash.js kendi
|
|
@@ -64,21 +66,9 @@ function expandHome(p) {
|
|
|
64
66
|
// Hassas dosya yolu koruması (safe modda). Coding-agent proje dosyalarina serbest erisir
|
|
65
67
|
// ama kimlik/secret yollari prompt-injection ile suistimal edilebilir (SSH backdoor,
|
|
66
68
|
// credential sizintisi). Full modda (sahibin opt-in'i) bypass edilir.
|
|
67
|
-
const SENSITIVE_READ = [
|
|
68
|
-
/(^|[\\/])\.ssh[\\/]/i, /id_rsa|id_ed25519|id_ecdsa|id_dsa/i, /\.pem$|\.ppk$|\.key$/i,
|
|
69
|
-
/(^|[\\/])\.aws[\\/]/i, /gcloud[\\/].*(credential|token)/i, /(^|[\\/])\.npmrc$/i,
|
|
70
|
-
/(^|[\\/])\.git-credentials$/i, /\.natureco[\\/]config\.json$/i, /(^|[\\/])\.netrc$/i,
|
|
71
|
-
];
|
|
72
|
-
const SENSITIVE_WRITE = [
|
|
73
|
-
/(^|[\\/])\.ssh[\\/]/i, // authorized_keys/config yazma = backdoor
|
|
74
|
-
/^\/(etc|usr|bin|sbin|boot|sys)[\\/]/i, // mutlak sistem yollari
|
|
75
|
-
/(^|[\\/])etc[\\/](passwd|shadow|sudoers|hosts|crontab|ssh)/i, // goreceli traversal dahil (../../etc/shadow)
|
|
76
|
-
/System32[\\/]drivers[\\/]etc/i, /\.aws[\\/]credentials/i,
|
|
77
|
-
];
|
|
78
69
|
function sensitivePathBlocked(p, mode) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
return pats.some((re) => re.test(s));
|
|
70
|
+
const toolName = mode === 'write' ? 'write_file' : 'read_file';
|
|
71
|
+
return !assessToolPath(toolName, { path: p }).allowed;
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
// Ajanin urettigi komutlar icin ekstra koruma. bash.js kendi politikasini uygular
|
|
@@ -315,19 +305,34 @@ async function executeCall(call, opts = {}) {
|
|
|
315
305
|
let files = call.args && call.args.files;
|
|
316
306
|
if (typeof files === 'string') { try { files = JSON.parse(files); } catch { files = null; } }
|
|
317
307
|
if (Array.isArray(files) && (/bulk/i.test(rawTool) || /file/i.test(rawTool) || norm === 'write_file')) {
|
|
308
|
+
// Bulk writes are still write_file operations. Apply the same allowlist and
|
|
309
|
+
// protected-path policy before loading or executing the tool; otherwise the
|
|
310
|
+
// early return below would bypass the normal checks later in this function.
|
|
311
|
+
if (!opts.execFull && !allowed.has('write_file')) {
|
|
312
|
+
records.push({ tool: rawTool, status: 'error', error: 'Guvenli modda write_file kapali' });
|
|
313
|
+
return { records, feedback: `${rawTool}: guvenli modda write_file izni yok.` };
|
|
314
|
+
}
|
|
318
315
|
let wf;
|
|
319
316
|
try { wf = loadTool('write_file'); } catch { wf = null; }
|
|
320
317
|
for (const f of files) {
|
|
321
318
|
if (!wf || !f || !f.path) { records.push({ tool: 'write_file', status: 'error', error: 'gecersiz dosya girisi' }); feedbacks.push('write_file HATA: gecersiz giris'); continue; }
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
} catch (e) {
|
|
328
|
-
records.push({ tool: 'write_file', status: 'error', args: { path: f.path }, error: e.message });
|
|
329
|
-
feedbacks.push(`write_file HATA: ${e.message}`);
|
|
319
|
+
const targetPath = expandHome(String(f.path).trim());
|
|
320
|
+
if (!opts.execFull && sensitivePathBlocked(targetPath, 'write')) {
|
|
321
|
+
records.push({ tool: 'write_file', status: 'error', args: { path: targetPath }, error: 'Hassas dosya yolu (guvenli modda engellendi)' });
|
|
322
|
+
feedbacks.push(`write_file: "${targetPath}" hassas bir yol oldugu icin CALISTIRILMADI.`);
|
|
323
|
+
continue;
|
|
330
324
|
}
|
|
325
|
+
const res = await executeThroughGateway({
|
|
326
|
+
toolName: 'write_file',
|
|
327
|
+
args: { path: targetPath, content: f.content != null ? String(f.content) : '' },
|
|
328
|
+
resolveTool: () => wf,
|
|
329
|
+
normalizeSuccess: value => value,
|
|
330
|
+
normalizeError: error => ({ success: false, error }),
|
|
331
|
+
allowSensitivePaths: !!opts.execFull,
|
|
332
|
+
});
|
|
333
|
+
const ok = res && res.success !== false;
|
|
334
|
+
records.push({ tool: 'write_file', status: ok ? 'done' : 'error', args: { path: targetPath }, result: res, error: ok ? undefined : (res && res.error) });
|
|
335
|
+
feedbacks.push(ok ? `write_file OK: ${res.path || targetPath} (${res.size != null ? res.size : '?'} bytes)` : `write_file HATA: ${res && res.error}`);
|
|
331
336
|
}
|
|
332
337
|
return { records, feedback: feedbacks.join('\n') };
|
|
333
338
|
}
|
|
@@ -384,17 +389,20 @@ async function executeCall(call, opts = {}) {
|
|
|
384
389
|
records.push({ tool: norm, status: 'error', error: 'execute yok' });
|
|
385
390
|
return { records, feedback: `${norm} HATA: execute fonksiyonu yok` };
|
|
386
391
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
392
|
+
const res = await executeThroughGateway({
|
|
393
|
+
toolName: norm,
|
|
394
|
+
args,
|
|
395
|
+
resolveTool: () => mod,
|
|
396
|
+
execute: fn,
|
|
397
|
+
normalizeSuccess: value => value,
|
|
398
|
+
normalizeError: error => ({ success: false, error }),
|
|
399
|
+
allowSensitivePaths: !!opts.execFull,
|
|
400
|
+
});
|
|
401
|
+
const ok = typeof res === 'string' ? true : (res && res.success !== false);
|
|
402
|
+
const status = ok ? 'done' : 'error';
|
|
403
|
+
const feedback = buildFeedback(norm, res);
|
|
404
|
+
records.push({ tool: norm, status, args: sanitizeArgs(args), result: res, error: ok ? undefined : res.error });
|
|
405
|
+
return { records, feedback };
|
|
398
406
|
}
|
|
399
407
|
|
|
400
408
|
/**
|
|
@@ -9,6 +9,7 @@ const fs = require("fs");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const os = require("os");
|
|
11
11
|
const { writeJsonAtomicSync, readJsonSafeSync } = require("../utils/atomic-file");
|
|
12
|
+
const { createMemoryRecord, resolveConflict, factKey } = require("../utils/memory-record");
|
|
12
13
|
|
|
13
14
|
const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
|
|
14
15
|
|
|
@@ -131,7 +132,7 @@ function verifyMemoryWrite(username, expectedFact, expectedBotName) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
|
|
134
|
-
function addMemory({ username, fact, score = 5, category = "general", botName, nickname, name }) {
|
|
135
|
+
function addMemory({ username, fact, score = 5, category = "general", botName, nickname, name, source = "tool", confidence = 0.5, ttlMs, userConfirmed = false }) {
|
|
135
136
|
// Username yoksa ve 'name' parametresi varsa, onu username olarak kullan
|
|
136
137
|
// (hitap bicimi icin)
|
|
137
138
|
const effectiveUsername = username || (name && name.toLowerCase()) || 'default';
|
|
@@ -149,18 +150,17 @@ function addMemory({ username, fact, score = 5, category = "general", botName, n
|
|
|
149
150
|
|
|
150
151
|
if (fact) {
|
|
151
152
|
// duplicate kontrol
|
|
152
|
-
const
|
|
153
|
+
const incoming = createMemoryRecord({ value: fact, score, category, source, confidence, ttlMs, userConfirmed, verified: true });
|
|
154
|
+
const key = factKey(fact, category);
|
|
155
|
+
const existing = memory.facts.find(f => factKey(f.value || f, f.category || category) === key);
|
|
153
156
|
if (existing) {
|
|
154
|
-
|
|
155
|
-
existing.
|
|
157
|
+
const sameValue = String(existing.value || existing).toLowerCase() === fact.toLowerCase();
|
|
158
|
+
const previousScore = existing.score || 5;
|
|
159
|
+
const resolved = resolveConflict(existing, incoming);
|
|
160
|
+
Object.assign(existing, resolved.winner);
|
|
161
|
+
if (sameValue) existing.score = Math.min(10, previousScore + 2);
|
|
156
162
|
} else {
|
|
157
|
-
memory.facts.push(
|
|
158
|
-
value: fact,
|
|
159
|
-
score,
|
|
160
|
-
category,
|
|
161
|
-
updatedAt: new Date().toISOString(),
|
|
162
|
-
createdAt: new Date().toISOString(),
|
|
163
|
-
});
|
|
163
|
+
memory.facts.push(incoming);
|
|
164
164
|
}
|
|
165
165
|
// Limit'i ZIM push'tan sonra uygula → yeni eklenen fact düşürülmez.
|
|
166
166
|
memory = enforceFactLimit(memory, { recentValue: fact });
|
|
@@ -236,6 +236,10 @@ module.exports = {
|
|
|
236
236
|
fact: { type: "string", description: 'Yeni fact (ornek: "Kullanici kahve seviyor", "Istanbul\'da yasiyor")' },
|
|
237
237
|
score: { type: "number", description: "Onem derecesi 1-10 (default 5)" },
|
|
238
238
|
category: { type: "string", description: "Kategori: personal, preference, work, hobby, fact (default general)" },
|
|
239
|
+
source: { type: "string", description: "Bilginin kaynağı (user, tool, import, inference)" },
|
|
240
|
+
confidence: { type: "number", description: "Güven puanı 0-1" },
|
|
241
|
+
ttlMs: { type: "number", description: "İsteğe bağlı yaşam süresi (milisaniye)" },
|
|
242
|
+
userConfirmed: { type: "boolean", description: "Kullanıcı tarafından açıkça doğrulandı mı" },
|
|
239
243
|
botName: { type: "string", description: "Bot adini degistir (memory.botName)" },
|
|
240
244
|
nickname: { type: "string", description: "Kullanici nickname'i" },
|
|
241
245
|
name: { type: "string", description: "Kullanici gercek adi" },
|
|
@@ -249,4 +253,4 @@ module.exports = {
|
|
|
249
253
|
if (action === "show") return showMemory(params);
|
|
250
254
|
return addMemory(params);
|
|
251
255
|
},
|
|
252
|
-
};
|
|
256
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { StructuralPatchEngine } = require('../utils/structural-patch');
|
|
2
|
+
const engine = new StructuralPatchEngine();
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
name: 'structural_patch',
|
|
6
|
+
description: 'Apply conflict-safe anchored text patches with preview and rollback support.',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object', required: ['action'],
|
|
9
|
+
properties: {
|
|
10
|
+
action: { type: 'string', enum: ['preview', 'apply', 'rollback'] },
|
|
11
|
+
path: { type: 'string' }, expectedHash: { type: 'string' }, patchId: { type: 'string' }, dryRun: { type: 'boolean' },
|
|
12
|
+
operations: { type: 'array', items: { type: 'object', required: ['search'], properties: {
|
|
13
|
+
search: { type: 'string' }, replace: { type: 'string' }, replaceAll: { type: 'boolean' },
|
|
14
|
+
} } },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async execute(params) {
|
|
18
|
+
if (params.action === 'rollback') return engine.rollback(params.patchId);
|
|
19
|
+
if (!params.path) return { success: false, error: 'path is required' };
|
|
20
|
+
const result = params.action === 'preview'
|
|
21
|
+
? engine.preview(params.path, params.operations, { expectedHash: params.expectedHash, dryRun: true })
|
|
22
|
+
: engine.apply(params.path, params.operations, { expectedHash: params.expectedHash, dryRun: params.dryRun });
|
|
23
|
+
return result.ok ? { success: true, ...result } : { success: false, ...result };
|
|
24
|
+
},
|
|
25
|
+
};
|
package/src/tools/workflow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { executeThroughGateway } = require('../utils/tool-execution-gateway');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
|
|
6
7
|
const WORKFLOW_DIR = path.join(os.homedir(), '.natureco', 'workflows');
|
|
@@ -517,7 +518,14 @@ async function workflow(params) {
|
|
|
517
518
|
const toolMod = require(path.join(__dirname, '..', 'tools', step.tool + '.js'));
|
|
518
519
|
const fn = toolMod.execute || (toolMod.default && toolMod.default.execute);
|
|
519
520
|
if (!fn) { throw new Error(step.tool + ' toolunda execute fonksiyonu bulunamadi'); }
|
|
520
|
-
const toolResult = await
|
|
521
|
+
const toolResult = await executeThroughGateway({
|
|
522
|
+
toolName: step.tool,
|
|
523
|
+
args,
|
|
524
|
+
resolveTool: () => toolMod,
|
|
525
|
+
execute: fn,
|
|
526
|
+
normalizeSuccess: value => value,
|
|
527
|
+
normalizeError: error => ({ success: false, error }),
|
|
528
|
+
});
|
|
521
529
|
stepResults.push({ step: step.step, tool: step.tool, status: 'done', args, result: toolResult });
|
|
522
530
|
} else if (msg.content) {
|
|
523
531
|
stepResults.push({ step: step.step, tool: step.tool, status: 'done', note: 'Tool cagrilmadi, model dogrudan yanit verdi', content: msg.content.slice(0, 500) });
|