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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const TB = require('./token-budget');
|
|
6
|
+
const { writeFileAtomicSync } = require('./atomic-file');
|
|
7
|
+
|
|
8
|
+
class CodingSession {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.maxSnapshots = options.maxSnapshots || 20;
|
|
11
|
+
this.snapshots = [];
|
|
12
|
+
this.lastUserMessage = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
rememberUserMessage(message) { if (message && !message.startsWith('/')) this.lastUserMessage = message; }
|
|
16
|
+
retryMessage() { return this.lastUserMessage; }
|
|
17
|
+
|
|
18
|
+
capture(filePath) {
|
|
19
|
+
if (!filePath) return null;
|
|
20
|
+
const target = path.resolve(filePath);
|
|
21
|
+
const existed = fs.existsSync(target);
|
|
22
|
+
const content = existed ? fs.readFileSync(target) : null;
|
|
23
|
+
const snapshot = { path: target, existed, content, capturedAt: Date.now() };
|
|
24
|
+
this.snapshots.push(snapshot);
|
|
25
|
+
if (this.snapshots.length > this.maxSnapshots) this.snapshots.shift();
|
|
26
|
+
return snapshot;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
undo() {
|
|
30
|
+
const snapshot = this.snapshots.pop();
|
|
31
|
+
if (!snapshot) return { ok: false, error: 'Geri alınacak değişiklik yok.' };
|
|
32
|
+
if (snapshot.existed) writeFileAtomicSync(snapshot.path, snapshot.content);
|
|
33
|
+
else if (fs.existsSync(snapshot.path)) fs.unlinkSync(snapshot.path);
|
|
34
|
+
return { ok: true, path: snapshot.path, restored: snapshot.existed };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
compact(messages) {
|
|
38
|
+
const before = messages.length;
|
|
39
|
+
const compacted = TB.smartTrim(messages);
|
|
40
|
+
return { messages: compacted, before, after: compacted.length, removed: before - compacted.length };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
riskSummary(toolCall) {
|
|
44
|
+
const name = toolCall?.name || '';
|
|
45
|
+
const args = toolCall?.input || {};
|
|
46
|
+
const risks = [];
|
|
47
|
+
if (name === 'write_file' || name === 'edit_file') risks.push('filesystem-write');
|
|
48
|
+
if (name === 'bash' || name === 'shell_command') risks.push('command-execution');
|
|
49
|
+
if (/\b(rm|delete|drop|truncate|chmod|chown)\b/i.test(args.command || '')) risks.push('destructive');
|
|
50
|
+
return { level: risks.includes('destructive') ? 'high' : risks.length ? 'medium' : 'low', risks };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { CodingSession };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function estimateTokens(content) {
|
|
4
|
+
if (!content) return 0;
|
|
5
|
+
return Math.ceil(String(content).length / 4);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function prepareConversationHistory(messages, options = {}) {
|
|
9
|
+
if (!Array.isArray(messages)) return [];
|
|
10
|
+
|
|
11
|
+
if (typeof options === 'number') options = { maxMessages: options };
|
|
12
|
+
const maxMessages = Math.max(1, options.maxMessages || 20);
|
|
13
|
+
const maxTokens = Math.max(1, options.maxTokens || 2048);
|
|
14
|
+
|
|
15
|
+
const candidates = messages
|
|
16
|
+
.filter(message =>
|
|
17
|
+
message &&
|
|
18
|
+
(message.role === 'user' || message.role === 'assistant') &&
|
|
19
|
+
typeof message.content === 'string' &&
|
|
20
|
+
message.content.trim()
|
|
21
|
+
)
|
|
22
|
+
.slice(-maxMessages);
|
|
23
|
+
|
|
24
|
+
const selected = [];
|
|
25
|
+
let remainingTokens = maxTokens;
|
|
26
|
+
|
|
27
|
+
for (let i = candidates.length - 1; i >= 0 && remainingTokens > 0; i--) {
|
|
28
|
+
const message = candidates[i];
|
|
29
|
+
const content = message.content.trim();
|
|
30
|
+
const contentTokens = estimateTokens(content);
|
|
31
|
+
if (contentTokens <= remainingTokens) {
|
|
32
|
+
selected.unshift({ role: message.role, content });
|
|
33
|
+
remainingTokens -= contentTokens;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Preserve the newest oversized turn, but never let a generated file or
|
|
38
|
+
// verbose tool summary consume the entire next request's context budget.
|
|
39
|
+
if (selected.length === 0) {
|
|
40
|
+
const maxChars = Math.max(4, remainingTokens * 4);
|
|
41
|
+
selected.unshift({
|
|
42
|
+
role: message.role,
|
|
43
|
+
content: content.slice(0, Math.max(0, maxChars - 36)) + '\n[... context truncated ...]',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return selected;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { estimateTokens, prepareConversationHistory };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { writeJsonAtomicSync, readJsonSafeSync } = require('./atomic-file');
|
|
7
|
+
|
|
8
|
+
class DeliveryStore {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.file = options.file || path.join(os.homedir(), '.natureco', 'delivery-queue.json');
|
|
11
|
+
this.maxItems = options.maxItems || 10000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
load() {
|
|
15
|
+
const data = readJsonSafeSync(this.file, { version: 1, queue: [], deadLetters: [] });
|
|
16
|
+
return {
|
|
17
|
+
version: 1,
|
|
18
|
+
queue: Array.isArray(data.queue) ? data.queue.slice(0, this.maxItems) : [],
|
|
19
|
+
deadLetters: Array.isArray(data.deadLetters) ? data.deadLetters.slice(-this.maxItems) : [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
save(state) {
|
|
24
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 });
|
|
25
|
+
writeJsonAtomicSync(this.file, {
|
|
26
|
+
version: 1,
|
|
27
|
+
savedAt: new Date().toISOString(),
|
|
28
|
+
queue: (state.queue || []).slice(0, this.maxItems),
|
|
29
|
+
deadLetters: (state.deadLetters || []).slice(-this.maxItems),
|
|
30
|
+
}, { mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { DeliveryStore };
|
package/src/utils/i18n.js
CHANGED
|
@@ -92,4 +92,10 @@ function t(key, vars) {
|
|
|
92
92
|
return s;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
function validateCatalogParity(catalog = MESSAGES) {
|
|
96
|
+
const tr = Object.keys(catalog.tr || {}).sort();
|
|
97
|
+
const en = Object.keys(catalog.en || {}).sort();
|
|
98
|
+
return { ok: tr.length === en.length && tr.every((key, index) => key === en[index]), trOnly: tr.filter(key => !en.includes(key)), enOnly: en.filter(key => !tr.includes(key)), keys: tr.length };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { t, getLang, setLangCache, MESSAGES, validateCatalogParity };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function valueType(value) {
|
|
4
|
+
if (Array.isArray(value)) return 'array';
|
|
5
|
+
if (value === null) return 'null';
|
|
6
|
+
if (Number.isInteger(value)) return 'integer';
|
|
7
|
+
return typeof value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function validateJsonSchema(schema, value, at = '$') {
|
|
11
|
+
const errors = [];
|
|
12
|
+
if (!schema || typeof schema !== 'object') return { valid: true, errors };
|
|
13
|
+
const allowedTypes = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
14
|
+
const actual = valueType(value);
|
|
15
|
+
if (allowedTypes.length && !allowedTypes.includes(actual) && !(actual === 'integer' && allowedTypes.includes('number'))) {
|
|
16
|
+
return { valid: false, errors: [`${at}: expected ${allowedTypes.join('|')}, got ${actual}`] };
|
|
17
|
+
}
|
|
18
|
+
if (schema.enum && !schema.enum.some(item => Object.is(item, value))) {
|
|
19
|
+
errors.push(`${at}: value is not in enum`);
|
|
20
|
+
}
|
|
21
|
+
if (actual === 'object' && value !== null) {
|
|
22
|
+
for (const key of schema.required || []) {
|
|
23
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) errors.push(`${at}.${key}: required`);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, child] of Object.entries(schema.properties || {})) {
|
|
26
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
27
|
+
errors.push(...validateJsonSchema(child, value[key], `${at}.${key}`).errors);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (schema.additionalProperties === false) {
|
|
31
|
+
const known = new Set(Object.keys(schema.properties || {}));
|
|
32
|
+
for (const key of Object.keys(value)) if (!known.has(key)) errors.push(`${at}.${key}: additional property`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (actual === 'array') {
|
|
36
|
+
if (schema.minItems != null && value.length < schema.minItems) errors.push(`${at}: minItems ${schema.minItems}`);
|
|
37
|
+
if (schema.maxItems != null && value.length > schema.maxItems) errors.push(`${at}: maxItems ${schema.maxItems}`);
|
|
38
|
+
if (schema.items) value.forEach((item, index) => errors.push(...validateJsonSchema(schema.items, item, `${at}[${index}]`).errors));
|
|
39
|
+
}
|
|
40
|
+
return { valid: errors.length === 0, errors };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { validateJsonSchema };
|