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 +32 -0
- package/package.json +1 -1
- package/src/commands/repl.js +8 -1
- package/src/tools/cross_session_memory.js +142 -0
- package/src/tools/dashboard.js +346 -0
- package/src/tools/plan.js +161 -0
- package/src/tools/plugin.js +322 -0
- package/src/utils/error.js +144 -0
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
|
+
"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"
|
package/src/commands/repl.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
};
|