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 +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/plan.js +161 -0
- package/src/tools/voice_chat.js +178 -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.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"
|
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,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
|
+
};
|