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
@@ -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 s = String(p || '');
80
- const pats = mode === 'write' ? SENSITIVE_WRITE : SENSITIVE_READ;
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
- try {
323
- const res = await wf.execute({ path: expandHome(f.path), content: f.content != null ? String(f.content) : '' });
324
- const ok = res && res.success !== false;
325
- records.push({ tool: 'write_file', status: ok ? 'done' : 'error', args: { path: f.path }, result: res, error: ok ? undefined : (res && res.error) });
326
- feedbacks.push(ok ? `write_file OK: ${res.path || f.path} (${res.size != null ? res.size : '?'} bytes)` : `write_file HATA: ${res && res.error}`);
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
- try {
388
- const res = await fn(args);
389
- const ok = typeof res === 'string' ? true : (res && res.success !== false);
390
- const status = ok ? 'done' : 'error';
391
- const feedback = buildFeedback(norm, res);
392
- records.push({ tool: norm, status, args: sanitizeArgs(args), result: res });
393
- return { records, feedback };
394
- } catch (e) {
395
- records.push({ tool: norm, status: 'error', args: sanitizeArgs(args), error: e.message });
396
- return { records, feedback: `${norm} HATA: ${e.message}` };
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 existing = memory.facts.find(f => (f.value || f).toLowerCase() === fact.toLowerCase());
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
- existing.score = Math.min(10, (existing.score || 5) + 2);
155
- existing.updatedAt = new Date().toISOString();
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
+ };
@@ -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 fn(args);
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) });
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { ToolGuardrails } = require('./tool-guardrails');
4
+ const { standardToolResult } = require('./tool-result');
5
+
6
+ class AgentCore {
7
+ constructor(options = {}) {
8
+ this.guardrails = options.guardrails || new ToolGuardrails({ hardStopEnabled: true });
9
+ this.maxIterations = options.maxIterations || 10;
10
+ this.iteration = 0;
11
+ }
12
+
13
+ startRequest() {
14
+ this.iteration = 0;
15
+ this.guardrails.reset();
16
+ }
17
+
18
+ startIteration() {
19
+ this.iteration++;
20
+ this.guardrails.startIteration();
21
+ return { iteration: this.iteration, maxIterations: this.maxIterations, allowed: this.iteration <= this.maxIterations };
22
+ }
23
+
24
+ parseToolCalls(toolCalls = []) {
25
+ return toolCalls.map((call, index) => {
26
+ const name = call.function?.name || call.name;
27
+ const raw = call.function?.arguments ?? call.input ?? call.args ?? {};
28
+ let input = raw;
29
+ let parseError = null;
30
+ if (typeof raw === 'string') {
31
+ try { input = JSON.parse(raw || '{}'); }
32
+ catch (error) { input = {}; parseError = error.message; }
33
+ }
34
+ return { id: call.id || `call_${this.iteration}_${index}`, name, input, parseError, original: call };
35
+ });
36
+ }
37
+
38
+ assess(call) {
39
+ if (!call?.name) return { blocked: true, reason: 'Araç adı eksik' };
40
+ if (call.parseError) return { blocked: true, reason: `Araç argümanları JSON değil: ${call.parseError}` };
41
+ return this.guardrails.check(call.name, call.input || {});
42
+ }
43
+
44
+ record(call, result) {
45
+ const standard = standardToolResult(result, { tool: call.name, iteration: this.iteration });
46
+ this.guardrails.record(call.name, call.input || {}, standard.ok);
47
+ return standard;
48
+ }
49
+ }
50
+
51
+ module.exports = { AgentCore };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { Worktree } = require('./worktree');
4
+
5
+ class AgentWorkspaceManager {
6
+ constructor(options = {}) { this.createWorktree = options.createWorktree || (() => new Worktree()); }
7
+
8
+ async run(agentId, task, options = {}) {
9
+ const worktree = this.createWorktree();
10
+ if (options.gitRepo !== undefined) worktree._mockGitRepo = options.gitRepo;
11
+ const entered = worktree.enter({ id: `agent-${agentId}` });
12
+ if (entered.error) throw new Error(entered.error);
13
+ try {
14
+ const result = await task({ id: agentId, cwd: entered.worktreeDir, worktree });
15
+ const exited = worktree.exit({ merge: options.merge === true });
16
+ return { ok: true, result, workspace: entered.worktreeDir, merged: !!exited.merged };
17
+ } catch (error) {
18
+ try { worktree.exit({ merge: false }); } catch {}
19
+ return { ok: false, error: error.message, workspace: entered.worktreeDir, merged: false };
20
+ }
21
+ }
22
+ }
23
+
24
+ module.exports = { AgentWorkspaceManager };
package/src/utils/api.js CHANGED
@@ -9,9 +9,8 @@ const { getConfig } = require('./config');
9
9
  const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
10
10
  const { MCPClient } = require('./mcp-client');
11
11
  const TB = require('./token-budget');
12
- const { accumulateToolCallDeltas } = require('./streaming-tools');
13
- const { ToolGuardrails } = require('./tool-guardrails');
14
- const guardrails = new ToolGuardrails();
12
+ const { accumulateToolCallDeltas } = require('./streaming-tools');
13
+ const { AgentCore } = require('./agent-core');
15
14
 
16
15
  /**
17
16
  * v5.5.0: Provider-specific format detection
@@ -671,7 +670,12 @@ async function sendMessageAnthropic(providerConfig, messages, tools) {
671
670
  /**
672
671
  * Send message with tool support (universal)
673
672
  */
674
- async function sendMessageToProvider(apiKey, message, conversationId = null, systemPrompt = null, options = {}) {
673
+ async function sendMessageToProvider(apiKey, message, conversationId = null, systemPrompt = null, options = {}) {
674
+ // Per-request state prevents concurrent conversations from contaminating one
675
+ // another while preserving failure/no-progress counters across tool rounds.
676
+ const agentCore = new AgentCore({ maxIterations: 10 });
677
+ agentCore.startRequest();
678
+ const guardrails = agentCore.guardrails;
675
679
  const providerConfig = getProviderConfig();
676
680
 
677
681
  if (!providerConfig) {
@@ -784,8 +788,7 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
784
788
  const localCalls = toolCalls.filter(tc => !mcpTools.find(t => t.name === tc.name));
785
789
 
786
790
  // Guardrails: filter blocked tools
787
- guardrails.reset();
788
- guardrails.startIteration();
791
+ agentCore.startIteration();
789
792
  const blockedMcp = mcpCalls.filter(tc => {
790
793
  const check = guardrails.check(tc.name, tc.input);
791
794
  return check.blocked;
@@ -803,8 +806,9 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
803
806
  localCalls.filter(tc => !blockedLocal.includes(tc)),
804
807
  { toolDefinitions: getToolDefinitions() }
805
808
  );
806
- for (const r of localResults) {
807
- guardrails.record(r.name, {}, r.result?.success !== false);
809
+ for (const r of localResults) {
810
+ const original = localCalls.find(tc => tc.id === r.id);
811
+ guardrails.record(r.name, original?.input || {}, r.result?.success !== false && !r.result?.error);
808
812
  }
809
813
  }
810
814
 
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { EventEmitter } = require('events');
5
+
6
+ function deliveryId(channel, target, payload, explicitKey) {
7
+ if (explicitKey) return String(explicitKey);
8
+ return crypto.createHash('sha256').update(`${channel}\0${target}\0${JSON.stringify(payload)}`).digest('hex').slice(0, 24);
9
+ }
10
+
11
+ class ChannelAdapter extends EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+ if (!options.name) throw new Error('channel adapter name is required');
15
+ this.name = options.name;
16
+ this.connectFn = options.connect;
17
+ this.disconnectFn = options.disconnect;
18
+ this.sendFn = options.send;
19
+ this.healthFn = options.health;
20
+ this.state = 'disconnected';
21
+ this.lastError = null;
22
+ this.connectedAt = null;
23
+ }
24
+
25
+ async connect() {
26
+ if (this.state === 'connected') return { ok: true, reused: true };
27
+ this.state = 'connecting';
28
+ try {
29
+ if (this.connectFn) await this.connectFn();
30
+ this.state = 'connected'; this.connectedAt = new Date().toISOString(); this.lastError = null;
31
+ this.emit('state', this.state);
32
+ return { ok: true };
33
+ } catch (error) {
34
+ this.state = 'degraded'; this.lastError = error.message; this.emit('state', this.state);
35
+ return { ok: false, error: error.message };
36
+ }
37
+ }
38
+
39
+ async disconnect() {
40
+ try { if (this.disconnectFn) await this.disconnectFn(); } finally { this.state = 'disconnected'; this.emit('state', this.state); }
41
+ }
42
+
43
+ async send(delivery) {
44
+ if (typeof this.sendFn !== 'function') throw new Error(`channel ${this.name} cannot send`);
45
+ if (this.state !== 'connected') {
46
+ const connected = await this.connect();
47
+ if (!connected.ok) throw new Error(connected.error);
48
+ }
49
+ return this.sendFn(delivery);
50
+ }
51
+
52
+ async health() {
53
+ try {
54
+ const detail = this.healthFn ? await this.healthFn() : { ok: this.state === 'connected' };
55
+ return { channel: this.name, state: this.state, connectedAt: this.connectedAt, lastError: this.lastError, ...detail };
56
+ } catch (error) { return { channel: this.name, state: 'degraded', ok: false, error: error.message }; }
57
+ }
58
+ }
59
+
60
+ class ChannelDeliveryManager extends EventEmitter {
61
+ constructor(options = {}) {
62
+ super();
63
+ this.adapters = new Map();
64
+ this.queue = [];
65
+ this.completed = new Map();
66
+ this.inFlight = new Map();
67
+ this.maxAttempts = options.maxAttempts || 4;
68
+ this.baseDelayMs = options.baseDelayMs ?? 250;
69
+ this.maxCompleted = options.maxCompleted || 10000;
70
+ this.sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
71
+ this.store = options.store || null;
72
+ this.deadLetters = [];
73
+ this.metrics = { enqueued: 0, delivered: 0, failed: 0, retried: 0, deduplicated: 0, byChannel: {} };
74
+ if (this.store) {
75
+ const persisted = this.store.load();
76
+ this.queue = persisted.queue || [];
77
+ this.deadLetters = persisted.deadLetters || [];
78
+ }
79
+ }
80
+
81
+ register(adapter) { if (!(adapter instanceof ChannelAdapter)) throw new Error('ChannelAdapter required'); this.adapters.set(adapter.name, adapter); return adapter; }
82
+
83
+ enqueue(channel, target, payload, options = {}) {
84
+ const id = deliveryId(channel, target, payload, options.idempotencyKey);
85
+ if (this.completed.has(id) || this.inFlight.has(id) || this.queue.some(item => item.id === id)) {
86
+ this.metrics.deduplicated++; return { ok: true, id, duplicate: true };
87
+ }
88
+ const item = { id, channel, target, payload, attempt: 0, createdAt: new Date().toISOString(), metadata: options.metadata || {} };
89
+ this.queue.push(item); this.metrics.enqueued++; this._channel(channel).enqueued++;
90
+ this._persist();
91
+ this.emit('queued', item);
92
+ return { ok: true, id, duplicate: false };
93
+ }
94
+
95
+ async drain() {
96
+ const results = [];
97
+ while (this.queue.length) {
98
+ const item = this.queue.shift();
99
+ this._persist();
100
+ results.push(await this._deliver(item));
101
+ }
102
+ return results;
103
+ }
104
+
105
+ async _deliver(item) {
106
+ const adapter = this.adapters.get(item.channel);
107
+ if (!adapter) return this._failure(item, `channel adapter not registered: ${item.channel}`);
108
+ this.inFlight.set(item.id, item);
109
+ while (item.attempt < this.maxAttempts) {
110
+ item.attempt++;
111
+ try {
112
+ const result = await adapter.send(item);
113
+ const record = { ok: true, id: item.id, attempts: item.attempt, result, deliveredAt: new Date().toISOString() };
114
+ this.inFlight.delete(item.id); this.completed.set(item.id, record);
115
+ while (this.completed.size > this.maxCompleted) this.completed.delete(this.completed.keys().next().value);
116
+ this.metrics.delivered++; this._channel(item.channel).delivered++; this.emit('delivered', record);
117
+ this._persist();
118
+ return record;
119
+ } catch (error) {
120
+ item.lastError = error.message;
121
+ if (item.attempt < this.maxAttempts) {
122
+ this.metrics.retried++; this._channel(item.channel).retried++;
123
+ await this.sleep(Math.min(this.baseDelayMs * (2 ** (item.attempt - 1)), 30000));
124
+ }
125
+ }
126
+ }
127
+ this.inFlight.delete(item.id);
128
+ return this._failure(item, item.lastError || 'delivery failed');
129
+ }
130
+
131
+ _failure(item, error) {
132
+ const record = { ok: false, id: item.id, attempts: item.attempt, error };
133
+ this.deadLetters.push({ ...item, failedAt: new Date().toISOString(), error });
134
+ if (this.deadLetters.length > 10000) this.deadLetters.shift();
135
+ this.metrics.failed++; this._channel(item.channel).failed++; this.emit('failed', record); this._persist(); return record;
136
+ }
137
+
138
+ _channel(name) {
139
+ if (!this.metrics.byChannel[name]) this.metrics.byChannel[name] = { enqueued: 0, delivered: 0, failed: 0, retried: 0 };
140
+ return this.metrics.byChannel[name];
141
+ }
142
+
143
+ snapshotMetrics() { return JSON.parse(JSON.stringify({ ...this.metrics, queueDepth: this.queue.length, inFlight: this.inFlight.size })); }
144
+ async health() { return Promise.all([...this.adapters.values()].map(adapter => adapter.health())); }
145
+ _persist() { if (this.store) this.store.save({ queue: this.queue, deadLetters: this.deadLetters }); }
146
+ requeueDeadLetter(id) {
147
+ const index = this.deadLetters.findIndex(item => item.id === id);
148
+ if (index === -1) return { ok: false, error: 'dead letter not found' };
149
+ const [item] = this.deadLetters.splice(index, 1);
150
+ item.attempt = 0; delete item.error; delete item.failedAt;
151
+ this.queue.push(item); this._persist();
152
+ return { ok: true, id };
153
+ }
154
+ }
155
+
156
+ class ReconnectSupervisor extends EventEmitter {
157
+ constructor(options = {}) {
158
+ super();
159
+ this.baseDelayMs = options.baseDelayMs ?? 1000;
160
+ this.maxDelayMs = options.maxDelayMs ?? 60000;
161
+ this.maxAttempts = options.maxAttempts ?? Infinity;
162
+ this.jitter = options.jitter ?? 0.2;
163
+ this.sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
164
+ this.random = options.random || Math.random;
165
+ this.jobs = new Map();
166
+ this.metrics = { attempts: 0, reconnected: 0, exhausted: 0, cancelled: 0 };
167
+ }
168
+
169
+ reconnect(adapter) {
170
+ if (!(adapter instanceof ChannelAdapter)) return Promise.reject(new Error('ChannelAdapter required'));
171
+ if (this.jobs.has(adapter.name)) return this.jobs.get(adapter.name).promise;
172
+ const job = { cancelled: false, attempt: 0, promise: null };
173
+ job.promise = this._run(adapter, job).finally(() => this.jobs.delete(adapter.name));
174
+ this.jobs.set(adapter.name, job);
175
+ return job.promise;
176
+ }
177
+
178
+ cancel(name) {
179
+ const job = this.jobs.get(name);
180
+ if (!job) return false;
181
+ job.cancelled = true; this.metrics.cancelled++; this.emit('cancelled', { channel: name }); return true;
182
+ }
183
+
184
+ async _run(adapter, job) {
185
+ while (!job.cancelled && job.attempt < this.maxAttempts) {
186
+ job.attempt++; this.metrics.attempts++;
187
+ if (job.attempt > 1) await this.sleep(this._delay(job.attempt - 1));
188
+ if (job.cancelled) break;
189
+ const result = await adapter.connect();
190
+ this.emit('attempt', { channel: adapter.name, attempt: job.attempt, result });
191
+ if (result.ok) {
192
+ this.metrics.reconnected++; this.emit('reconnected', { channel: adapter.name, attempts: job.attempt });
193
+ return { ok: true, channel: adapter.name, attempts: job.attempt };
194
+ }
195
+ // connect() leaves degraded state; reset so the next attempt calls connectFn.
196
+ adapter.state = 'disconnected';
197
+ }
198
+ if (job.cancelled) return { ok: false, channel: adapter.name, stopped: 'cancelled', attempts: job.attempt };
199
+ this.metrics.exhausted++; this.emit('exhausted', { channel: adapter.name, attempts: job.attempt });
200
+ return { ok: false, channel: adapter.name, stopped: 'exhausted', attempts: job.attempt };
201
+ }
202
+
203
+ _delay(attempt) {
204
+ const raw = Math.min(this.baseDelayMs * (2 ** Math.max(0, attempt - 1)), this.maxDelayMs);
205
+ const spread = raw * this.jitter;
206
+ return Math.max(0, Math.round(raw - spread + this.random() * spread * 2));
207
+ }
208
+
209
+ snapshotMetrics() { return { ...this.metrics, active: this.jobs.size }; }
210
+ }
211
+
212
+ module.exports = { ChannelAdapter, ChannelDeliveryManager, ReconnectSupervisor, deliveryId };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const CODE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java']);
7
+ const DEFINITION_PATTERNS = [
8
+ /\b(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g,
9
+ /^\s*def\s+([A-Za-z_]\w*)/gm,
10
+ /^\s*(?:func|type)\s+([A-Za-z_]\w*)/gm,
11
+ ];
12
+
13
+ function walkCodeFiles(root, limit = 5000) {
14
+ const files = [];
15
+ const ignored = new Set(['node_modules', '.git', '.natureco', 'dist', 'build', 'coverage']);
16
+ function walk(dir) {
17
+ if (files.length >= limit) return;
18
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
19
+ if (ignored.has(entry.name)) continue;
20
+ const target = path.join(dir, entry.name);
21
+ if (entry.isDirectory()) walk(target);
22
+ else if (entry.isFile() && CODE_EXTENSIONS.has(path.extname(entry.name))) files.push(target);
23
+ }
24
+ }
25
+ walk(path.resolve(root));
26
+ return files;
27
+ }
28
+
29
+ class CodeIntelligence {
30
+ constructor(root = process.cwd()) { this.root = path.resolve(root); this.files = []; this.definitions = new Map(); }
31
+
32
+ index() {
33
+ this.files = walkCodeFiles(this.root);
34
+ this.definitions.clear();
35
+ for (const file of this.files) {
36
+ const text = fs.readFileSync(file, 'utf8');
37
+ for (const pattern of DEFINITION_PATTERNS) {
38
+ pattern.lastIndex = 0;
39
+ let match;
40
+ while ((match = pattern.exec(text))) {
41
+ const line = text.slice(0, match.index).split('\n').length;
42
+ const item = { symbol: match[1], file, line };
43
+ if (!this.definitions.has(match[1])) this.definitions.set(match[1], []);
44
+ this.definitions.get(match[1]).push(item);
45
+ }
46
+ }
47
+ }
48
+ return { files: this.files.length, symbols: this.definitions.size };
49
+ }
50
+
51
+ findDefinitions(symbol) { return this.definitions.get(symbol) || []; }
52
+
53
+ findReferences(symbol, limit = 200) {
54
+ if (!symbol || !/^[A-Za-z_$][\w$]*$/.test(symbol)) return [];
55
+ const regex = new RegExp(`\\b${symbol.replace(/[$]/g, '\\$&')}\\b`, 'g');
56
+ const results = [];
57
+ for (const file of this.files) {
58
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
59
+ lines.forEach((text, index) => {
60
+ regex.lastIndex = 0;
61
+ if (regex.test(text)) results.push({ symbol, file, line: index + 1, text: text.trim().slice(0, 240) });
62
+ });
63
+ if (results.length >= limit) break;
64
+ }
65
+ return results.slice(0, limit);
66
+ }
67
+ }
68
+
69
+ module.exports = { CodeIntelligence, walkCodeFiles };