natureco-cli 5.7.0 → 5.7.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 +89 -0
- package/package.json +14 -6
- package/src/commands/chat.js +5 -0
- package/src/commands/config.js +4 -0
- package/src/commands/gateway-server.js +38 -3
- package/src/commands/gateway.js +2 -0
- package/src/commands/nodes.js +4 -0
- package/src/commands/repl.js +13 -18
- package/src/commands/setup.js +7 -6
- package/src/tools/dashboard.js +2 -1
- package/src/tools/memory_write.js +70 -16
- package/src/utils/api.js +47 -40
- package/src/utils/approvals.js +27 -12
- package/src/utils/atomic-file.js +91 -0
- package/src/utils/dashboard-server.js +3 -2
- package/src/utils/error.js +4 -0
- package/src/utils/history.js +6 -14
- package/src/utils/ports.js +44 -0
- package/src/utils/process-errors.js +115 -0
- package/src/utils/provider-detect.js +78 -0
- package/src/utils/sessions.js +6 -13
- package/src/utils/streaming-tools.js +97 -0
- package/src/utils/tools.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NatureCo CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [5.7.1] - 2026-06-25 — "BUG FIX SPRINT"
|
|
6
|
+
|
|
7
|
+
Comprehensive audit-driven sprint: 7 real runtime bugs fixed, 6 new
|
|
8
|
+
utility modules with ~90%+ test coverage, ESLint + flat config installed,
|
|
9
|
+
test scripts wired to vitest (previously `npm test` was just running
|
|
10
|
+
`--help`). Pure quality / stability — no public API change.
|
|
11
|
+
|
|
12
|
+
### 🐛 Fixed (runtime bugs)
|
|
13
|
+
- **REPL `/system <text>` crashed with TypeError** (`commands/repl.js`):
|
|
14
|
+
systemPrompt was `const` but the slash handler reassigned it. → `let`.
|
|
15
|
+
- **Telegram + IRC + SMS message handlers ReferenceError on every inbound**
|
|
16
|
+
(`commands/gateway-server.js`): `cleanCommand` variable never declared
|
|
17
|
+
in those scopes. Added `stripSlashPrefix(text)` helper mirroring the
|
|
18
|
+
v5.6.41+ WhatsApp transform; all three channels now derive it correctly.
|
|
19
|
+
- **Tool-alias rewrites threw ReferenceError** (`utils/tools.js`): typo
|
|
20
|
+
`TOOL_ALIASES[t.name]` where the local was `ALIAS_MAP`. Fixed.
|
|
21
|
+
- **5 silent `no-undef` ReferenceErrors** across `commands/{chat,nodes,
|
|
22
|
+
gateway,config}.js` + `utils/error.js` — missing `require()` calls
|
|
23
|
+
that had been hidden by CommonJS load-order side effects (would
|
|
24
|
+
crash on a fresh process or worker restart).
|
|
25
|
+
|
|
26
|
+
### 🔒 Security
|
|
27
|
+
- **exec-approvals.json was world-readable (0644)** — local privilege
|
|
28
|
+
escalation hedef. Now 0o600 (file) + 0o700 (parent dir), with
|
|
29
|
+
auto-tightening of pre-existing loose installs. Removed dangling
|
|
30
|
+
`APPROVALS_SOCKET_PATH` constant + unused `net` require (socket never
|
|
31
|
+
existed; storage is the JSON file).
|
|
32
|
+
- **Anthropic `system` field sent as `''` or `undefined`** (api.js): now
|
|
33
|
+
always non-empty via `extractSystemForAnthropic(messages)` helper with
|
|
34
|
+
a meaningful default. Prevents 400 "system: cannot be empty" on
|
|
35
|
+
recent Messages API revisions + unanchored-model drift.
|
|
36
|
+
|
|
37
|
+
### ⚙️ Reliability
|
|
38
|
+
- **Crash-safe atomic file writes** for sessions, history, memory,
|
|
39
|
+
approvals (new `utils/atomic-file.js`: temp-write + rename(2)).
|
|
40
|
+
Prior `fs.writeFileSync` left truncated JSON on SIGTERM / OOM /
|
|
41
|
+
power loss.
|
|
42
|
+
- **Memory fact cap silent fail fixed** (`tools/memory_write.js`): the
|
|
43
|
+
hardcoded `slice(0, 15)` ran BEFORE push, so once 15 high-score
|
|
44
|
+
facts were saved, every new write was the next iteration's eviction
|
|
45
|
+
victim — silently. Now: `MAX_FACTS_PER_USER` default 50 (env
|
|
46
|
+
`NATURECO_MAX_FACTS`), cap applied AFTER push, just-written fact
|
|
47
|
+
pinned at top, `console.warn` on breach (`NATURECO_QUIET_MEMORY=1`
|
|
48
|
+
to silence).
|
|
49
|
+
- **Global `unhandledRejection` + `uncaughtException` handlers**
|
|
50
|
+
(`utils/process-errors.js`, installed as the first statement in
|
|
51
|
+
`bin/natureco.js`): structured audit log entry + friendly Turkish
|
|
52
|
+
stderr + exit 1, instead of Node's default ugly stack dump.
|
|
53
|
+
- **Dashboard port + host de-hardcoded** (`utils/ports.js`):
|
|
54
|
+
`NATURECO_DASHBOARD_PORT` + `NATURECO_DASHBOARD_HOST` env overrides
|
|
55
|
+
with range/format validation. Previously 7421 was inlined in two
|
|
56
|
+
separate modules; drift risk eliminated.
|
|
57
|
+
|
|
58
|
+
### 🧹 Refactor (DRY)
|
|
59
|
+
- **Streaming tool-call delta accumulator** extracted to
|
|
60
|
+
`utils/streaming-tools.js`. The per-index buffer + string-concat
|
|
61
|
+
pattern was duplicated in `utils/api.js` and `commands/repl.js` —
|
|
62
|
+
any drift between them would silently break tool calling on
|
|
63
|
+
Groq / MiniMax / DeepSeek / OpenAI.
|
|
64
|
+
- **Provider detection** centralized in `utils/provider-detect.js`.
|
|
65
|
+
Three call sites (`utils/api.js`, `commands/setup.js`) used three
|
|
66
|
+
different versions of the URL→provider mapping; the setup.js variant
|
|
67
|
+
was already incorrect (missed `minimax.cn`). Helper makes
|
|
68
|
+
`detectProvider`, `isMiniMax`, `isAnthropic`, `isGroq`, `isOllama`
|
|
69
|
+
the single source of truth.
|
|
70
|
+
|
|
71
|
+
### 🧪 Testing
|
|
72
|
+
- **`npm test` actually runs tests now** — was previously just
|
|
73
|
+
`node bin/natureco.js help` (a load-smoke). Wired to `vitest run`.
|
|
74
|
+
- **+95 unit tests** across 9 new spec files. Coverage of the new
|
|
75
|
+
utility modules: streaming-tools 97%, provider-detect 100%,
|
|
76
|
+
process-errors 88%, ports ~93%, atomic-file ~93%, memory_write
|
|
77
|
+
internals ~85%.
|
|
78
|
+
- **Test suite: 12 files / 270 tests → 21 files / 365 tests.**
|
|
79
|
+
- `@vitest/coverage-v8` dev dep added; `npm run test:coverage` works.
|
|
80
|
+
- **prepublishOnly gate strengthened**: now runs `node --check` +
|
|
81
|
+
`eslint --quiet` + `vitest run` in sequence. A broken publish to
|
|
82
|
+
`npm install -g natureco-cli` users is now strictly blocked.
|
|
83
|
+
|
|
84
|
+
### 🔧 Tooling
|
|
85
|
+
- **ESLint v9 flat config added** (`eslint.config.js`):
|
|
86
|
+
`@eslint/js` recommended + warn-level checks for unused-vars,
|
|
87
|
+
useless-escape, case-declarations, control-regex. Test files get
|
|
88
|
+
ES-module sourceType + vitest globals; `src/tools/browser*.js`
|
|
89
|
+
get browser globals for Playwright page.evaluate context.
|
|
90
|
+
Scripts: `npm run lint`, `lint:fix`, `lint:errors-only`.
|
|
91
|
+
After the no-undef fixes: 0 errors (293 unused-vars warnings
|
|
92
|
+
remain for a follow-up sprint).
|
|
93
|
+
|
|
5
94
|
## [5.7.0] - 2026-06-24 - SOUL SCRUBBED (MINOR)
|
|
6
95
|
|
|
7
96
|
### Security
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.1",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
|
@@ -15,12 +15,16 @@
|
|
|
15
15
|
"DEPLOY_v2.0.0.md"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "
|
|
19
|
-
"test:unit": "
|
|
20
|
-
"test:watch": "
|
|
21
|
-
"test:coverage": "
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:unit": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"test:coverage": "vitest run --coverage",
|
|
22
|
+
"lint": "eslint src/ bin/ test/",
|
|
23
|
+
"lint:fix": "eslint src/ bin/ test/ --fix",
|
|
24
|
+
"lint:errors-only": "eslint src/ bin/ test/ --quiet",
|
|
25
|
+
"smoke": "node --check bin/natureco.js && node bin/natureco.js help",
|
|
22
26
|
"postinstall": "node bin/natureco.js doctor || true",
|
|
23
|
-
"prepublishOnly": "node --check bin/natureco.js"
|
|
27
|
+
"prepublishOnly": "node --check bin/natureco.js && eslint src/ bin/ test/ --quiet && vitest run"
|
|
24
28
|
},
|
|
25
29
|
"keywords": [
|
|
26
30
|
"natureco",
|
|
@@ -81,6 +85,10 @@
|
|
|
81
85
|
"ws": "^8.20.0"
|
|
82
86
|
},
|
|
83
87
|
"devDependencies": {
|
|
88
|
+
"@eslint/js": "^9.39.4",
|
|
89
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
90
|
+
"eslint": "^9.39.4",
|
|
91
|
+
"globals": "^15.15.0",
|
|
84
92
|
"vitest": "^4.1.9"
|
|
85
93
|
}
|
|
86
94
|
}
|
package/src/commands/chat.js
CHANGED
|
@@ -17,6 +17,11 @@ const { runHooks } = require('../utils/hooks');
|
|
|
17
17
|
const { createSession, loadSession, getLatestSession, addMessageToSession, loadLastSession, listSessions, saveSession } = require('../utils/sessions');
|
|
18
18
|
const { NatureCoError, ApiError, handleError } = require('../utils/errors');
|
|
19
19
|
const { getSessionStats, resetSessionStats } = require('../utils/tool-runner');
|
|
20
|
+
// getBots + sendMessage / _sendMessage are referenced later (loadProviders
|
|
21
|
+
// pre-warm, fallback non-streaming path) but the require was missing —
|
|
22
|
+
// the code only worked when something else in the load order had already
|
|
23
|
+
// required api.js.
|
|
24
|
+
const { getBots, sendMessage, _sendMessage } = require('../utils/api');
|
|
20
25
|
|
|
21
26
|
const ASCII_LOGO = [
|
|
22
27
|
'███╗ ██╗ █████╗ ████████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗',
|
package/src/commands/config.js
CHANGED
|
@@ -4,6 +4,10 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const { getConfig, setConfigValue, getAllConfig, listBackups, restoreConfig, saveConfig, CONFIG_FILE, CONFIG_BACKUP_DIR } = require('../utils/config');
|
|
6
6
|
const TB = require('../utils/token-budget');
|
|
7
|
+
// `tui` is referenced 25× below for styled output. Was relying on a
|
|
8
|
+
// side-effect global from elsewhere in the load order; require it
|
|
9
|
+
// explicitly so the file works in isolation too (caught by no-undef).
|
|
10
|
+
const tui = require('../utils/tui');
|
|
7
11
|
|
|
8
12
|
function config(args) {
|
|
9
13
|
const [action, key, ...valueParts] = args;
|
|
@@ -10,6 +10,31 @@ const { ApiError } = require('../utils/errors');
|
|
|
10
10
|
const PID_FILE = path.join(os.homedir(), '.natureco', 'gateway.pid');
|
|
11
11
|
const LOG_FILE = path.join(os.homedir(), '.natureco', 'gateway.log');
|
|
12
12
|
|
|
13
|
+
// `https` is used in the webhook delivery path (~line 570). The require was
|
|
14
|
+
// missing; the module only worked because Node's CommonJS cache happens to
|
|
15
|
+
// have it loaded from elsewhere, but in a fresh process or after a worker
|
|
16
|
+
// restart this would crash. Explicit.
|
|
17
|
+
const https = require('https');
|
|
18
|
+
// saveConfig (and the loaded `config` value, when not shadowed in scope)
|
|
19
|
+
// referenced in the Discord ready handler / Mattermost HTTP path. Same
|
|
20
|
+
// side-effect-global story as above.
|
|
21
|
+
const { saveConfig } = require('../utils/config');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Strip a leading slash command prefix from a messaging-channel inbound.
|
|
25
|
+
* v5.6.41+ added slash-prefix routing (e.g. `/ask`, `/help`, `/translate`)
|
|
26
|
+
* for iMessage + WhatsApp; the Telegram/IRC/SMS handlers were referencing
|
|
27
|
+
* a `cleanCommand` variable that no longer existed (3 ReferenceError sites,
|
|
28
|
+
* caught by ESLint no-undef). This helper restores the missing transform:
|
|
29
|
+
* "/ask merhaba" → "merhaba"
|
|
30
|
+
* "merhaba" → "merhaba"
|
|
31
|
+
* "" / undefined → ""
|
|
32
|
+
*/
|
|
33
|
+
function stripSlashPrefix(text) {
|
|
34
|
+
if (!text || typeof text !== 'string') return '';
|
|
35
|
+
return text.replace(/^\/[a-zA-Z0-9_-]+\s*/, '').trim() || text.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
// Silent logger for Baileys
|
|
14
39
|
const silentLogger = {
|
|
15
40
|
level: 'silent',
|
|
@@ -655,9 +680,10 @@ async function startTelegramProvider(config) {
|
|
|
655
680
|
systemPrompt += '\n\n' + memoryPrompt;
|
|
656
681
|
}
|
|
657
682
|
|
|
683
|
+
const cleanCommand = stripSlashPrefix(messageText);
|
|
658
684
|
const response = await sendMessage(cfg.providerApiKey || cfg.apiKey || '', botId, cleanCommand, conversationId, systemPrompt);
|
|
659
685
|
const reply = response?.reply || response?.message || '';
|
|
660
|
-
|
|
686
|
+
|
|
661
687
|
if (reply) {
|
|
662
688
|
log('telegram', 'Sending reply...', 'cyan');
|
|
663
689
|
|
|
@@ -1171,6 +1197,8 @@ async function processIrcMessage(config, sender, target, text, socket) {
|
|
|
1171
1197
|
let systemPrompt = `You are a helpful IRC assistant. Keep responses concise. Use IRC-friendly formatting.`;
|
|
1172
1198
|
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1173
1199
|
|
|
1200
|
+
// IRC handler's parameter is `text` (see signature above), not `messageText`.
|
|
1201
|
+
const cleanCommand = stripSlashPrefix(text);
|
|
1174
1202
|
const response = await sendMessage(
|
|
1175
1203
|
cfg.providerApiKey || cfg.apiKey || '', botId, cleanCommand,
|
|
1176
1204
|
conversationId, systemPrompt
|
|
@@ -1732,6 +1760,9 @@ async function handleSmsWebhook(config, body, req) {
|
|
|
1732
1760
|
let systemPrompt = `You are a helpful SMS assistant. Keep responses concise (SMS format).`;
|
|
1733
1761
|
if (memoryPrompt) systemPrompt += '\n\n' + memoryPrompt;
|
|
1734
1762
|
|
|
1763
|
+
// SMS handler binds the inbound to `text` (Twilio webhook body), unlike
|
|
1764
|
+
// Telegram/IRC which use `messageText`.
|
|
1765
|
+
const cleanCommand = stripSlashPrefix(text);
|
|
1735
1766
|
const response = await sendMessage(
|
|
1736
1767
|
cfg.providerApiKey || cfg.apiKey || '', botId, cleanCommand,
|
|
1737
1768
|
conversationId, systemPrompt
|
|
@@ -1880,7 +1911,10 @@ function startHttpServer() {
|
|
|
1880
1911
|
res.end(JSON.stringify({ error: 'Mattermost not connected' }));
|
|
1881
1912
|
return;
|
|
1882
1913
|
}
|
|
1883
|
-
|
|
1914
|
+
// Load fresh config inside the handler — the surrounding
|
|
1915
|
+
// `startHttpServer` scope does not bind `config`.
|
|
1916
|
+
const { getConfig: _gc1 } = require('../utils/config');
|
|
1917
|
+
await sendMattermostMessage(_gc1(), target, message);
|
|
1884
1918
|
log('http', `Mattermost message sent to channel ${target}`, 'green');
|
|
1885
1919
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1886
1920
|
res.end(JSON.stringify({ success: true, channel: 'mattermost', target }));
|
|
@@ -1907,7 +1941,8 @@ function startHttpServer() {
|
|
|
1907
1941
|
res.end(JSON.stringify({ error: 'SMS not connected' }));
|
|
1908
1942
|
return;
|
|
1909
1943
|
}
|
|
1910
|
-
|
|
1944
|
+
const { getConfig: _gc2 } = require('../utils/config');
|
|
1945
|
+
await sendSmsMessage(_gc2(), target, message);
|
|
1911
1946
|
log('http', `SMS sent to ${target}`, 'green');
|
|
1912
1947
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1913
1948
|
res.end(JSON.stringify({ success: true, channel: 'sms', target }));
|
package/src/commands/gateway.js
CHANGED
|
@@ -12,6 +12,8 @@ const { loadBaileys } = require('../utils/baileys');
|
|
|
12
12
|
const { NatureCoError, GatewayError, handleError } = require('../utils/errors');
|
|
13
13
|
const pino = require('pino');
|
|
14
14
|
const logger = pino({ level: 'silent' });
|
|
15
|
+
// Version banner pulls packageJson at line 315 — was missing the require.
|
|
16
|
+
const packageJson = require('../../package.json');
|
|
15
17
|
|
|
16
18
|
async function gateway(action, ...args) {
|
|
17
19
|
if (action === 'start') {
|
package/src/commands/nodes.js
CHANGED
|
@@ -4,6 +4,10 @@ const F = require('../utils/format');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
// getConfig + saveConfig referenced 8× below — missing require relied on
|
|
8
|
+
// side-effect global from another module's load order.
|
|
9
|
+
const { getConfig, saveConfig } = require('../utils/config');
|
|
10
|
+
|
|
7
11
|
function nodes(args) {
|
|
8
12
|
const [action, ...params] = args || [];
|
|
9
13
|
|
package/src/commands/repl.js
CHANGED
|
@@ -23,6 +23,7 @@ const { spawn } = require('child_process');
|
|
|
23
23
|
const chalk = require('chalk');
|
|
24
24
|
const tui = require('../utils/tui');
|
|
25
25
|
const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
|
|
26
|
+
const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/streaming-tools');
|
|
26
27
|
|
|
27
28
|
// v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
|
|
28
29
|
const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
|
|
@@ -313,17 +314,10 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
|
|
|
313
314
|
onChunk(delta.content);
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
// Tool calls (streaming delta)
|
|
317
|
+
// Tool calls (streaming delta) — shared accumulator,
|
|
318
|
+
// see src/utils/streaming-tools.js for the per-index pattern.
|
|
317
319
|
if (delta.tool_calls) {
|
|
318
|
-
|
|
319
|
-
const idx = tcDelta.index;
|
|
320
|
-
if (!toolCalls[idx]) {
|
|
321
|
-
toolCalls[idx] = { index: idx, id: '', name: '', args: '' };
|
|
322
|
-
}
|
|
323
|
-
if (tcDelta.id) toolCalls[idx].id = tcDelta.id;
|
|
324
|
-
if (tcDelta.function?.name) toolCalls[idx].name += tcDelta.function.name;
|
|
325
|
-
if (tcDelta.function?.arguments) toolCalls[idx].args += tcDelta.function.arguments;
|
|
326
|
-
}
|
|
320
|
+
accumulateToolCallDeltas(toolCalls, delta.tool_calls);
|
|
327
321
|
}
|
|
328
322
|
} catch {}
|
|
329
323
|
}
|
|
@@ -338,17 +332,15 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
|
|
|
338
332
|
|
|
339
333
|
fullText = result.streamText;
|
|
340
334
|
|
|
341
|
-
// Tool call var mı?
|
|
342
|
-
|
|
335
|
+
// Tool call var mı? finalizeToolCalls drops empty entries + synthesizes ids,
|
|
336
|
+
// so we only need to check the resulting length.
|
|
337
|
+
const finalized = finalizeToolCalls(result.toolCalls);
|
|
338
|
+
if (finalized.length > 0) {
|
|
343
339
|
// Assistant mesajını ekle (tool_calls ile)
|
|
344
340
|
currentMessages.push({
|
|
345
341
|
role: 'assistant',
|
|
346
342
|
content: result.streamText || null,
|
|
347
|
-
tool_calls:
|
|
348
|
-
id: tc.id || `call_${Date.now()}_${tc.index}`,
|
|
349
|
-
type: 'function',
|
|
350
|
-
function: { name: tc.name, arguments: tc.args },
|
|
351
|
-
})),
|
|
343
|
+
tool_calls: finalized,
|
|
352
344
|
});
|
|
353
345
|
// Her tool call'ı çalıştır, sonuçları tool mesajı olarak ekle
|
|
354
346
|
const toolResults = await processToolCalls(result.toolCalls, onToolCall);
|
|
@@ -530,7 +522,10 @@ async function startRepl(args) {
|
|
|
530
522
|
(cfg.providerUrl || '').includes('mistral.ai') ||
|
|
531
523
|
(cfg.providerUrl || '').includes('localhost') ||
|
|
532
524
|
(cfg.providerUrl || '').includes('ollama');
|
|
533
|
-
const
|
|
525
|
+
// `let` (not `const`) because /system <text> reassigns it at line ~796.
|
|
526
|
+
// Before this fix, /system would throw "Assignment to constant variable"
|
|
527
|
+
// and tear down the REPL session mid-conversation.
|
|
528
|
+
let systemPrompt = [
|
|
534
529
|
// === v5.4.14: EN KRITIK KIMLIK BILGILERI (her zaman ilk) ===
|
|
535
530
|
`SENIN ADIN: ${botName}. SADECE ${botName} adini kullan, model adi SOYLEME.`,
|
|
536
531
|
`PATRONUN: Gencay (Parton) — NatureCo CEO'sudur. Sana "Parton" diye hitap eder.`,
|
package/src/commands/setup.js
CHANGED
|
@@ -631,22 +631,23 @@ module.exports = setup;
|
|
|
631
631
|
async function validateApiKey(providerUrl, apiKey) {
|
|
632
632
|
return new Promise((resolve) => {
|
|
633
633
|
const https = require('https');
|
|
634
|
-
const
|
|
635
|
-
const isMM =
|
|
636
|
-
const endpoint = isMM
|
|
634
|
+
const { isMiniMax, isGroq, isAnthropic } = require('../utils/provider-detect');
|
|
635
|
+
const isMM = isMiniMax(providerUrl);
|
|
636
|
+
const endpoint = isMM
|
|
637
637
|
? providerUrl.replace(/\/+$/, '') + '/v1/text/chatcompletion_v2'
|
|
638
638
|
: providerUrl.replace(/\/+$/, '') + '/chat/completions';
|
|
639
639
|
|
|
640
640
|
// Her provider icin test modeli
|
|
641
641
|
let testModel = 'gpt-3.5-turbo';
|
|
642
|
-
if (providerUrl
|
|
643
|
-
else if (providerUrl
|
|
642
|
+
if (isGroq(providerUrl)) testModel = 'llama-3.1-8b-instant';
|
|
643
|
+
else if (isAnthropic(providerUrl)) testModel = 'claude-3-haiku-20240307';
|
|
644
644
|
else if (isMM) testModel = 'MiniMax-M2.5';
|
|
645
645
|
else if (providerUrl.includes('gemini')) testModel = 'gemini-1.5-flash';
|
|
646
646
|
else if (providerUrl.includes('mistral.ai')) testModel = 'mistral-tiny';
|
|
647
647
|
else if (providerUrl.includes('openrouter.ai')) testModel = 'meta-llama/llama-3.1-8b-instruct:free';
|
|
648
648
|
else if (providerUrl.includes('deepseek.com')) testModel = 'deepseek-chat';
|
|
649
|
-
|
|
649
|
+
|
|
650
|
+
|
|
650
651
|
const data = JSON.stringify({
|
|
651
652
|
model: testModel,
|
|
652
653
|
messages: [{ role: 'user', content: 'hi' }],
|
package/src/tools/dashboard.js
CHANGED
|
@@ -15,8 +15,9 @@ const http = require("http");
|
|
|
15
15
|
const fs = require("fs");
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const os = require("os");
|
|
18
|
+
const { getDashboardPort } = require("../utils/ports");
|
|
18
19
|
|
|
19
|
-
const PORT =
|
|
20
|
+
const PORT = getDashboardPort();
|
|
20
21
|
const DASHBOARD_HTML = `
|
|
21
22
|
<!DOCTYPE html>
|
|
22
23
|
<html lang="tr">
|
|
@@ -8,53 +8,93 @@
|
|
|
8
8
|
const fs = require("fs");
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const os = require("os");
|
|
11
|
+
const { writeJsonAtomicSync, readJsonSafeSync } = require("../utils/atomic-file");
|
|
11
12
|
|
|
12
13
|
const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
|
|
13
14
|
|
|
15
|
+
// Soft cap on per-user fact count. Decay already prunes old/low-importance
|
|
16
|
+
// facts; this just bounds the worst-case file size and prompt-injection
|
|
17
|
+
// surface. Configurable via NATURECO_MAX_FACTS (default 50, was a hard 15
|
|
18
|
+
// that was silently truncating new writes when full).
|
|
19
|
+
const MAX_FACTS_PER_USER = (() => {
|
|
20
|
+
const raw = parseInt(process.env.NATURECO_MAX_FACTS || "", 10);
|
|
21
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 50;
|
|
22
|
+
})();
|
|
23
|
+
|
|
14
24
|
function getMemoryFile(username) {
|
|
15
25
|
const name = (username || "default").toLowerCase();
|
|
16
26
|
return path.join(MEMORY_DIR, `${name}.json`);
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
function _emptyMemory(username) {
|
|
30
|
+
return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
function loadMemory(username) {
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
if (!fs.existsSync(file)) {
|
|
23
|
-
return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
|
|
24
|
-
}
|
|
25
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
26
|
-
} catch {
|
|
27
|
-
return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
|
|
28
|
-
}
|
|
34
|
+
return readJsonSafeSync(getMemoryFile(username), _emptyMemory(username));
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
function saveMemory(username, memory) {
|
|
32
38
|
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
33
39
|
memory.lastUpdated = new Date().toISOString();
|
|
34
|
-
|
|
40
|
+
writeJsonAtomicSync(getMemoryFile(username), memory);
|
|
35
41
|
return memory;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
39
|
-
* Score azalt (eski fact'ler zamanla
|
|
45
|
+
* Score azalt (eski fact'ler zamanla unutulur).
|
|
46
|
+
* Sıralama veya soft-cap UYGULAMAZ — onu enforceFactLimit yapar
|
|
47
|
+
* (push'tan sonra çağrılır, böylece yeni fact silinmez).
|
|
40
48
|
*/
|
|
41
49
|
function decayFacts(memory) {
|
|
42
50
|
if (!memory.facts) return memory;
|
|
43
51
|
const now = Date.now();
|
|
44
52
|
memory.facts = memory.facts.map(f => {
|
|
45
53
|
if (!f.score) f.score = 5;
|
|
46
|
-
// 1 haftadan eski -1, 1 aydan eski -3
|
|
47
54
|
const ageMs = now - new Date(f.updatedAt || f.createdAt || now).getTime();
|
|
48
55
|
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
49
56
|
if (ageDays > 30) f.score = Math.max(0, f.score - 3);
|
|
50
57
|
else if (ageDays > 7) f.score = Math.max(0, f.score - 1);
|
|
51
58
|
return f;
|
|
52
59
|
});
|
|
53
|
-
// score 0 olanlari sil
|
|
54
60
|
memory.facts = memory.facts.filter(f => (f.score || 0) > 0);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
return memory;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Soft cap: yeni fact eklenmesinden SONRA uygulanır, böylece az önce
|
|
66
|
+
* yazılan fact eviction kurbanı olmaz. En düşük score + en eski updatedAt
|
|
67
|
+
* önce gider. Cap aşılırsa stderr'e tek satır warn yazar (eski "silent
|
|
68
|
+
* truncate to 15" davranışını gözlemlenebilir hale getirir).
|
|
69
|
+
*
|
|
70
|
+
* @param {{facts: Array<object>}} memory
|
|
71
|
+
* @param {{recentValue?: string}} [opts] Korunması zorunlu fact (yeni eklenen)
|
|
72
|
+
* @returns the same memory
|
|
73
|
+
*/
|
|
74
|
+
function enforceFactLimit(memory, opts = {}) {
|
|
75
|
+
if (!memory.facts || memory.facts.length <= MAX_FACTS_PER_USER) return memory;
|
|
76
|
+
const before = memory.facts.length;
|
|
77
|
+
const recent = opts.recentValue ? opts.recentValue.toLowerCase() : null;
|
|
78
|
+
// Sort by score desc, then updatedAt desc (newest+highest first).
|
|
79
|
+
// The just-pushed fact is pinned at the top regardless of its score.
|
|
80
|
+
memory.facts.sort((a, b) => {
|
|
81
|
+
if (recent) {
|
|
82
|
+
if ((a.value || "").toLowerCase() === recent) return -1;
|
|
83
|
+
if ((b.value || "").toLowerCase() === recent) return 1;
|
|
84
|
+
}
|
|
85
|
+
const sa = a.score || 0, sb = b.score || 0;
|
|
86
|
+
if (sa !== sb) return sb - sa;
|
|
87
|
+
return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
|
|
88
|
+
});
|
|
89
|
+
memory.facts = memory.facts.slice(0, MAX_FACTS_PER_USER);
|
|
90
|
+
const dropped = before - memory.facts.length;
|
|
91
|
+
if (dropped > 0 && !process.env.NATURECO_QUIET_MEMORY) {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.warn(
|
|
94
|
+
`[memory] cap ${MAX_FACTS_PER_USER} aşıldı, en düşük skorlu ${dropped} fact düşürüldü ` +
|
|
95
|
+
`(NATURECO_MAX_FACTS ile değiştir, NATURECO_QUIET_MEMORY=1 ile sustur)`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
58
98
|
return memory;
|
|
59
99
|
}
|
|
60
100
|
|
|
@@ -122,6 +162,8 @@ function addMemory({ username, fact, score = 5, category = "general", botName, n
|
|
|
122
162
|
createdAt: new Date().toISOString(),
|
|
123
163
|
});
|
|
124
164
|
}
|
|
165
|
+
// Limit'i ZIM push'tan sonra uygula → yeni eklenen fact düşürülmez.
|
|
166
|
+
memory = enforceFactLimit(memory, { recentValue: fact });
|
|
125
167
|
}
|
|
126
168
|
|
|
127
169
|
if (!memory.preferences) memory.preferences = [];
|
|
@@ -173,6 +215,18 @@ function showMemory({ username }) {
|
|
|
173
215
|
}
|
|
174
216
|
|
|
175
217
|
module.exports = {
|
|
218
|
+
// Exposed for tests / advanced consumers — not part of the tool schema.
|
|
219
|
+
_internals: {
|
|
220
|
+
MAX_FACTS_PER_USER,
|
|
221
|
+
enforceFactLimit,
|
|
222
|
+
decayFacts,
|
|
223
|
+
loadMemory,
|
|
224
|
+
saveMemory,
|
|
225
|
+
addMemory,
|
|
226
|
+
clearMemory,
|
|
227
|
+
showMemory,
|
|
228
|
+
getMemoryFile,
|
|
229
|
+
},
|
|
176
230
|
name: "memory_write",
|
|
177
231
|
description: "Memory'ye yeni fact/bilgi kaydet veya bot ismini/nickname'i degistir. Kalici, REPL'in extractMemoryFromMessage ozelligi.",
|
|
178
232
|
inputSchema: {
|