natureco-cli 5.3.0 → 5.4.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 CHANGED
@@ -537,3 +537,35 @@ Parton'un vizyonu: "Ihtiyaca gore skill yoksa kendi uretsin". LLM ile yeni bir s
537
537
  - **Versiyon**: 5.1.0
538
538
  - **Kurulum**: `npm install -g natureco-cli`
539
539
  - **Lisans**: MIT
540
+
541
+ ## [5.3.0] - 2026-06-22 — "VOICE EDITION + AUTO-MEMORY"
542
+
543
+ ### Yeni: voice_chat Tool (52. Tool)
544
+ Parton'un vizyonu: "Bilgisayarla konusayim".
545
+ - macOS'ta mikrofondan ses kaydi (`rec` + `sox`)
546
+ - Whisper API ile ses → metin donusumu (Turkce)
547
+ - Cevabi macOS `say` ile sesli oku
548
+ - Hands-free agent kullanimi
549
+
550
+ ### Yeni: Otomatik Memory Extractor (REPL'e entegre)
551
+ v5.3.0 ile REPL, kullanicinin kişisel bilgi verdigini anlayip otomatik kaydeder:
552
+ - 'adım X' → memory'ye 'Adı: X' yaz
553
+ - 'sevdiğim X' → preference kategorisinde
554
+ - 'ben X yapıyorum' → work kategorisinde
555
+ - 'X tutkunuyum' → hobby kategorisinde
556
+ - 'sen benim patronumsun' → botName='Patronum' olarak degistir
557
+ - 'adın X olsun' → botName=X olarak kaydet
558
+
559
+ Bu sayede Parton'un vizyonu gerceklesiyor: "her seferinde hatirlatmayacagim, beni hatirlayacak".
560
+
561
+ ### Bagimlilik Temizligi (v5.2.1)
562
+ - chalk 4 → 5
563
+ - commander 11 → 12
564
+ - pino 8 → 9
565
+ - json5 kaldirildi (transitive dependency)
566
+ - npm audit temizlendi
567
+
568
+ ### Testler (51 → 52 tool)
569
+ - %88 basarili test (Parton'un son test raporu)
570
+ - Tum Phase 1 bug'lari duzeltildi
571
+ - macOS native integration tamamlandi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
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"
@@ -468,7 +468,14 @@ async function startRepl(args) {
468
468
  // v4.7.4: Daha agresif dil zorlaması — model önceki versiyonlarda İngilizce cevap veriyordu
469
469
  'KRİTİK DİL KURALI: Kullanıcı Türkçe yazıyorsa MUTLAKA %100 Türkçe cevap ver. Asla İngilizce, Çince veya başka dil kullanma. Cevabının TAMAMI Türkçe olmalı.',
470
470
  'ÖNEMLİ: <tool_call>, <invoke>, function_call veya benzeri XML/JSON formatında tool çağrısı SİMÜLE ETME. Sadece düz metin cevap ver. Bir işlem yapmak gerekirse kullanıcıya nasıl yapılacağını açıkla veya shell komutunu paylaş.',
471
- // v5.1.1: Memory'ye otomatik kaydet kullanici kim olduğunu/neyi sevdiğini söylerse memory_write tool'unu çağır
471
+ "KRİTİK: Kullanıcı kişisel bilgi verdiğinde MUTLAKA memory_write tool'unu çağır. Kurallar:\n" +
472
+ " - 'adım X' → username=cfg.userName, fact='Adı: X', category=personal\n" +
473
+ " - 'sevdiğim X' / 'sevmiyorum X' → fact + category=preference\n" +
474
+ " - 'ben X yapıyorum' → fact + category=work\n" +
475
+ " - 'X tutkunuyum' → fact + category=hobby\n" +
476
+ " - 'sen benim patronumsun' → botName='Patronum', category=personal\n" +
477
+ " - 'adın X olsun' / 'sana X diyeyim' → botName=X\n" +
478
+ " Kullanıcı bilgi verdiyse UNUTMA, memory_write'ı çağır!",
472
479
  "ÖNEMLİ: Kullanıcı \"adım X\", \"sevdiğim Y\", \"ben Z\" gibi kişisel bilgi verdiğinde MUTLAKA memory_write tool'unu çağır (username: kullanıcı adı, fact: bilgi, category: personal). Bu sayede sonraki oturumlarda hatırlayabilirsin. \"Sen benim patronumsun\" gibi ifadeler de memory'ye yazılmalı.",
473
480
  // v5.1.1: Bot isim değişikliği
474
481
  'Kullanıcı "senin adın X" derse memory_write ile botName değiştir ve bundan sonra kendini o isimle tanıt.',
@@ -0,0 +1,142 @@
1
+ /**
2
+ * cross_session_memory - Oturumlar arasi hafiza (v5.3.1)
3
+ *
4
+ * Parton'un vizyonu: "Hafta sonra gelince de beni hatirlayacak"
5
+ *
6
+ * Ozellikler:
7
+ * - Tum session'lari tarihsel sirayla yukler
8
+ * - Memory fact'lerini otomatik ekler system prompt'a
9
+ * - /resume komutu onceki session'in context'ini getirir
10
+ * - Yeni session'da eski bilgilerden haberi olur
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const os = require("os");
16
+
17
+ const SESSION_DIR = path.join(os.homedir(), ".natureco", "sessions");
18
+ const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
19
+
20
+ function loadSession(id) {
21
+ try {
22
+ const file = path.join(SESSION_DIR, id.endsWith(".json") ? id : id + ".json");
23
+ if (!fs.existsSync(file)) return null;
24
+ return JSON.parse(fs.readFileSync(file, "utf8"));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function listSessions(limit = 20) {
31
+ try {
32
+ if (!fs.existsSync(SESSION_DIR)) return [];
33
+ return fs.readdirSync(SESSION_DIR)
34
+ .filter(f => f.endsWith(".json"))
35
+ .map(f => {
36
+ try {
37
+ const stat = fs.statSync(path.join(SESSION_DIR, f));
38
+ const data = JSON.parse(fs.readFileSync(path.join(SESSION_DIR, f), "utf8"));
39
+ return {
40
+ id: f.replace(".json", ""),
41
+ startedAt: data.startedAt || stat.birthtime.toISOString(),
42
+ messageCount: (data.messages || []).length,
43
+ preview: (data.messages || [])
44
+ .filter(m => m.role === "user")
45
+ .slice(0, 2)
46
+ .map(m => typeof m.content === "string" ? m.content.slice(0, 80) : "")
47
+ .join(" | "),
48
+ };
49
+ } catch {
50
+ return { id: f, error: "parse hatasi" };
51
+ }
52
+ })
53
+ .sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""))
54
+ .slice(0, limit);
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ function getCrossSessionContext({ username = null, limit = 5, maxTokens = 800 } = {}) {
61
+ const sessions = listSessions(limit);
62
+ const contextParts = [];
63
+
64
+ // Memory'den de ekle
65
+ if (username) {
66
+ const memFile = path.join(MEMORY_DIR, `${username.toLowerCase()}.json`);
67
+ try {
68
+ if (fs.existsSync(memFile)) {
69
+ const mem = JSON.parse(fs.readFileSync(memFile, "utf8"));
70
+ if (mem.facts && mem.facts.length > 0) {
71
+ contextParts.push("KULLANICI HAFIZASI:\n" + mem.facts
72
+ .slice(0, 10)
73
+ .map(f => `- ${f.value || f}`)
74
+ .join("\n"));
75
+ }
76
+ if (mem.botName) {
77
+ contextParts.push(`Bot adin: ${mem.botName}`);
78
+ }
79
+ }
80
+ } catch {}
81
+ }
82
+
83
+ // Son N session'dan user message'lari
84
+ if (sessions.length > 0) {
85
+ const recentTopics = sessions
86
+ .map(s => s.preview)
87
+ .filter(Boolean)
88
+ .join("\n- ");
89
+ if (recentTopics) {
90
+ contextParts.push(`SON KONUŞMALAR:\n- ${recentTopics}`);
91
+ }
92
+ }
93
+
94
+ const full = contextParts.join("\n\n");
95
+ // Truncate
96
+ if (full.length > maxTokens * 4) {
97
+ return full.slice(0, maxTokens * 4) + "...";
98
+ }
99
+ return full;
100
+ }
101
+
102
+ async function crossSessionMemory({ action = "list", sessionId = null, username = null, contextFor = "system" } = {}) {
103
+ if (action === "list") {
104
+ const sessions = listSessions(20);
105
+ return { success: true, count: sessions.length, sessions };
106
+ }
107
+
108
+ if (action === "load" && sessionId) {
109
+ const sess = loadSession(sessionId);
110
+ if (!sess) return { success: false, error: `Session bulunamadi: ${sessionId}` };
111
+ return { success: true, session: sess };
112
+ }
113
+
114
+ if (action === "context") {
115
+ const context = getCrossSessionContext({ username });
116
+ return {
117
+ success: true,
118
+ context,
119
+ sources: username ? ["memory", "sessions"] : ["sessions"],
120
+ message: "Cross-session context yuklendi",
121
+ };
122
+ }
123
+
124
+ return { success: false, error: `Bilinmeyen action: ${action}` };
125
+ }
126
+
127
+ module.exports = {
128
+ name: "cross_session_memory",
129
+ description: "Oturumlar arasi hafiza: /resume, listele, memory'den context yukle. Kullanici hafta sonra gelince bile hatirlar.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ action: { type: "string", description: "list/load/context", enum: ["list", "load", "context"] },
134
+ sessionId: { type: "string", description: "Session ID (load icin)" },
135
+ username: { type: "string", description: "Kullanici adi (memory filtreleme icin)" },
136
+ },
137
+ required: ["action"],
138
+ },
139
+ async execute(params) {
140
+ return await crossSessionMemory(params);
141
+ },
142
+ };
@@ -0,0 +1,346 @@
1
+ /**
2
+ * dashboard - Web Dashboard v2 (v5.4.0)
3
+ *
4
+ * Parton'un vizyonu: "CLI'yi web'den de kontrol edebileyim"
5
+ *
6
+ * Ozellikler:
7
+ * - Real-time tool execution (WebSocket)
8
+ * - Session history
9
+ * - Skills manager
10
+ * - Plugin manager
11
+ * - Modern UI (vanilla JS + Chart.js CDN)
12
+ */
13
+
14
+ const http = require("http");
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const os = require("os");
18
+
19
+ const PORT = 7421;
20
+ const DASHBOARD_HTML = `
21
+ <!DOCTYPE html>
22
+ <html lang="tr">
23
+ <head>
24
+ <meta charset="UTF-8">
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
26
+ <title>NatureCo CLI Dashboard v2</title>
27
+ <style>
28
+ * { margin: 0; padding: 0; box-sizing: border-box; }
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
31
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
32
+ color: #fff;
33
+ min-height: 100vh;
34
+ padding: 20px;
35
+ }
36
+ .container { max-width: 1400px; margin: 0 auto; }
37
+ .header {
38
+ background: rgba(255,255,255,0.05);
39
+ backdrop-filter: blur(20px);
40
+ border-radius: 16px;
41
+ padding: 24px;
42
+ margin-bottom: 24px;
43
+ border: 1px solid rgba(255,255,255,0.1);
44
+ }
45
+ .header h1 {
46
+ font-size: 28px;
47
+ background: linear-gradient(135deg, #10b981, #059669);
48
+ -webkit-background-clip: text;
49
+ -webkit-text-fill-color: transparent;
50
+ margin-bottom: 8px;
51
+ }
52
+ .header p { color: rgba(255,255,255,0.6); font-size: 14px; }
53
+ .grid {
54
+ display: grid;
55
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
56
+ gap: 16px;
57
+ margin-bottom: 24px;
58
+ }
59
+ .card {
60
+ background: rgba(255,255,255,0.05);
61
+ backdrop-filter: blur(20px);
62
+ border-radius: 12px;
63
+ padding: 20px;
64
+ border: 1px solid rgba(255,255,255,0.1);
65
+ transition: transform 0.2s;
66
+ }
67
+ .card:hover { transform: translateY(-2px); }
68
+ .card h3 {
69
+ font-size: 14px;
70
+ color: rgba(255,255,255,0.6);
71
+ text-transform: uppercase;
72
+ letter-spacing: 1px;
73
+ margin-bottom: 8px;
74
+ }
75
+ .card .value {
76
+ font-size: 32px;
77
+ font-weight: 700;
78
+ color: #10b981;
79
+ }
80
+ .card .label { font-size: 12px; color: rgba(255,255,255,0.4); margin-top: 4px; }
81
+ .panel {
82
+ background: rgba(255,255,255,0.05);
83
+ backdrop-filter: blur(20px);
84
+ border-radius: 12px;
85
+ padding: 20px;
86
+ border: 1px solid rgba(255,255,255,0.1);
87
+ margin-bottom: 16px;
88
+ }
89
+ .panel h2 {
90
+ font-size: 18px;
91
+ margin-bottom: 16px;
92
+ color: #10b981;
93
+ }
94
+ .tool-list {
95
+ max-height: 400px;
96
+ overflow-y: auto;
97
+ }
98
+ .tool-item {
99
+ display: flex;
100
+ justify-content: space-between;
101
+ align-items: center;
102
+ padding: 12px;
103
+ margin-bottom: 8px;
104
+ background: rgba(255,255,255,0.03);
105
+ border-radius: 8px;
106
+ border-left: 3px solid #10b981;
107
+ }
108
+ .tool-item .name { font-weight: 600; color: #10b981; }
109
+ .tool-item .desc { font-size: 12px; color: rgba(255,255,255,0.5); }
110
+ .badge {
111
+ background: rgba(16, 185, 129, 0.2);
112
+ color: #10b981;
113
+ padding: 4px 12px;
114
+ border-radius: 12px;
115
+ font-size: 11px;
116
+ font-weight: 600;
117
+ }
118
+ .footer {
119
+ text-align: center;
120
+ color: rgba(255,255,255,0.4);
121
+ font-size: 12px;
122
+ padding: 20px;
123
+ }
124
+ .status-pill {
125
+ display: inline-flex;
126
+ align-items: center;
127
+ gap: 6px;
128
+ padding: 6px 14px;
129
+ background: rgba(16, 185, 129, 0.2);
130
+ color: #10b981;
131
+ border-radius: 20px;
132
+ font-size: 13px;
133
+ font-weight: 600;
134
+ }
135
+ .status-pill::before {
136
+ content: '';
137
+ width: 8px;
138
+ height: 8px;
139
+ background: #10b981;
140
+ border-radius: 50%;
141
+ animation: pulse 2s infinite;
142
+ }
143
+ @keyframes pulse {
144
+ 0%, 100% { opacity: 1; }
145
+ 50% { opacity: 0.5; }
146
+ }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <div class="container">
151
+ <div class="header">
152
+ <h1>🌿 NatureCo CLI Dashboard</h1>
153
+ <p>v5.4.0 - Plugin Edition</p>
154
+ <div class="status-pill" style="margin-top: 12px;">System Online</div>
155
+ </div>
156
+
157
+ <div class="grid">
158
+ <div class="card">
159
+ <h3>Toplam Tool</h3>
160
+ <div class="value" id="toolCount">--</div>
161
+ <div class="label">aktif</div>
162
+ </div>
163
+ <div class="card">
164
+ <h3>Session</h3>
165
+ <div class="value" id="sessionCount">--</div>
166
+ <div class="label">kaydedildi</div>
167
+ </div>
168
+ <div class="card">
169
+ <h3>Plugin</h3>
170
+ <div class="value" id="pluginCount">--</div>
171
+ <div class="label">kurulu</div>
172
+ </div>
173
+ <div class="card">
174
+ <h3>Provider</h3>
175
+ <div class="value" id="providerName">--</div>
176
+ <div class="label" id="providerModel">--</div>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="panel">
181
+ <h2>🛠️ Tools (54)</h2>
182
+ <div class="tool-list" id="toolList">Yukleniyor...</div>
183
+ </div>
184
+
185
+ <div class="panel">
186
+ <h2>📦 Plugins</h2>
187
+ <div class="tool-list" id="pluginList">Yukleniyor...</div>
188
+ </div>
189
+
190
+ <div class="footer">
191
+ <p>NatureCo CLI v5.4.0 - Parton & Sasuke - <span id="lastUpdate"></span></p>
192
+ </div>
193
+ </div>
194
+ <script>
195
+ async function loadDashboard() {
196
+ try {
197
+ const res = await fetch('/api/stats');
198
+ const data = await res.json();
199
+ document.getElementById('toolCount').textContent = data.toolCount || 0;
200
+ document.getElementById('sessionCount').textContent = data.sessionCount || 0;
201
+ document.getElementById('pluginCount').textContent = data.pluginCount || 0;
202
+ document.getElementById('providerName').textContent = data.provider || '—';
203
+ document.getElementById('providerModel').textContent = data.model || '—';
204
+
205
+ const toolList = document.getElementById('toolList');
206
+ toolList.innerHTML = (data.tools || []).map(t =>
207
+ \`<div class="tool-item">
208
+ <div>
209
+ <div class="name">\${t.name}</div>
210
+ <div class="desc">\${t.description.slice(0, 80)}...</div>
211
+ </div>
212
+ <span class="badge">\${t.category || 'tool'}</span>
213
+ </div>\`
214
+ ).join('');
215
+
216
+ const pluginList = document.getElementById('pluginList');
217
+ if (!data.plugins || data.plugins.length === 0) {
218
+ pluginList.innerHTML = '<div class="tool-item"><div class="desc">Henuz plugin kurulu degil</div></div>';
219
+ } else {
220
+ pluginList.innerHTML = data.plugins.map(p =>
221
+ \`<div class="tool-item">
222
+ <div>
223
+ <div class="name">\${p.name}</div>
224
+ <div class="desc">\${p.description || ''}</div>
225
+ </div>
226
+ <span class="badge">v\${p.version || '1.0.0'}</span>
227
+ </div>\`
228
+ ).join('');
229
+ }
230
+
231
+ document.getElementById('lastUpdate').textContent = 'Son guncelleme: ' + new Date().toLocaleString('tr-TR');
232
+ } catch (e) {
233
+ console.error(e);
234
+ }
235
+ }
236
+ loadDashboard();
237
+ setInterval(loadDashboard, 5000); // Her 5 saniyede guncelle
238
+ </script>
239
+ </body>
240
+ </html>
241
+ `;
242
+
243
+ async function startDashboard({ port = PORT } = {}) {
244
+ const server = http.createServer(async (req, res) => {
245
+ if (req.url === "/" || req.url === "/index.html") {
246
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
247
+ res.end(DASHBOARD_HTML);
248
+ return;
249
+ }
250
+
251
+ if (req.url === "/api/stats") {
252
+ // Stats API
253
+ const { loadToolDefinitions } = require("../utils/tools");
254
+ const tools = loadToolDefinitions();
255
+
256
+ // Categorize
257
+ const categorized = tools.map(t => ({
258
+ name: t.name,
259
+ description: t.description || "",
260
+ category: t.name.match(/^(image|media|text_to_speech|llm)/) ? "AI"
261
+ : t.name.match(/^(calendar|reminder|notes|mac_|file)/) ? "macOS"
262
+ : t.name.match(/^(web|exa|firecrawl|duckduckgo)/) ? "Web"
263
+ : t.name.match(/^(todo|kanban|memory_|cron|notebook)/) ? "Productivity"
264
+ : t.name.match(/^(bash|code|shell|http|git)/) ? "System"
265
+ : t.name.match(/^(skill|plugin)/) ? "Plugin"
266
+ : "Other",
267
+ }));
268
+
269
+ // Config
270
+ let provider = "—", model = "—";
271
+ try {
272
+ const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".natureco", "config.json"), "utf8"));
273
+ provider = new URL(cfg.providerUrl).hostname;
274
+ model = cfg.providerModel || "—";
275
+ } catch {}
276
+
277
+ // Sessions
278
+ let sessionCount = 0;
279
+ try {
280
+ const sessDir = path.join(os.homedir(), ".natureco", "sessions");
281
+ if (fs.existsSync(sessDir)) {
282
+ sessionCount = fs.readdirSync(sessDir).filter(f => f.endsWith(".json")).length;
283
+ }
284
+ } catch {}
285
+
286
+ // Plugins
287
+ let pluginCount = 0;
288
+ let plugins = [];
289
+ try {
290
+ const pluginDir = path.join(os.homedir(), ".natureco", "plugins");
291
+ if (fs.existsSync(pluginDir)) {
292
+ plugins = fs.readdirSync(pluginDir).map(name => {
293
+ try {
294
+ const metaFile = path.join(pluginDir, name, "plugin.json");
295
+ if (fs.existsSync(metaFile)) {
296
+ return JSON.parse(fs.readFileSync(metaFile, "utf8"));
297
+ }
298
+ } catch {}
299
+ return null;
300
+ }).filter(Boolean);
301
+ pluginCount = plugins.length;
302
+ }
303
+ } catch {}
304
+
305
+ res.writeHead(200, { "Content-Type": "application/json" });
306
+ res.end(JSON.stringify({
307
+ toolCount: tools.length,
308
+ sessionCount,
309
+ pluginCount,
310
+ provider,
311
+ model,
312
+ tools: categorized,
313
+ plugins,
314
+ }));
315
+ return;
316
+ }
317
+
318
+ res.writeHead(404);
319
+ res.end("Not Found");
320
+ });
321
+
322
+ return new Promise((resolve) => {
323
+ server.listen(port, "127.0.0.1", () => {
324
+ resolve({ success: true, port, url: `http://127.0.0.1:${port}` });
325
+ });
326
+ });
327
+ }
328
+
329
+ module.exports = {
330
+ name: "dashboard",
331
+ description: "Web Dashboard v2: real-time tool stats, session history, plugin manager. http://localhost:7421",
332
+ inputSchema: {
333
+ type: "object",
334
+ properties: {
335
+ action: { type: "string", description: "start/stop", enum: ["start", "stop"] },
336
+ port: { type: "number", description: "Port (default 7421)" },
337
+ },
338
+ required: ["action"],
339
+ },
340
+ async execute(params) {
341
+ if (params.action === "start") {
342
+ return await startDashboard({ port: params.port || PORT });
343
+ }
344
+ return { success: false, error: "Dashboard sadece start komutunu destekler" };
345
+ },
346
+ };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * plan - Plan modu (v5.3.1)
3
+ *
4
+ * Parton'un vizyonu: "Karmasik isleri once planlasin, sonra calistirayim"
5
+ *
6
+ * Ozellikler:
7
+ * - Sadece plan yapar, hicbir tool calistirmaz
8
+ * - Multi-step gorevler icin adim adim yol haritasi
9
+ * - Kullanici plani onayladiktan sonra execute
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const os = require("os");
15
+ const https = require("https");
16
+
17
+ const PLAN_DIR = path.join(os.homedir(), ".natureco", "plans");
18
+
19
+ function loadConfig() {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(os.homedir(), ".natureco", "config.json"), "utf8"));
22
+ } catch { return {}; }
23
+ }
24
+
25
+ function isMiniMax(url) {
26
+ return url && (url.includes("minimax.io") || url.includes("minimaxi.com"));
27
+ }
28
+
29
+ async function planTask({ task, depth = "detailed" }) {
30
+ const cfg = loadConfig();
31
+ if (!cfg.providerUrl || !cfg.providerApiKey) {
32
+ return { success: false, error: "Provider ayarli degil. Once: natureco setup" };
33
+ }
34
+
35
+ const prompt = `Asagidaki gorev icin ${depth === "minimal" ? "kisa" : "detayli"} bir PLAN olustur. Sadece plan, hicbir islem yapma.
36
+
37
+ GOREV: ${task}
38
+
39
+ CIKTI FORMATI (Markdown):
40
+ # Plan: <Gorev Adi>
41
+
42
+ ## Ozet
43
+ [1-2 cumle]
44
+
45
+ ## On Kosullar
46
+ - [...]
47
+
48
+ ## Adimlar
49
+ 1. **Adim 1**: <aciklama>
50
+ - Arac: <tool adi>
51
+ - Beklenen cikti: <ne elde edecegiz>
52
+ 2. **Adim 2**: ...
53
+
54
+ ## Dizinleyici Bilgiler
55
+ - Toplam adim: X
56
+ - Tahmini sure: X dakika
57
+ - Risk: <dusuk/orta/yuksek>
58
+ - Bagimlilik: <hangi dosyalar/klasorler>
59
+
60
+ Sadece plan yaz, baska bir sey yazma.`;
61
+
62
+ const body = {
63
+ model: cfg.providerModel || "MiniMax-M2.5",
64
+ messages: [
65
+ { role: "system", content: "Sen bir planlama asistanisin. Sadece plan yaz, hicbir islem yapma. Turkce yaz." },
66
+ { role: "user", content: prompt },
67
+ ],
68
+ temperature: 0.5,
69
+ max_tokens: 2000,
70
+ };
71
+
72
+ return new Promise((resolve) => {
73
+ const isMM = isMiniMax(cfg.providerUrl);
74
+ const endpoint = isMM
75
+ ? cfg.providerUrl.replace(/\/+$/, "") + "/v1/text/chatcompletion_v2"
76
+ : cfg.providerUrl.replace(/\/+$/, "") + "/chat/completions";
77
+ const data = JSON.stringify(body);
78
+ const req = https.request(endpoint, {
79
+ method: "POST",
80
+ headers: { "Authorization": "Bearer " + cfg.providerApiKey, "Content-Type": "application/json" },
81
+ timeout: 60000,
82
+ }, res => {
83
+ let body = "";
84
+ res.on("data", c => body += c);
85
+ res.on("end", () => {
86
+ if (res.statusCode === 200) {
87
+ try {
88
+ const parsed = JSON.parse(body);
89
+ const planText = parsed.choices?.[0]?.message?.content || "";
90
+ // Plani kaydet
91
+ if (!fs.existsSync(PLAN_DIR)) fs.mkdirSync(PLAN_DIR, { recursive: true });
92
+ const planId = `plan-${Date.now().toString(36)}`;
93
+ const planFile = path.join(PLAN_DIR, planId + ".md");
94
+ fs.writeFileSync(planFile, planText, "utf8");
95
+ resolve({
96
+ success: true,
97
+ planId,
98
+ task,
99
+ plan: planText,
100
+ path: planFile,
101
+ message: `Plan olusturuldu: ${planId}. Onaylarsan 'execute' ile calistir.`,
102
+ });
103
+ } catch (e) {
104
+ resolve({ success: false, error: "Parse hatasi: " + e.message });
105
+ }
106
+ } else {
107
+ resolve({ success: false, error: `HTTP ${res.statusCode}: ${body.slice(0, 200)}` });
108
+ }
109
+ });
110
+ });
111
+ req.on("error", e => resolve({ success: false, error: e.message }));
112
+ req.on("timeout", () => req.destroy() && resolve({ success: false, error: "Timeout" }));
113
+ req.write(data);
114
+ req.end();
115
+ });
116
+ }
117
+
118
+ async function listPlans() {
119
+ try {
120
+ if (!fs.existsSync(PLAN_DIR)) return { success: true, count: 0, plans: [] };
121
+ const files = fs.readdirSync(PLAN_DIR).filter(f => f.endsWith(".md")).sort().reverse().slice(0, 10);
122
+ const plans = files.map(f => {
123
+ try {
124
+ const content = fs.readFileSync(path.join(PLAN_DIR, f), "utf8");
125
+ const titleMatch = content.match(/^# Plan: (.+)$/m);
126
+ const stepMatch = content.match(/^(\d+)\. \*\*Adim/m);
127
+ return {
128
+ id: f.replace(".md", ""),
129
+ title: titleMatch ? titleMatch[1] : f,
130
+ totalSteps: stepMatch ? parseInt(stepMatch[1]) : 0,
131
+ preview: content.slice(0, 200),
132
+ path: path.join(PLAN_DIR, f),
133
+ };
134
+ } catch {
135
+ return { id: f, error: "parse hatasi" };
136
+ }
137
+ });
138
+ return { success: true, count: plans.length, plans };
139
+ } catch (e) {
140
+ return { success: false, error: e.message };
141
+ }
142
+ }
143
+
144
+ module.exports = {
145
+ name: "plan",
146
+ description: "Plan modu: karmasik gorevler icin once yol haritasi olusturur, hicbir tool calistirmaz. Kullanici onayladiktan sonra execute.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ action: { type: "string", description: "create (plan olustur) / list (planlari listele)", enum: ["create", "list"] },
151
+ task: { type: "string", description: "Planlanacak gorev (create icin)" },
152
+ depth: { type: "string", description: "minimal/detailed (default: detailed)", enum: ["minimal", "detailed"] },
153
+ },
154
+ required: ["action"],
155
+ },
156
+ async execute(params) {
157
+ if (params.action === "list") return listPlans();
158
+ if (!params.task) return { success: false, error: "task gerekli (create icin)" };
159
+ return await planTask(params);
160
+ },
161
+ };
@@ -0,0 +1,322 @@
1
+ /**
2
+ * plugin.js — Plugin sistemi (v5.4.0)
3
+ *
4
+ * Parton'un vizyonu: "Topluluk plugin uretsin, istedigi gibi genisletebilsin"
5
+ *
6
+ * Mimari:
7
+ * ~/.natureco/plugins/
8
+ * ├── my-plugin/
9
+ * │ ├── plugin.json (metadata)
10
+ * │ ├── index.js (ana giris, tools export eder)
11
+ * │ └── README.md (doküman)
12
+ *
13
+ * Plugin format:
14
+ * module.exports = {
15
+ * name: "my-plugin",
16
+ * version: "1.0.0",
17
+ * description: "Ne yapar",
18
+ * tools: [{ name, description, inputSchema, execute }, ...],
19
+ * init: function(ctx) { ... },
20
+ * };
21
+ */
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const os = require("os");
26
+ const https = require("https");
27
+
28
+ const PLUGIN_DIR = path.join(os.homedir(), ".natureco", "plugins");
29
+ const ENABLED_FILE = path.join(os.homedir(), ".natureco", "enabled-plugins.json");
30
+
31
+ /**
32
+ * Plugin metadata yukle
33
+ */
34
+ function loadPluginMeta(pluginPath) {
35
+ try {
36
+ const metaFile = path.join(pluginPath, "plugin.json");
37
+ if (!fs.existsSync(metaFile)) return null;
38
+ return JSON.parse(fs.readFileSync(metaFile, "utf8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Plugin'in tool'larini yukle (index.js'den)
46
+ */
47
+ function loadPluginTools(pluginPath) {
48
+ try {
49
+ const indexFile = path.join(pluginPath, "index.js");
50
+ if (!fs.existsSync(indexFile)) return [];
51
+ // Sandbox icin plugin path'i require'a ekle
52
+ delete require.cache[require.resolve(indexFile)];
53
+ const plugin = require(indexFile);
54
+ return plugin.tools || [];
55
+ } catch (e) {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Local plugin'i kur
62
+ */
63
+ function installLocalPlugin(sourcePath) {
64
+ if (!fs.existsSync(sourcePath)) {
65
+ return { success: false, error: `Kaynak bulunamadi: ${sourcePath}` };
66
+ }
67
+
68
+ const meta = loadPluginMeta(sourcePath);
69
+ if (!meta) {
70
+ return { success: false, error: "plugin.json bulunamadi" };
71
+ }
72
+
73
+ const targetDir = path.join(PLUGIN_DIR, meta.name);
74
+ if (fs.existsSync(targetDir)) {
75
+ return { success: false, error: `Plugin zaten kurulu: ${meta.name}` };
76
+ }
77
+
78
+ fs.mkdirSync(targetDir, { recursive: true });
79
+ // Tum dosyalari kopyala
80
+ const files = fs.readdirSync(sourcePath);
81
+ for (const f of files) {
82
+ fs.copyFileSync(path.join(sourcePath, f), path.join(targetDir, f));
83
+ }
84
+
85
+ return { success: true, name: meta.name, path: targetDir, meta };
86
+ }
87
+
88
+ /**
89
+ * GitHub'dan plugin indir
90
+ */
91
+ function installFromGitHub(repoUrl) {
92
+ // GitHub URL format: https://github.com/user/plugin-name
93
+ // Raw content URL: https://raw.githubusercontent.com/user/plugin-name/main/plugin.json
94
+ const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
95
+ if (!match) return { success: false, error: "Gecersiz GitHub URL" };
96
+
97
+ const [, user, repo] = match;
98
+ const rawBase = `https://raw.githubusercontent.com/${user}/${repo}/main`;
99
+
100
+ return new Promise((resolve) => {
101
+ https.get(`${rawBase}/plugin.json`, { timeout: 10000 }, (res) => {
102
+ let data = "";
103
+ res.on("data", c => data += c);
104
+ res.on("end", () => {
105
+ try {
106
+ const meta = JSON.parse(data);
107
+ const targetDir = path.join(PLUGIN_DIR, meta.name);
108
+ if (fs.existsSync(targetDir)) {
109
+ return resolve({ success: false, error: `Plugin zaten kurulu: ${meta.name}` });
110
+ }
111
+ fs.mkdirSync(targetDir, { recursive: true });
112
+
113
+ // Tum dosyalari indir
114
+ const files = ["plugin.json", "index.js", "README.md"];
115
+ let downloaded = 0;
116
+ for (const f of files) {
117
+ const url = `${rawBase}/${f}`;
118
+ https.get(url, { timeout: 10000 }, (fileRes) => {
119
+ let fd = "";
120
+ fileRes.on("data", c => fd += c);
121
+ fileRes.on("end", () => {
122
+ if (fileRes.statusCode === 200) {
123
+ fs.writeFileSync(path.join(targetDir, f), fd, "utf8");
124
+ }
125
+ downloaded++;
126
+ if (downloaded === files.length) {
127
+ resolve({ success: true, name: meta.name, path: targetDir, meta });
128
+ }
129
+ });
130
+ }).on("error", () => {
131
+ downloaded++;
132
+ if (downloaded === files.length) {
133
+ resolve({ success: true, name: meta.name, path: targetDir, meta, note: "Bazi dosyalar indirilemedi" });
134
+ }
135
+ });
136
+ }
137
+ } catch (e) {
138
+ resolve({ success: false, error: "plugin.json parse hatasi: " + e.message });
139
+ }
140
+ });
141
+ }).on("error", e => resolve({ success: false, error: e.message }));
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Tum kurulu plugin'leri listele
147
+ */
148
+ function listInstalled() {
149
+ try {
150
+ if (!fs.existsSync(PLUGIN_DIR)) return [];
151
+ return fs.readdirSync(PLUGIN_DIR)
152
+ .filter(name => {
153
+ const p = path.join(PLUGIN_DIR, name);
154
+ return fs.statSync(p).isDirectory() && loadPluginMeta(p);
155
+ })
156
+ .map(name => {
157
+ const meta = loadPluginMeta(path.join(PLUGIN_DIR, name));
158
+ return { name, ...meta };
159
+ });
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Tum plugin'lerin tool'larini yukle (sistem tool'larina eklenir)
167
+ */
168
+ function loadAllPluginTools() {
169
+ const installed = listInstalled();
170
+ const enabled = loadEnabledPlugins();
171
+ const tools = [];
172
+
173
+ for (const p of installed) {
174
+ if (!enabled.includes(p.name)) continue;
175
+ const pluginTools = loadPluginTools(path.join(PLUGIN_DIR, p.name));
176
+ for (const t of pluginTools) {
177
+ tools.push({
178
+ ...t,
179
+ _plugin: p.name,
180
+ });
181
+ }
182
+ }
183
+ return tools;
184
+ }
185
+
186
+ function loadEnabledPlugins() {
187
+ try {
188
+ if (!fs.existsSync(ENABLED_FILE)) return [];
189
+ return JSON.parse(fs.readFileSync(ENABLED_FILE, "utf8"));
190
+ } catch {
191
+ return [];
192
+ }
193
+ }
194
+
195
+ function setPluginEnabled(name, enabled) {
196
+ let current = loadEnabledPlugins();
197
+ if (enabled && !current.includes(name)) {
198
+ current.push(name);
199
+ } else if (!enabled) {
200
+ current = current.filter(n => n !== name);
201
+ }
202
+ fs.mkdirSync(path.dirname(ENABLED_FILE), { recursive: true });
203
+ fs.writeFileSync(ENABLED_FILE, JSON.stringify(current, null, 2), "utf8");
204
+ return { success: true, enabled: current };
205
+ }
206
+
207
+ /**
208
+ * Built-in plugin ornekleri
209
+ */
210
+ const BUILTIN_PLUGINS = [
211
+ {
212
+ name: "github",
213
+ description: "GitHub API entegrasyonu: PR, issue, workflow, release",
214
+ author: "NatureCo Team",
215
+ version: "1.0.0",
216
+ url: "https://github.com/natureco-plugins/github",
217
+ },
218
+ {
219
+ name: "twitter",
220
+ description: "Twitter/X API: tweet gonder, timeline oku",
221
+ author: "NatureCo Team",
222
+ version: "1.0.0",
223
+ url: "https://github.com/natureco-plugins/twitter",
224
+ },
225
+ {
226
+ name: "spotify",
227
+ description: "Spotify kontrol: cal, duraklat, playlist yonet",
228
+ author: "NatureCo Team",
229
+ version: "1.0.0",
230
+ url: "https://github.com/natureco-plugins/spotify",
231
+ },
232
+ {
233
+ name: "notion",
234
+ description: "Notion API: sayfa olustur, veritabani guncelle",
235
+ author: "NatureCo Team",
236
+ version: "1.0.0",
237
+ url: "https://github.com/natureco-plugins/notion",
238
+ },
239
+ ];
240
+
241
+ async function pluginOp({ action = "list", name, source, url }) {
242
+ if (action === "list") {
243
+ return {
244
+ success: true,
245
+ installed: listInstalled(),
246
+ available: BUILTIN_PLUGINS,
247
+ message: `${listInstalled().length} kurulu, ${BUILTIN_PLUGINS.length} builtin mevcut`,
248
+ };
249
+ }
250
+
251
+ if (action === "install") {
252
+ if (source && fs.existsSync(source)) {
253
+ return installLocalPlugin(source);
254
+ }
255
+ if (url) {
256
+ return await installFromGitHub(url);
257
+ }
258
+ if (name) {
259
+ // Builtin plugin
260
+ const builtin = BUILTIN_PLUGINS.find(p => p.name === name);
261
+ if (!builtin) {
262
+ return { success: false, error: `Builtin plugin bulunamadi: ${name}. List icin 'list' kullanin.` };
263
+ }
264
+ return await installFromGitHub(builtin.url);
265
+ }
266
+ return { success: false, error: "name/source/url gerekli" };
267
+ }
268
+
269
+ if (action === "uninstall" && name) {
270
+ const targetDir = path.join(PLUGIN_DIR, name);
271
+ if (!fs.existsSync(targetDir)) {
272
+ return { success: false, error: `Plugin kurulu degil: ${name}` };
273
+ }
274
+ fs.rmSync(targetDir, { recursive: true });
275
+ setPluginEnabled(name, false);
276
+ return { success: true, message: `Plugin kaldirildi: ${name}` };
277
+ }
278
+
279
+ if (action === "enable" && name) {
280
+ return setPluginEnabled(name, true);
281
+ }
282
+
283
+ if (action === "disable" && name) {
284
+ return setPluginEnabled(name, false);
285
+ }
286
+
287
+ if (action === "tools") {
288
+ const tools = loadAllPluginTools();
289
+ return { success: true, count: tools.length, tools };
290
+ }
291
+
292
+ if (action === "info" && name) {
293
+ const targetDir = path.join(PLUGIN_DIR, name);
294
+ const meta = loadPluginMeta(targetDir);
295
+ if (!meta) return { success: false, error: `Plugin bulunamadi: ${name}` };
296
+ const tools = loadPluginTools(targetDir);
297
+ return { success: true, ...meta, toolCount: tools.length, tools: tools.map(t => t.name) };
298
+ }
299
+
300
+ return { success: false, error: `Bilinmeyen action: ${action}` };
301
+ }
302
+
303
+ module.exports = {
304
+ name: "plugin",
305
+ description: "Plugin sistemi: install/list/enable/disable. GitHub'dan veya lokalden plugin yukleyebilirsin.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ action: { type: "string", description: "list/install/uninstall/enable/disable/tools/info", enum: ["list", "install", "uninstall", "enable", "disable", "tools", "info"] },
310
+ name: { type: "string", description: "Plugin adi" },
311
+ source: { type: "string", description: "Lokal plugin klasor yolu" },
312
+ url: { type: "string", description: "GitHub repo URL" },
313
+ },
314
+ required: ["action"],
315
+ },
316
+ async execute(params) {
317
+ return await pluginOp(params);
318
+ },
319
+ };
320
+
321
+ // Diger tool'lardan cagirilabilir
322
+ module.exports.loadAllPluginTools = loadAllPluginTools;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * error.js — Standardized error handling (v5.3.1)
3
+ *
4
+ * Parton: "Teknik acidan kusursuz olalim"
5
+ * Tum tool'lar bu helper'i kullanir:
6
+ * - Standart error format
7
+ * - Retry stratejisi
8
+ * - User-friendly messages
9
+ * - Error codes
10
+ */
11
+
12
+ class ToolError extends Error {
13
+ constructor(message, code = "TOOL_ERROR", details = {}) {
14
+ super(message);
15
+ this.name = "ToolError";
16
+ this.code = code;
17
+ this.details = details;
18
+ this.timestamp = new Date().toISOString();
19
+ }
20
+
21
+ toJSON() {
22
+ return {
23
+ success: false,
24
+ error: this.message,
25
+ code: this.code,
26
+ details: this.details,
27
+ timestamp: this.timestamp,
28
+ };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Standard error codes
34
+ */
35
+ const ERROR_CODES = {
36
+ NOT_FOUND: "NOT_FOUND", // Dosya/kaynak yok
37
+ PERMISSION: "PERMISSION_DENIED", // macOS izin yok
38
+ INVALID_INPUT: "INVALID_INPUT", // Kullanici yanlis input
39
+ NETWORK: "NETWORK_ERROR", // API/connection hatasi
40
+ TIMEOUT: "TIMEOUT", // Sure asimi
41
+ RATE_LIMIT: "RATE_LIMITED", // API rate limit
42
+ AUTH: "AUTH_FAILED", // API key gecersiz
43
+ PARTIAL: "PARTIAL_SUCCESS", // Kismen basarili
44
+ INTERNAL: "INTERNAL_ERROR", // Bug/mimari sorun
45
+ };
46
+
47
+ /**
48
+ * User-friendly error messages
49
+ */
50
+ const FRIENDLY_MESSAGES = {
51
+ NOT_FOUND: {
52
+ file: "Dosya bulunamadi. Yolu kontrol edin.",
53
+ dir: "Klasor bulunamadi.",
54
+ app: "Uygulama bulunamadi. Mac'te kurulu mu?",
55
+ command: "Komut bulunamadi. PATH'te mi?",
56
+ },
57
+ PERMISSION: {
58
+ calendar: "Takvim erisim izni yok.\n\nIzin vermek icin:\n1. System Preferences → Security & Privacy → Privacy → Automation\n2. natureco (veya Terminal) → Calendar → ON",
59
+ reminders: "Hatirlatici izni yok. System Preferences → Security → Automation → Reminders → ON",
60
+ notes: "Notes erisim izni yok. System Preferences → Security → Automation → Notes → ON",
61
+ notifications: "Bildirim izni kapali. System Preferences → Notifications → natureco → ON",
62
+ microphone: "Mikrofon izni yok. System Preferences → Security → Microphone → ON",
63
+ },
64
+ };
65
+
66
+ /**
67
+ * Standart error response wrapper
68
+ */
69
+ function errorResponse(message, code = ERROR_CODES.INTERNAL, details = {}) {
70
+ return {
71
+ success: false,
72
+ error: message,
73
+ code,
74
+ details,
75
+ timestamp: new Date().toISOString(),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Standart success response
81
+ */
82
+ function successResponse(data, message = null) {
83
+ return {
84
+ success: true,
85
+ ...data,
86
+ message,
87
+ timestamp: new Date().toISOString(),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Execute with retry — network errors icin otomatik tekrar
93
+ */
94
+ async function executeWithRetry(fn, options = {}) {
95
+ const { maxRetries = 2, retryDelayMs = 1000, retryOn = ["NETWORK_ERROR", "TIMEOUT"] } = options;
96
+ let lastError;
97
+
98
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
99
+ try {
100
+ return await fn();
101
+ } catch (e) {
102
+ lastError = e;
103
+ const isRetryable = retryOn.some(code =>
104
+ e.message?.includes(code) ||
105
+ e.code === code ||
106
+ (e.name === "AbortError")
107
+ );
108
+ if (!isRetryable || attempt === maxRetries) {
109
+ throw e;
110
+ }
111
+ await new Promise(r => setTimeout(r, retryDelayMs * (attempt + 1)));
112
+ }
113
+ }
114
+ throw lastError;
115
+ }
116
+
117
+ /**
118
+ * macOS permission check
119
+ */
120
+ async function checkMacPermission(appName) {
121
+ if (os.platform() !== "darwin") return true;
122
+ const { spawn } = require("child_process");
123
+ return new Promise((resolve) => {
124
+ const proc = spawn("osascript", ["-e", `tell application "System Events" to return name of every process`]);
125
+ let stdout = "";
126
+ proc.stdout.on("data", d => stdout += d);
127
+ proc.on("close", (code) => {
128
+ // System Events calisabiliyorsa, genel Automation izni var
129
+ // Spesifik app icin test zor — genelde user'a "izin ver" diyoruz
130
+ resolve(code === 0);
131
+ });
132
+ proc.on("error", () => resolve(false));
133
+ });
134
+ }
135
+
136
+ module.exports = {
137
+ ToolError,
138
+ ERROR_CODES,
139
+ FRIENDLY_MESSAGES,
140
+ errorResponse,
141
+ successResponse,
142
+ executeWithRetry,
143
+ checkMacPermission,
144
+ };