natureco-cli 5.7.0 → 5.7.2
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/bin/natureco.js +0 -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 +19 -20
- package/src/commands/setup.js +7 -6
- package/src/tools/dashboard.js +2 -1
- package/src/tools/edit_file.js +143 -0
- package/src/tools/memory_write.js +70 -16
- package/src/tools/read_file.js +122 -63
- package/src/tools/todo_write.js +219 -52
- 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/paste-safe-input.js +173 -0
- 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/bin/natureco.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.2",
|
|
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,8 @@ 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');
|
|
27
|
+
const { createPasteSafeInput, enableBracketedPaste, disableBracketedPaste, restoreNewlines } = require('../utils/paste-safe-input');
|
|
26
28
|
|
|
27
29
|
// v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
|
|
28
30
|
const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
|
|
@@ -313,17 +315,10 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
|
|
|
313
315
|
onChunk(delta.content);
|
|
314
316
|
}
|
|
315
317
|
|
|
316
|
-
// Tool calls (streaming delta)
|
|
318
|
+
// Tool calls (streaming delta) — shared accumulator,
|
|
319
|
+
// see src/utils/streaming-tools.js for the per-index pattern.
|
|
317
320
|
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
|
-
}
|
|
321
|
+
accumulateToolCallDeltas(toolCalls, delta.tool_calls);
|
|
327
322
|
}
|
|
328
323
|
} catch {}
|
|
329
324
|
}
|
|
@@ -338,17 +333,15 @@ async function sendStreaming(providerUrl, providerApiKey, messages, model, onChu
|
|
|
338
333
|
|
|
339
334
|
fullText = result.streamText;
|
|
340
335
|
|
|
341
|
-
// Tool call var mı?
|
|
342
|
-
|
|
336
|
+
// Tool call var mı? finalizeToolCalls drops empty entries + synthesizes ids,
|
|
337
|
+
// so we only need to check the resulting length.
|
|
338
|
+
const finalized = finalizeToolCalls(result.toolCalls);
|
|
339
|
+
if (finalized.length > 0) {
|
|
343
340
|
// Assistant mesajını ekle (tool_calls ile)
|
|
344
341
|
currentMessages.push({
|
|
345
342
|
role: 'assistant',
|
|
346
343
|
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
|
-
})),
|
|
344
|
+
tool_calls: finalized,
|
|
352
345
|
});
|
|
353
346
|
// Her tool call'ı çalıştır, sonuçları tool mesajı olarak ekle
|
|
354
347
|
const toolResults = await processToolCalls(result.toolCalls, onToolCall);
|
|
@@ -530,7 +523,10 @@ async function startRepl(args) {
|
|
|
530
523
|
(cfg.providerUrl || '').includes('mistral.ai') ||
|
|
531
524
|
(cfg.providerUrl || '').includes('localhost') ||
|
|
532
525
|
(cfg.providerUrl || '').includes('ollama');
|
|
533
|
-
const
|
|
526
|
+
// `let` (not `const`) because /system <text> reassigns it at line ~796.
|
|
527
|
+
// Before this fix, /system would throw "Assignment to constant variable"
|
|
528
|
+
// and tear down the REPL session mid-conversation.
|
|
529
|
+
let systemPrompt = [
|
|
534
530
|
// === v5.4.14: EN KRITIK KIMLIK BILGILERI (her zaman ilk) ===
|
|
535
531
|
`SENIN ADIN: ${botName}. SADECE ${botName} adini kullan, model adi SOYLEME.`,
|
|
536
532
|
`PATRONUN: Gencay (Parton) — NatureCo CEO'sudur. Sana "Parton" diye hitap eder.`,
|
|
@@ -620,8 +616,10 @@ async function startRepl(args) {
|
|
|
620
616
|
let totalInputTokens = 0;
|
|
621
617
|
let totalOutputTokens = 0;
|
|
622
618
|
|
|
619
|
+
enableBracketedPaste(process.stdout);
|
|
620
|
+
|
|
623
621
|
const rl = readline.createInterface({
|
|
624
|
-
input: process.stdin,
|
|
622
|
+
input: createPasteSafeInput(process.stdin),
|
|
625
623
|
output: process.stdout,
|
|
626
624
|
prompt: tui.styled('\n You ', { color: tui.PALETTE.primary, bold: true }),
|
|
627
625
|
terminal: true,
|
|
@@ -648,6 +646,7 @@ async function startRepl(args) {
|
|
|
648
646
|
}
|
|
649
647
|
// Global buffer temizle
|
|
650
648
|
if (global._fixBuffer) global._fixBuffer = '';
|
|
649
|
+
disableBracketedPaste(process.stdout);
|
|
651
650
|
console.log(chalk.gray('\n 👋 Görüşürüz!\n'));
|
|
652
651
|
process.exit(exitCode);
|
|
653
652
|
};
|
|
@@ -726,7 +725,7 @@ async function startRepl(args) {
|
|
|
726
725
|
process.on('SIGTERM', () => cleanup(0));
|
|
727
726
|
|
|
728
727
|
rl.on('line', async (input) => {
|
|
729
|
-
const line = input.trim();
|
|
728
|
+
const line = restoreNewlines(input).trim();
|
|
730
729
|
if (!line) { rl.prompt(); return; }
|
|
731
730
|
|
|
732
731
|
// Slash komutlar
|
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">
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit_file — Precise string replacement in an existing file.
|
|
3
|
+
*
|
|
4
|
+
* Modeled after Claude Code's `Edit` tool semantics, with three rules
|
|
5
|
+
* that catch ~all "the agent broke my file" failure modes:
|
|
6
|
+
*
|
|
7
|
+
* 1. The file must exist (no accidental file creation via Edit — use
|
|
8
|
+
* write_file for that).
|
|
9
|
+
* 2. `old_string` must appear in the file. If it doesn't, return an
|
|
10
|
+
* error pointing at the file path; do NOT silently no-op.
|
|
11
|
+
* 3. `old_string` must be UNIQUE in the file unless `replace_all: true`
|
|
12
|
+
* is passed. Otherwise the agent would mutate an unintended site
|
|
13
|
+
* without realizing it.
|
|
14
|
+
*
|
|
15
|
+
* On success: writes via atomic rename (no half-edited files on crash),
|
|
16
|
+
* returns the patched line count + a small diff summary.
|
|
17
|
+
*
|
|
18
|
+
* Why this exists in natureco: until this commit, natureco's only file
|
|
19
|
+
* mutation tool was `write_file`, which overwrites the entire file. Any
|
|
20
|
+
* "rename X to Y in src/foo.js" request meant the agent had to read the
|
|
21
|
+
* file, do the substitution in its head, and re-emit the whole file —
|
|
22
|
+
* a process that loses indentation, drops trailing newlines, and
|
|
23
|
+
* occasionally hallucinates unrelated edits. `edit_file` makes the
|
|
24
|
+
* mutation a 2-3 line delta the agent can't get wrong.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { writeFileAtomicSync } = require('../utils/atomic-file');
|
|
29
|
+
|
|
30
|
+
function _expand(p) {
|
|
31
|
+
if (!p) return p;
|
|
32
|
+
if (p.startsWith('~')) {
|
|
33
|
+
return path.join(require('os').homedir(), p.slice(1));
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(p);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function editFile({ path: filePath, old_string, new_string, replace_all = false }) {
|
|
39
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
40
|
+
return { success: false, error: 'path is required (absolute path or ~-prefixed)' };
|
|
41
|
+
}
|
|
42
|
+
if (typeof old_string !== 'string' || old_string.length === 0) {
|
|
43
|
+
return { success: false, error: 'old_string is required and cannot be empty' };
|
|
44
|
+
}
|
|
45
|
+
if (typeof new_string !== 'string') {
|
|
46
|
+
return { success: false, error: 'new_string is required (use empty string to delete)' };
|
|
47
|
+
}
|
|
48
|
+
if (old_string === new_string) {
|
|
49
|
+
return { success: false, error: 'old_string and new_string are identical — nothing to do' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const target = _expand(filePath);
|
|
53
|
+
if (!fs.existsSync(target)) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: `file not found: ${target}. Edit cannot create files; use write_file for that.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let original;
|
|
60
|
+
try {
|
|
61
|
+
original = fs.readFileSync(target, 'utf8');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return { success: false, error: `read failed: ${e.message}` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Find occurrences. Use indexOf loop for an accurate count without
|
|
67
|
+
// pulling in regex escaping bugs (old_string is raw text, not a pattern).
|
|
68
|
+
let count = 0;
|
|
69
|
+
let i = 0;
|
|
70
|
+
while ((i = original.indexOf(old_string, i)) !== -1) {
|
|
71
|
+
count++;
|
|
72
|
+
i += old_string.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
// Help the agent debug: include a small excerpt so it sees how close
|
|
77
|
+
// its old_string is to reality (whitespace, casing).
|
|
78
|
+
const excerpt = original.length > 400 ? original.slice(0, 400) + '\n... (truncated)' : original;
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: 'old_string not found in file',
|
|
82
|
+
path: target,
|
|
83
|
+
hint: 'Verify exact whitespace, casing, and that you Read the file recently.',
|
|
84
|
+
file_excerpt: excerpt,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (count > 1 && !replace_all) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: `old_string is not unique (${count} occurrences). Pass replace_all: true to change every instance, or extend old_string with more surrounding context to make it unique.`,
|
|
92
|
+
path: target,
|
|
93
|
+
occurrences: count,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const updated = replace_all
|
|
98
|
+
? original.split(old_string).join(new_string)
|
|
99
|
+
: original.replace(old_string, new_string);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
writeFileAtomicSync(target, updated);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { success: false, error: `write failed: ${e.message}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const linesBefore = original.split('\n').length;
|
|
108
|
+
const linesAfter = updated.split('\n').length;
|
|
109
|
+
const lineDelta = linesAfter - linesBefore;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
path: target,
|
|
114
|
+
replacements: count,
|
|
115
|
+
replace_all: !!replace_all,
|
|
116
|
+
lines_before: linesBefore,
|
|
117
|
+
lines_after: linesAfter,
|
|
118
|
+
line_delta: lineDelta,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
name: 'edit_file',
|
|
124
|
+
description:
|
|
125
|
+
'Performs exact string replacement in an existing file. The old_string ' +
|
|
126
|
+
'must match the file content EXACTLY (including whitespace and indentation) ' +
|
|
127
|
+
'and must be UNIQUE unless replace_all=true. Use this for any targeted ' +
|
|
128
|
+
'rename/refactor/patch instead of rewriting the file with write_file. ' +
|
|
129
|
+
'Fails with a helpful hint when old_string is missing or ambiguous.',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: {
|
|
133
|
+
path: { type: 'string', description: 'Absolute path of file to edit (~/-prefix allowed)' },
|
|
134
|
+
old_string: { type: 'string', description: 'Exact substring to replace (case-sensitive, whitespace-sensitive)' },
|
|
135
|
+
new_string: { type: 'string', description: 'Replacement text (must differ from old_string; pass "" to delete)' },
|
|
136
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of failing on multiple matches', default: false },
|
|
137
|
+
},
|
|
138
|
+
required: ['path', 'old_string', 'new_string'],
|
|
139
|
+
},
|
|
140
|
+
execute: editFile,
|
|
141
|
+
// Exposed for unit tests.
|
|
142
|
+
_internals: { editFile },
|
|
143
|
+
};
|