natureco-cli 5.2.1 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md 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.2.1",
3
+ "version": "5.3.1",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -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,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,178 @@
1
+ /**
2
+ * voice_chat - Sesli asistan (v5.3.0)
3
+ *
4
+ * Parton'un vizyonu: "Bilgisayarla konusayim"
5
+ *
6
+ * Mikrofon → Whisper STT → REPL'e metin olarak gönder
7
+ * Bot cevabı → TTS ile sesli oku
8
+ *
9
+ * macOS icin: afplay + sox/rec + Whisper
10
+ * Cross-platform: openai-whisper API veya local whisper.cpp
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const os = require("os");
15
+ const path = require("path");
16
+ const { spawn } = require("child_process");
17
+ const https = require("https");
18
+
19
+ const IS_MAC = os.platform() === "darwin";
20
+
21
+ /**
22
+ * Whisper API ile sesi metne cevir
23
+ */
24
+ function whisperTranscribe(audioBuffer, provider = "openai") {
25
+ return new Promise((resolve, reject) => {
26
+ const apiKey = process.env.OPENAI_API_KEY || process.env.WHISPER_API_KEY;
27
+ if (!apiKey) {
28
+ reject(new Error("OPENAI_API_KEY gerekli (Whisper API icin)"));
29
+ return;
30
+ }
31
+
32
+ // multipart/form-data olustur
33
+ const boundary = "----formdata-" + Date.now();
34
+ const filename = "/tmp/audio-" + Date.now() + ".wav";
35
+
36
+ // Basit multipart builder
37
+ let body = Buffer.alloc(0);
38
+ body = Buffer.concat([body, Buffer.from(`--${boundary}\r\n`)]);
39
+ body = Buffer.concat([body, Buffer.from(`Content-Disposition: form-data; name="file"; filename="audio.wav"\r\n`)]);
40
+ body = Buffer.concat([body, Buffer.from(`Content-Type: audio/wav\r\n\r\n`)]);
41
+ body = Buffer.concat([body, audioBuffer]);
42
+ body = Buffer.concat([body, Buffer.from(`\r\n--${boundary}\r\n`)]);
43
+ body = Buffer.concat([body, Buffer.from(`Content-Disposition: form-data; name="model"\r\n\r\n`)]);
44
+ body = Buffer.concat([body, Buffer.from(`whisper-1`)]);
45
+ body = Buffer.concat([body, Buffer.from(`\r\n--${boundary}\r\n`)]);
46
+ body = Buffer.concat([body, Buffer.from(`Content-Disposition: form-data; name="language"\r\n\r\n`)]);
47
+ body = Buffer.concat([body, Buffer.from(`tr`)]);
48
+ body = Buffer.concat([body, Buffer.from(`\r\n--${boundary}--\r\n`)]);
49
+
50
+ const req = https.request({
51
+ hostname: "api.openai.com",
52
+ port: 443,
53
+ path: "/v1/audio/transcriptions",
54
+ method: "POST",
55
+ headers: {
56
+ "Authorization": `Bearer ${apiKey}`,
57
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
58
+ "Content-Length": body.length,
59
+ },
60
+ timeout: 30000,
61
+ }, res => {
62
+ let data = "";
63
+ res.on("data", c => data += c);
64
+ res.on("end", () => {
65
+ try {
66
+ const parsed = JSON.parse(data);
67
+ resolve(parsed.text || "");
68
+ } catch (e) {
69
+ reject(new Error("Whisper API yanit parse hatasi: " + data.slice(0, 200)));
70
+ }
71
+ });
72
+ });
73
+ req.on("error", reject);
74
+ req.on("timeout", () => req.destroy() && reject(new Error("Whisper timeout")));
75
+ req.write(body);
76
+ req.end();
77
+ });
78
+ }
79
+
80
+ /**
81
+ * macOS'ta mikrofondan ses kaydet
82
+ * "sox" veya "rec" komutunu kullanir
83
+ */
84
+ function recordMac(durationSec = 5) {
85
+ return new Promise((resolve, reject) => {
86
+ const outFile = path.join(os.tmpdir(), `voice-${Date.now()}.wav`);
87
+
88
+ // sox varsa onu kullan, yoksa built-in "rec" (sox)
89
+ const proc = spawn("rec", [
90
+ "-r", "16000", // 16kHz (Whisper icin ideal)
91
+ "-c", "1", // mono
92
+ "-b", "16", // 16-bit
93
+ outFile,
94
+ "trim", "0", String(durationSec), // sure
95
+ ], { timeout: durationSec * 1000 + 5000 });
96
+
97
+ proc.on("close", (code) => {
98
+ if (code === 0 && fs.existsSync(outFile)) {
99
+ resolve(outFile);
100
+ } else {
101
+ reject(new Error("Ses kaydi basarisiz. Kur: brew install sox"));
102
+ }
103
+ });
104
+ proc.on("error", (e) => reject(new Error("'rec' komutu bulunamadi. brew install sox ile kur")));
105
+ });
106
+ }
107
+
108
+ /**
109
+ * macOS'ta text-to-speech (say)
110
+ */
111
+ function macSay(text) {
112
+ return new Promise((resolve) => {
113
+ if (!text) return resolve({ success: false, error: "text bos" });
114
+ const proc = spawn("say", ["-v", "Yelda", text]);
115
+ proc.on("close", (code) => {
116
+ if (code === 0) resolve({ success: true, provider: "mac-say" });
117
+ else resolve({ success: false, error: `say exit ${code}` });
118
+ });
119
+ proc.on("error", (e) => resolve({ success: false, error: e.message }));
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Voice chat — record → transcribe → reply → speak
125
+ */
126
+ async function voiceChat({ action = "speak", text, durationSec = 5 } = {}) {
127
+ if (!IS_MAC) return { success: false, error: "Voice chat sadece macOS'ta" };
128
+
129
+ if (action === "record") {
130
+ try {
131
+ const audioFile = await recordMac(durationSec);
132
+ const buffer = fs.readFileSync(audioFile);
133
+ const text = await whisperTranscribe(buffer);
134
+ fs.unlinkSync(audioFile);
135
+ return { success: true, action: "record", text, message: `Algilanan: "${text}"` };
136
+ } catch (e) {
137
+ return { success: false, error: e.message };
138
+ }
139
+ }
140
+
141
+ if (action === "speak" || !action) {
142
+ if (!text) return { success: false, error: "text gerekli (speak icin)" };
143
+ return await macSay(text);
144
+ }
145
+
146
+ if (action === "test") {
147
+ // Setup check
148
+ try {
149
+ const outFile = path.join(os.tmpdir(), "voice-test.wav");
150
+ const proc = spawn("which", ["rec"]);
151
+ proc.on("close", (code) => {
152
+ if (code === 0) return { success: true, message: "Voice chat hazir" };
153
+ });
154
+ return { success: false, error: "'rec' komutu yok. brew install sox" };
155
+ } catch (e) {
156
+ return { success: false, error: e.message };
157
+ }
158
+ }
159
+
160
+ return { success: false, error: `Bilinmeyen action: ${action}` };
161
+ }
162
+
163
+ module.exports = {
164
+ name: "voice_chat",
165
+ description: "Sesli asistan: mikrofonla konus, Whisper ile metne cevir, yaniti sesli oku. macOS + sox gerekli.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ action: { type: "string", description: "record (konusarak yaz), speak (yaziyi sesli oku), test (hazir mi?)", enum: ["record", "speak", "test"] },
170
+ text: { type: "string", description: "Sesli okunacak metin (speak icin)" },
171
+ durationSec: { type: "number", description: "Kayit suresi (default 5)" },
172
+ },
173
+ required: [],
174
+ },
175
+ async execute(params) {
176
+ return await voiceChat(params);
177
+ },
178
+ };
@@ -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
+ };