natureco-cli 5.62.0 → 5.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/bin/natureco.js +10 -3
- package/package.json +8 -5
- package/scripts/benchmark-startup.js +26 -0
- package/src/commands/backup.js +3 -3
- package/src/commands/chat.js +17 -17
- package/src/commands/code.js +49 -14
- package/src/commands/code_v5.js +15 -1
- package/src/commands/gateway-server.js +128 -24
- package/src/commands/git.js +2 -2
- package/src/commands/pairing.js +2 -22
- package/src/commands/repl.js +118 -112
- package/src/tools/agentic-runner.js +41 -33
- package/src/tools/memory_write.js +16 -12
- package/src/tools/structural_patch.js +25 -0
- package/src/tools/workflow.js +10 -2
- package/src/utils/agent-core.js +51 -0
- package/src/utils/agent-workspace.js +24 -0
- package/src/utils/api.js +12 -8
- package/src/utils/channel-sdk.js +212 -0
- package/src/utils/code-intelligence.js +69 -0
- package/src/utils/coding-session.js +54 -0
- package/src/utils/delivery-store.js +34 -0
- package/src/utils/i18n.js +7 -1
- package/src/utils/json-schema.js +43 -0
- package/src/utils/lsp-client.js +129 -0
- package/src/utils/memory-record.js +49 -0
- package/src/utils/pairing-store.js +55 -0
- package/src/utils/pattern-detector.js +13 -3
- package/src/utils/plugin-registry.js +3 -3
- package/src/utils/process-errors.js +14 -7
- package/src/utils/runtime-health.js +28 -0
- package/src/utils/secret-store.js +90 -0
- package/src/utils/secure-sync.js +63 -0
- package/src/utils/skill-lifecycle.js +59 -0
- package/src/utils/structural-patch.js +68 -0
- package/src/utils/sub-agent.js +13 -2
- package/src/utils/test-failure-analyzer.js +64 -0
- package/src/utils/tool-execution-gateway.js +56 -0
- package/src/utils/tool-manifest.js +31 -0
- package/src/utils/tool-path-policy.js +49 -0
- package/src/utils/tool-result.js +17 -0
- package/src/utils/tool-runner.js +81 -53
- package/src/utils/tools.js +30 -42
|
@@ -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,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 };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const { EventEmitter } = require('events');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { pathToFileURL } = require('url');
|
|
7
|
+
|
|
8
|
+
class LspClient extends EventEmitter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
super();
|
|
11
|
+
this.command = options.command;
|
|
12
|
+
this.args = options.args || [];
|
|
13
|
+
this.cwd = options.cwd || process.cwd();
|
|
14
|
+
this.timeoutMs = options.timeoutMs || 10000;
|
|
15
|
+
this.spawnFn = options.spawnFn || spawn;
|
|
16
|
+
this.process = null;
|
|
17
|
+
this.buffer = Buffer.alloc(0);
|
|
18
|
+
this.nextId = 1;
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start() {
|
|
23
|
+
if (this.process) return this;
|
|
24
|
+
if (!this.command) throw new Error('LSP command is required');
|
|
25
|
+
this.process = this.spawnFn(this.command, this.args, {
|
|
26
|
+
cwd: this.cwd, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: false,
|
|
27
|
+
});
|
|
28
|
+
this.process.stdout.on('data', chunk => this._onData(chunk));
|
|
29
|
+
this.process.stderr?.on('data', chunk => this.emit('stderr', chunk.toString()));
|
|
30
|
+
this.process.on('error', error => this._failAll(error));
|
|
31
|
+
this.process.on('exit', code => { this._failAll(new Error(`LSP exited (${code})`)); this.process = null; });
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(capabilities = {}) {
|
|
36
|
+
this.start();
|
|
37
|
+
const rootUri = pathToFileURL(path.resolve(this.cwd)).href;
|
|
38
|
+
const result = await this.request('initialize', {
|
|
39
|
+
processId: process.pid, rootUri, capabilities,
|
|
40
|
+
workspaceFolders: [{ uri: rootUri, name: path.basename(this.cwd) }],
|
|
41
|
+
});
|
|
42
|
+
this.notify('initialized', {});
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
request(method, params, timeoutMs = this.timeoutMs) {
|
|
47
|
+
this.start();
|
|
48
|
+
const id = this.nextId++;
|
|
49
|
+
this._send({ jsonrpc: '2.0', id, method, params });
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
this.pending.delete(id);
|
|
53
|
+
reject(new Error(`LSP request timed out: ${method}`));
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
this.pending.set(id, { resolve, reject, timer, method });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
notify(method, params) { this.start(); this._send({ jsonrpc: '2.0', method, params }); }
|
|
60
|
+
|
|
61
|
+
definition(filePath, line, character) {
|
|
62
|
+
return this.request('textDocument/definition', {
|
|
63
|
+
textDocument: { uri: pathToFileURL(path.resolve(filePath)).href },
|
|
64
|
+
position: { line, character },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
references(filePath, line, character, includeDeclaration = true) {
|
|
69
|
+
return this.request('textDocument/references', {
|
|
70
|
+
textDocument: { uri: pathToFileURL(path.resolve(filePath)).href },
|
|
71
|
+
position: { line, character }, context: { includeDeclaration },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async stop() {
|
|
76
|
+
if (!this.process) return;
|
|
77
|
+
try { await this.request('shutdown', null, 2000); } catch {}
|
|
78
|
+
try { this.notify('exit', null); } catch {}
|
|
79
|
+
this.process.kill();
|
|
80
|
+
this.process = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_send(message) {
|
|
84
|
+
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
85
|
+
this.process.stdin.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_onData(chunk) {
|
|
89
|
+
this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk)]);
|
|
90
|
+
while (true) {
|
|
91
|
+
const headerEnd = this.buffer.indexOf('\r\n\r\n');
|
|
92
|
+
if (headerEnd === -1) return;
|
|
93
|
+
const header = this.buffer.slice(0, headerEnd).toString('ascii');
|
|
94
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
95
|
+
if (!match) { this.buffer = this.buffer.slice(headerEnd + 4); continue; }
|
|
96
|
+
const length = Number(match[1]);
|
|
97
|
+
const bodyStart = headerEnd + 4;
|
|
98
|
+
if (this.buffer.length < bodyStart + length) return;
|
|
99
|
+
const body = this.buffer.slice(bodyStart, bodyStart + length).toString('utf8');
|
|
100
|
+
this.buffer = this.buffer.slice(bodyStart + length);
|
|
101
|
+
try { this._handle(JSON.parse(body)); } catch (error) { this.emit('protocolError', error); }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_handle(message) {
|
|
106
|
+
if (message.id != null && this.pending.has(message.id)) {
|
|
107
|
+
const pending = this.pending.get(message.id);
|
|
108
|
+
clearTimeout(pending.timer);
|
|
109
|
+
this.pending.delete(message.id);
|
|
110
|
+
if (message.error) pending.reject(new Error(message.error.message || 'LSP error'));
|
|
111
|
+
else pending.resolve(message.result);
|
|
112
|
+
} else this.emit('notification', message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_failAll(error) {
|
|
116
|
+
for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); }
|
|
117
|
+
this.pending.clear();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const DEFAULT_SERVERS = {
|
|
122
|
+
javascript: { command: 'typescript-language-server', args: ['--stdio'] },
|
|
123
|
+
typescript: { command: 'typescript-language-server', args: ['--stdio'] },
|
|
124
|
+
python: { command: 'pyright-langserver', args: ['--stdio'] },
|
|
125
|
+
rust: { command: 'rust-analyzer', args: [] },
|
|
126
|
+
go: { command: 'gopls', args: [] },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
module.exports = { LspClient, DEFAULT_SERVERS };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); }
|
|
6
|
+
function factKey(value, category = 'general') {
|
|
7
|
+
const text = String(value || '').trim().toLowerCase();
|
|
8
|
+
const subject = text.includes(':') ? text.split(':', 1)[0] : text.split(/\s+(?:is|=|likes?|prefers?|seviyor|yaşıyor)\s+/i, 1)[0];
|
|
9
|
+
return `${category}:${subject.trim()}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createMemoryRecord(input = {}, now = new Date()) {
|
|
13
|
+
const createdAt = now.toISOString();
|
|
14
|
+
const ttlMs = input.ttlMs == null ? null : Math.max(0, Number(input.ttlMs));
|
|
15
|
+
return {
|
|
16
|
+
id: input.id || `mem_${crypto.randomBytes(8).toString('hex')}`,
|
|
17
|
+
value: String(input.value || '').trim(), category: input.category || 'general',
|
|
18
|
+
score: clamp(Number(input.score ?? 5), 0, 10),
|
|
19
|
+
source: input.source || 'unknown', confidence: clamp(Number(input.confidence ?? 0.5), 0, 1),
|
|
20
|
+
createdAt: input.createdAt || createdAt, updatedAt: createdAt,
|
|
21
|
+
lastVerifiedAt: input.lastVerifiedAt || (input.verified ? createdAt : null),
|
|
22
|
+
expiresAt: ttlMs == null ? null : new Date(now.getTime() + ttlMs).toISOString(),
|
|
23
|
+
status: input.status || 'active', userConfirmed: input.userConfirmed === true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isActive(record, now = new Date()) {
|
|
28
|
+
if (!record || record.status === 'rejected' || record.status === 'superseded') return false;
|
|
29
|
+
return !record.expiresAt || new Date(record.expiresAt).getTime() > now.getTime();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveConflict(existing, incoming) {
|
|
33
|
+
if (!existing) return { winner: incoming, loser: null, reason: 'new' };
|
|
34
|
+
if (existing.value === incoming.value) {
|
|
35
|
+
const merged = { ...existing, ...incoming, id: existing.id, createdAt: existing.createdAt, confidence: Math.max(existing.confidence || 0, incoming.confidence || 0) };
|
|
36
|
+
return { winner: merged, loser: null, reason: 'same-value' };
|
|
37
|
+
}
|
|
38
|
+
const existingWeight = (existing.userConfirmed ? 2 : 0) + (existing.confidence || 0);
|
|
39
|
+
const incomingWeight = (incoming.userConfirmed ? 2 : 0) + (incoming.confidence || 0);
|
|
40
|
+
if (incomingWeight > existingWeight) return { winner: incoming, loser: { ...existing, status: 'superseded' }, reason: 'higher-confidence' };
|
|
41
|
+
return { winner: existing, loser: { ...incoming, status: 'rejected' }, reason: 'existing-preferred' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function promoteCandidate(record, approval) {
|
|
45
|
+
if (!approval?.approved) return { ok: false, error: 'user approval required' };
|
|
46
|
+
return { ok: true, record: { ...record, status: 'active', userConfirmed: true, lastVerifiedAt: new Date().toISOString(), confidence: Math.max(record.confidence || 0, 0.9) } };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { createMemoryRecord, isActive, resolveConflict, promoteCandidate, factKey };
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
const PAIRINGS_FILE = path.join(os.homedir(), '.natureco', 'pairings.json');
|
|
9
|
+
|
|
10
|
+
function loadPairings() {
|
|
11
|
+
const value = readJsonSafeSync(PAIRINGS_FILE, []);
|
|
12
|
+
return Array.isArray(value) ? value : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function savePairings(pairings) {
|
|
16
|
+
fs.mkdirSync(path.dirname(PAIRINGS_FILE), { recursive: true, mode: 0o700 });
|
|
17
|
+
writeJsonAtomicSync(PAIRINGS_FILE, pairings, { mode: 0o600 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function genCode() {
|
|
21
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findPairing(pairings, channel, senderId, status) {
|
|
25
|
+
const sender = String(senderId);
|
|
26
|
+
return pairings.find(item => item.channel === channel && String(item.senderId) === sender && item.status === status);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createPairingEntry(channel, senderId) {
|
|
30
|
+
const sender = String(senderId);
|
|
31
|
+
return {
|
|
32
|
+
id: `pair_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
33
|
+
code: genCode(), channel, senderId: sender, nodeName: `${channel}:${sender}`,
|
|
34
|
+
status: 'pending', createdAt: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensurePendingPairing(channel, senderId) {
|
|
39
|
+
const pairings = loadPairings();
|
|
40
|
+
const existing = findPairing(pairings, channel, senderId, 'pending');
|
|
41
|
+
if (existing) return existing;
|
|
42
|
+
const entry = createPairingEntry(channel, senderId);
|
|
43
|
+
pairings.push(entry);
|
|
44
|
+
savePairings(pairings);
|
|
45
|
+
return entry;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPaired(channel, senderId) {
|
|
49
|
+
return !!findPairing(loadPairings(), channel, senderId, 'approved');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
PAIRINGS_FILE, loadPairings, savePairings, genCode, ensurePendingPairing, isPaired,
|
|
54
|
+
_internals: { findPairing, createPairingEntry },
|
|
55
|
+
};
|
|
@@ -265,10 +265,20 @@ function acceptProposal(proposalId) {
|
|
|
265
265
|
if (fs.existsSync(skillDir)) {
|
|
266
266
|
return { success: false, reason: `Skill zaten var: ${proposal.suggestedName}` };
|
|
267
267
|
}
|
|
268
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
269
|
-
|
|
270
268
|
const skillMd = generateSkillMd(proposal);
|
|
271
|
-
|
|
269
|
+
const { SkillLifecycle } = require('./skill-lifecycle');
|
|
270
|
+
const lifecycle = new SkillLifecycle(userSkillsDir);
|
|
271
|
+
const staged = lifecycle.stage({ name: proposal.suggestedName, content: skillMd, source: 'repeated-pattern', evidence: { hash: proposal.hash, count: proposal.count } });
|
|
272
|
+
if (!staged.ok) return { success: false, reason: staged.error };
|
|
273
|
+
// This function is only reached through the explicit `skills accept` command.
|
|
274
|
+
// Promotion stays synchronous here because validation is deterministic.
|
|
275
|
+
const candidate = staged.candidate;
|
|
276
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
277
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), candidate.content, 'utf8');
|
|
278
|
+
fs.writeFileSync(path.join(skillDir, 'lifecycle.json'), JSON.stringify({
|
|
279
|
+
currentVersion: 1,
|
|
280
|
+
versions: [{ version: 1, candidateId: candidate.id, source: candidate.source, evidence: candidate.evidence, approvedBy: 'cli-user', approvedAt: new Date().toISOString(), validation: { ok: true, type: 'frontmatter' } }],
|
|
281
|
+
}, null, 2), 'utf8');
|
|
272
282
|
|
|
273
283
|
proposals[idx].status = 'accepted';
|
|
274
284
|
proposals[idx].acceptedAt = new Date().toISOString();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
-
const {
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
5
|
const { PluginError, handleError } = require('./errors');
|
|
6
6
|
|
|
7
7
|
const PLUGINS_DIR = path.join(os.homedir(), '.natureco', 'plugins');
|
|
@@ -122,7 +122,7 @@ function installFromNpm(pkg) {
|
|
|
122
122
|
const tmpDir = path.join(os.tmpdir(), `nc-plugin-${Date.now()}`);
|
|
123
123
|
ensureDir(tmpDir);
|
|
124
124
|
try {
|
|
125
|
-
|
|
125
|
+
execFileSync('npm', ['install', pkg, '--prefix', tmpDir, '--no-save', '--ignore-scripts', '--no-audit', '--no-fund'], { stdio: 'pipe', timeout: 120000, shell: false });
|
|
126
126
|
const pkgDir = path.join(tmpDir, 'node_modules', pkg.split('/').pop());
|
|
127
127
|
const scopedPkgDir = pkg.startsWith('@') ? path.join(tmpDir, 'node_modules', pkg) : null;
|
|
128
128
|
const srcDir = scopedPkgDir && fs.existsSync(scopedPkgDir) ? scopedPkgDir : (fs.existsSync(pkgDir) ? pkgDir : null);
|
|
@@ -155,7 +155,7 @@ function installFromGit(spec) {
|
|
|
155
155
|
const tmpDir = path.join(os.tmpdir(), `nc-plugin-git-${Date.now()}`);
|
|
156
156
|
ensureDir(tmpDir);
|
|
157
157
|
try {
|
|
158
|
-
|
|
158
|
+
execFileSync('git', ['clone', '--depth', '1', repoUrl, tmpDir], { stdio: 'pipe', timeout: 60000, shell: false });
|
|
159
159
|
const slug = spec.split('/').pop().replace(/\.git$/, '');
|
|
160
160
|
const dest = path.join(PLUGINS_DIR, slug);
|
|
161
161
|
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
@@ -21,7 +21,17 @@ const os = require('os');
|
|
|
21
21
|
const ERROR_LOG_PATH = path.join(os.homedir(), '.natureco', 'logs', 'crash.log');
|
|
22
22
|
|
|
23
23
|
let _installed = false;
|
|
24
|
-
let _registered = { rejection: null, exception: null,
|
|
24
|
+
let _registered = { rejection: null, exception: null, streamError: null };
|
|
25
|
+
|
|
26
|
+
function _removeRegisteredHandlers() {
|
|
27
|
+
if (_registered.rejection) process.off('unhandledRejection', _registered.rejection);
|
|
28
|
+
if (_registered.exception) process.off('uncaughtException', _registered.exception);
|
|
29
|
+
if (_registered.streamError) {
|
|
30
|
+
process.stdout.off('error', _registered.streamError);
|
|
31
|
+
process.stderr.off('error', _registered.streamError);
|
|
32
|
+
}
|
|
33
|
+
_registered = { rejection: null, exception: null, streamError: null };
|
|
34
|
+
}
|
|
25
35
|
|
|
26
36
|
function _defaultAudit() {
|
|
27
37
|
try {
|
|
@@ -63,8 +73,7 @@ function install(opts = {}) {
|
|
|
63
73
|
const stderr = opts.stderr || ((msg) => process.stderr.write(msg));
|
|
64
74
|
|
|
65
75
|
// Replace any previous handlers (idempotency for tests).
|
|
66
|
-
|
|
67
|
-
if (_registered.exception) process.off('uncaughtException', _registered.exception);
|
|
76
|
+
_removeRegisteredHandlers();
|
|
68
77
|
|
|
69
78
|
const onRejection = (reason) => {
|
|
70
79
|
const payload = { kind: 'unhandledRejection', error: _serializeError(reason) };
|
|
@@ -106,13 +115,11 @@ function install(opts = {}) {
|
|
|
106
115
|
|
|
107
116
|
process.on('unhandledRejection', onRejection);
|
|
108
117
|
process.on('uncaughtException', onException);
|
|
109
|
-
_registered = { rejection: onRejection, exception: onException,
|
|
118
|
+
_registered = { rejection: onRejection, exception: onException, streamError: onStreamError };
|
|
110
119
|
_installed = true;
|
|
111
120
|
|
|
112
121
|
return function uninstall() {
|
|
113
|
-
|
|
114
|
-
if (_registered.exception) process.off('uncaughtException', _registered.exception);
|
|
115
|
-
_registered = { rejection: null, exception: null, warning: null };
|
|
122
|
+
_removeRegisteredHandlers();
|
|
116
123
|
_installed = false;
|
|
117
124
|
};
|
|
118
125
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function checkPidFile(pidFile, processKill = process.kill) {
|
|
7
|
+
if (!fs.existsSync(pidFile)) return { ok: false, status: 'stopped', reason: 'pid-file-missing' };
|
|
8
|
+
const pid = Number(fs.readFileSync(pidFile, 'utf8').trim());
|
|
9
|
+
if (!Number.isInteger(pid) || pid <= 0) return { ok: false, status: 'invalid', reason: 'invalid-pid' };
|
|
10
|
+
try { processKill(pid, 0); return { ok: true, status: 'running', pid }; }
|
|
11
|
+
catch { return { ok: false, status: 'stale', reason: 'process-not-running', pid }; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function checkDockerContainer(name = 'natureco-sandbox', execFile = execFileSync) {
|
|
15
|
+
try {
|
|
16
|
+
const output = execFile('docker', ['inspect', '--format', '{{json .State}}', name], { encoding: 'utf8', stdio: 'pipe', timeout: 5000 });
|
|
17
|
+
const state = JSON.parse(output.trim());
|
|
18
|
+
return { ok: state.Running === true && state.Health?.Status !== 'unhealthy', status: state.Health?.Status || (state.Running ? 'running' : 'stopped'), running: !!state.Running, restartCount: state.RestartCount || 0 };
|
|
19
|
+
} catch (error) { return { ok: false, status: 'unavailable', reason: error.code === 'ENOENT' ? 'docker-not-installed' : 'container-not-found' }; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function aggregateRuntimeHealth(parts) {
|
|
23
|
+
const entries = Object.entries(parts || {});
|
|
24
|
+
const failed = entries.filter(([, value]) => !value?.ok).map(([name]) => name);
|
|
25
|
+
return { ok: failed.length === 0, status: failed.length === 0 ? 'healthy' : failed.length === entries.length ? 'down' : 'degraded', failed, checks: parts };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { checkPidFile, checkDockerContainer, aggregateRuntimeHealth };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const { writeJsonAtomicSync, readJsonSafeSync } = require('./atomic-file');
|
|
9
|
+
|
|
10
|
+
class MacKeychainBackend {
|
|
11
|
+
constructor(service = 'natureco-cli', execFile = execFileSync) { this.service = service; this.execFile = execFile; }
|
|
12
|
+
set(key, value) { this.execFile('security', ['add-generic-password', '-U', '-a', key, '-s', this.service, '-w', value], { stdio: 'pipe' }); }
|
|
13
|
+
get(key) { try { return this.execFile('security', ['find-generic-password', '-a', key, '-s', this.service, '-w'], { encoding: 'utf8', stdio: 'pipe' }).trim(); } catch { return null; } }
|
|
14
|
+
delete(key) { try { this.execFile('security', ['delete-generic-password', '-a', key, '-s', this.service], { stdio: 'pipe' }); return true; } catch { return false; } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class LinuxSecretServiceBackend {
|
|
18
|
+
constructor(service = 'natureco-cli', execFile = execFileSync) { this.service = service; this.execFile = execFile; }
|
|
19
|
+
set(key, value) { this.execFile('secret-tool', ['store', '--label', `NatureCo ${key}`, 'service', this.service, 'account', key], { input: value, stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
20
|
+
get(key) { try { return this.execFile('secret-tool', ['lookup', 'service', this.service, 'account', key], { encoding: 'utf8', stdio: 'pipe' }).trim() || null; } catch { return null; } }
|
|
21
|
+
delete(key) { try { this.execFile('secret-tool', ['clear', 'service', this.service, 'account', key], { stdio: 'pipe' }); return true; } catch { return false; } }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class WindowsDpapiBackend {
|
|
25
|
+
constructor(options = {}) { this.file = options.file || path.join(os.homedir(), '.natureco', 'secrets.dpapi.json'); this.execFile = options.execFile || execFileSync; }
|
|
26
|
+
_run(script, input) {
|
|
27
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
28
|
+
return this.execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], { input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
|
|
29
|
+
}
|
|
30
|
+
_load() { return readJsonSafeSync(this.file, { version: 1, entries: {} }); }
|
|
31
|
+
_save(data) { fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 }); writeJsonAtomicSync(this.file, data, { mode: 0o600 }); }
|
|
32
|
+
set(name, value) {
|
|
33
|
+
const script = '$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($p)';
|
|
34
|
+
const data = this._load(); data.entries[name] = { data: this._run(script, String(value)), updatedAt: new Date().toISOString() }; this._save(data);
|
|
35
|
+
}
|
|
36
|
+
get(name) {
|
|
37
|
+
const entry = this._load().entries[name]; if (!entry) return null;
|
|
38
|
+
try {
|
|
39
|
+
const script = '$v=[Console]::In.ReadToEnd();$p=[Convert]::FromBase64String($v);$b=[Security.Cryptography.ProtectedData]::Unprotect($p,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Text.Encoding]::UTF8.GetString($b)';
|
|
40
|
+
return this._run(script, entry.data);
|
|
41
|
+
} catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
delete(name) { const data = this._load(); if (!data.entries[name]) return false; delete data.entries[name]; this._save(data); return true; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class EncryptedFileBackend {
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
if (!options.masterKey) throw new Error('NATURECO_MASTER_KEY is required for encrypted file secret storage');
|
|
49
|
+
this.file = options.file || path.join(os.homedir(), '.natureco', 'secrets.enc.json');
|
|
50
|
+
this.key = crypto.scryptSync(String(options.masterKey), 'natureco-secret-store-v1', 32);
|
|
51
|
+
}
|
|
52
|
+
_load() { return readJsonSafeSync(this.file, { version: 1, entries: {} }); }
|
|
53
|
+
_save(data) { fs.mkdirSync(path.dirname(this.file), { recursive: true, mode: 0o700 }); writeJsonAtomicSync(this.file, data, { mode: 0o600 }); }
|
|
54
|
+
set(name, value) {
|
|
55
|
+
const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', this.key, iv);
|
|
56
|
+
const encrypted = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]);
|
|
57
|
+
const data = this._load();
|
|
58
|
+
data.entries[name] = { iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), data: encrypted.toString('base64'), updatedAt: new Date().toISOString() };
|
|
59
|
+
this._save(data);
|
|
60
|
+
}
|
|
61
|
+
get(name) {
|
|
62
|
+
const entry = this._load().entries[name]; if (!entry) return null;
|
|
63
|
+
try {
|
|
64
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, Buffer.from(entry.iv, 'base64'));
|
|
65
|
+
decipher.setAuthTag(Buffer.from(entry.tag, 'base64'));
|
|
66
|
+
return Buffer.concat([decipher.update(Buffer.from(entry.data, 'base64')), decipher.final()]).toString('utf8');
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
delete(name) { const data = this._load(); if (!data.entries[name]) return false; delete data.entries[name]; this._save(data); return true; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class SecretStore {
|
|
73
|
+
constructor(backend) { if (!backend) throw new Error('secret store backend is required'); this.backend = backend; }
|
|
74
|
+
_name(name) { if (!/^[A-Za-z0-9_.-]{1,128}$/.test(name || '')) throw new Error('invalid secret name'); return name; }
|
|
75
|
+
set(name, value) { if (value == null || value === '') throw new Error('secret value cannot be empty'); this.backend.set(this._name(name), String(value)); return { ok: true, name }; }
|
|
76
|
+
get(name) { const value = this.backend.get(this._name(name)); return value == null ? { ok: false, error: 'secret not found' } : { ok: true, value }; }
|
|
77
|
+
delete(name) { return { ok: this.backend.delete(this._name(name)) }; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createSecretStore(options = {}) {
|
|
81
|
+
if (options.backend) return new SecretStore(options.backend);
|
|
82
|
+
if (process.platform === 'darwin') return new SecretStore(new MacKeychainBackend(options.service, options.execFile));
|
|
83
|
+
if (process.platform === 'win32') return new SecretStore(new WindowsDpapiBackend({ file: options.file, execFile: options.execFile }));
|
|
84
|
+
if (process.platform === 'linux' && options.useSecretService !== false) return new SecretStore(new LinuxSecretServiceBackend(options.service, options.execFile));
|
|
85
|
+
const masterKey = options.masterKey || process.env.NATURECO_MASTER_KEY;
|
|
86
|
+
if (!masterKey) throw new Error('No OS keychain backend available. Set NATURECO_MASTER_KEY for encrypted fallback.');
|
|
87
|
+
return new SecretStore(new EncryptedFileBackend({ masterKey, file: options.file }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { SecretStore, MacKeychainBackend, LinuxSecretServiceBackend, WindowsDpapiBackend, EncryptedFileBackend, createSecretStore };
|