pi-finance-tr 0.1.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 +44 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/examples.env +25 -0
- package/extensions/pi-finance-tr.ts +36 -0
- package/package.json +64 -0
- package/src/cache.ts +31 -0
- package/src/config.ts +56 -0
- package/src/constants.ts +7 -0
- package/src/modules/antigravity/index.ts +7 -0
- package/src/modules/antigravity/tools.ts +48 -0
- package/src/modules/borsa/index.ts +7 -0
- package/src/modules/borsa/service.ts +7 -0
- package/src/modules/borsa/tools.ts +29 -0
- package/src/modules/emtia/index.ts +7 -0
- package/src/modules/emtia/service.ts +7 -0
- package/src/modules/emtia/tools.ts +29 -0
- package/src/modules/kap/index.ts +7 -0
- package/src/modules/kap/service.ts +7 -0
- package/src/modules/kap/tools.ts +33 -0
- package/src/modules/kripto/index.ts +9 -0
- package/src/modules/kripto/service.ts +148 -0
- package/src/modules/kripto/tools.ts +103 -0
- package/src/modules/kur/index.ts +9 -0
- package/src/modules/kur/service.ts +125 -0
- package/src/modules/kur/tools.ts +106 -0
- package/src/types.ts +4 -0
- package/src/utils/http.ts +64 -0
- package/src/utils/xml.ts +5 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Tüm önemli değişiklikler bu dosyada belgelenir.
|
|
4
|
+
Format [Keep a Changelog](https://keepachangelog.com/tr/1.1.0/) standardını takip eder.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — 2026-05-14
|
|
7
|
+
|
|
8
|
+
### Eklendi
|
|
9
|
+
- `finance_tr_kripto_fiyat` aracı: CoinGecko üzerinden kripto para fiyatları (TRY + USD, 24 saatlik değişim)
|
|
10
|
+
- 25 popüler coin için BTC/ETH/SOL/BNB/XRP/DOGE/ADA/AVAX ve diğerleri
|
|
11
|
+
- 60 saniyelik TTL önbellekleme
|
|
12
|
+
- `UnknownCoinError`, `RateLimitError` hata sınıfları
|
|
13
|
+
- GitHub Actions CI: `ci.yml` — push/PR tetikleyici, Node 18/20/22 matris
|
|
14
|
+
- GitHub Actions entegrasyon testleri: `integration.yml` — haftalık Pazartesi + `workflow_dispatch`
|
|
15
|
+
- TCMB XML fixture'ı güncellendi: USD, AUD, EUR, GBP, JPY, CHF (2025-11-03)
|
|
16
|
+
- Retry mekanizması birim testi (2 ağ başarısızlığı + 1 başarı)
|
|
17
|
+
- Cache hit birim testi (aynı sorgu, tek fetch)
|
|
18
|
+
|
|
19
|
+
### Değişti
|
|
20
|
+
- `finance_tr_doviz_kur` çıktısı: her yanıtın sonuna TCMB sorumluluk reddi eklendi
|
|
21
|
+
- Hata mesajları kullanıcı dostu Türkçe'ye dönüştürüldü:
|
|
22
|
+
- `TatilGunuError`: "TCMB bu tarihte bülten yayımlamamış (tatil/hafta sonu olabilir)…"
|
|
23
|
+
- `UnknownCurrencyError`: desteklenen kurlar listeleniyor
|
|
24
|
+
- Timeout/AbortError: bağlantı önerisi
|
|
25
|
+
- ESLint kuralı: `_` önekli kullanılmayan parametreler artık hata üretmiyor
|
|
26
|
+
- Paket mimarisi netleştirildi: **Yol A** (TypeScript kaynak olarak yayın) — pi runtime `.ts` dosyalarını doğrudan çalıştırır, build adımı yoktur
|
|
27
|
+
|
|
28
|
+
### Döküman
|
|
29
|
+
- README: CI/lisans/node badge'leri
|
|
30
|
+
- README: "Niçin pi-finance-tr?" konumlandırma paragrafı
|
|
31
|
+
- README: Kripto aracı kullanım örnekleri ve parametre tablosu
|
|
32
|
+
- README: Roadmap tablosu (v0.1.0 → v1.0.0)
|
|
33
|
+
- README: Katkı bölümü
|
|
34
|
+
- CONTRIBUTING.md: issue şablonu, geliştirme ortamı, PR akışı, mimari notu
|
|
35
|
+
|
|
36
|
+
## [0.0.1-alpha.0] — 2026-05-14
|
|
37
|
+
|
|
38
|
+
### Eklendi
|
|
39
|
+
- `finance_tr_doviz_kur` aracı: TCMB üzerinden güncel ve geçmiş döviz kurları (USD, EUR, GBP vb.)
|
|
40
|
+
- TTL'li in-memory önbellekleme (varsayılan 1 saat)
|
|
41
|
+
- Timeout + otomatik yeniden deneme ile HTTP istemcisi
|
|
42
|
+
- TCMB XML ayrıştırıcı (`xml2js`)
|
|
43
|
+
- `borsa`, `emtia`, `kripto`, `kap` modülleri için iskelet yapı (v0.1.0'da implemente edilecek)
|
|
44
|
+
- Birim testleri (config, cache, kur servisi) ve gerçek TCMB entegrasyon testi
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ulusoyomer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# pi-finance-tr
|
|
2
|
+
|
|
3
|
+
[](https://github.com/burcineren/pi-finance-tr/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](package.json)
|
|
6
|
+
|
|
7
|
+
Türk finans verilerini pi için MCP araçları olarak sunan paket.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
> ## ⚠️ Önemli Uyarı
|
|
12
|
+
>
|
|
13
|
+
> **Bu paket yatırım tavsiyesi değildir ve SPK lisansı yoktur. Sunulan veriler yalnızca bilgilendirme amaçlıdır. Finansal kararlarınızı lisanslı yatırım danışmanlarınıza danışarak alınız. Veri gecikmeli veya hatalı olabilir; her türlü sorumluluk kullanıcıya aittir.**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Niçin pi-finance-tr?
|
|
18
|
+
|
|
19
|
+
Türkçe pi kullanıcısı için sade, terminal-dostu günlük finans araçları sunar: TCMB döviz kurları, kripto fiyatları, kira artış hesabı, kredi taksiti gibi pratik ihtiyaçlar. `borsa-mcp` gibi profesyonel teknik analiz odaklı bağımsız MCP sunucularına alternatif değildir; pi ekosistemine native entegre olur ve tek komutla kurulur.
|
|
20
|
+
|
|
21
|
+
## Araçlar
|
|
22
|
+
|
|
23
|
+
### Döviz (kur modülü — tam)
|
|
24
|
+
| Araç | Açıklama |
|
|
25
|
+
|------|----------|
|
|
26
|
+
| `finance_tr_doviz_kur` | TCMB resmi döviz kuru (güncel veya geçmiş tarih) |
|
|
27
|
+
|
|
28
|
+
### Kripto (v0.1.0 — tam)
|
|
29
|
+
| Araç | Açıklama |
|
|
30
|
+
|------|----------|
|
|
31
|
+
| `finance_tr_kripto_fiyat` | CoinGecko üzerinden kripto fiyatları (TRY + USD, 24s değişim) |
|
|
32
|
+
|
|
33
|
+
### Borsa (v0.2.0'da)
|
|
34
|
+
| Araç | Açıklama |
|
|
35
|
+
|------|----------|
|
|
36
|
+
| `finance_tr_borsa_hisse` | BIST hisse senedi fiyatı |
|
|
37
|
+
|
|
38
|
+
### Emtia (v0.3.0'da)
|
|
39
|
+
| Araç | Açıklama |
|
|
40
|
+
|------|----------|
|
|
41
|
+
| `finance_tr_emtia_fiyat` | Altın, gümüş vb. emtia fiyatları |
|
|
42
|
+
|
|
43
|
+
### KAP (v0.3.0'da)
|
|
44
|
+
| Araç | Açıklama |
|
|
45
|
+
|------|----------|
|
|
46
|
+
| `finance_tr_kap_bildirim` | KAP bildirimleri ve özel durum açıklamaları |
|
|
47
|
+
|
|
48
|
+
## Gereksinimler
|
|
49
|
+
|
|
50
|
+
- [pi](https://github.com/badlogic/pi-mono)
|
|
51
|
+
- Node.js >= 18
|
|
52
|
+
|
|
53
|
+
## Kurulum
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pi install npm:pi-finance-tr
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Yerel kaynak koddan kurulum:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pi install /path/to/pi-finance-tr
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Yapılandırma
|
|
66
|
+
|
|
67
|
+
`examples.env` dosyasını başlangıç noktası olarak kullanabilirsiniz.
|
|
68
|
+
|
|
69
|
+
### Ortam Değişkenleri
|
|
70
|
+
|
|
71
|
+
| Değişken | Açıklama | Varsayılan |
|
|
72
|
+
|----------|----------|------------|
|
|
73
|
+
| `FINANCE_TR_ENABLED_MODULES` | Açık modüller (virgülle ayrılmış) | tümü |
|
|
74
|
+
| `FINANCE_TR_EVDS_KEY` | TCMB EVDS API anahtarı (borsa/emtia için) | — |
|
|
75
|
+
| `FINANCE_TR_CACHE_TTL_OVERRIDE` | Önbellek süresi (saniye) | `3600` |
|
|
76
|
+
| `FINANCE_TR_USER_AGENT` | HTTP User-Agent başlığı | `pi-finance-tr/0.1.0` |
|
|
77
|
+
| `FINANCE_TR_TIMEOUT_MS` | HTTP istek zaman aşımı (ms) | `15000` |
|
|
78
|
+
|
|
79
|
+
### Örnek
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Yalnızca kur ve kripto modüllerini aç
|
|
83
|
+
export FINANCE_TR_ENABLED_MODULES=kur,kripto
|
|
84
|
+
|
|
85
|
+
# Önbelleği 30 dakikaya düşür
|
|
86
|
+
export FINANCE_TR_CACHE_TTL_OVERRIDE=1800
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Modül eşleşmesi
|
|
90
|
+
|
|
91
|
+
| Modül | Araç(lar) |
|
|
92
|
+
|-------|-----------|
|
|
93
|
+
| `kur` | `finance_tr_doviz_kur` |
|
|
94
|
+
| `kripto` | `finance_tr_kripto_fiyat` |
|
|
95
|
+
| `borsa` | `finance_tr_borsa_hisse` |
|
|
96
|
+
| `emtia` | `finance_tr_emtia_fiyat` |
|
|
97
|
+
| `kap` | `finance_tr_kap_bildirim` |
|
|
98
|
+
|
|
99
|
+
## Kullanım Örnekleri
|
|
100
|
+
|
|
101
|
+
### Döviz kuru sorgulama
|
|
102
|
+
|
|
103
|
+
- "Dolar bugün kaç TL?"
|
|
104
|
+
- "Euro alış ve satış kurları nedir?"
|
|
105
|
+
- "Geçen ay 15'inde pound kaçtı?"
|
|
106
|
+
- "2025-11-03 tarihinde USD satış kuru ne idi?"
|
|
107
|
+
- "Japon yeni kuru bugün TCMB'ye göre kaç?"
|
|
108
|
+
|
|
109
|
+
### Kripto fiyat sorgulama
|
|
110
|
+
|
|
111
|
+
- "Bitcoin kaç TL?"
|
|
112
|
+
- "ETH son 24 saatte ne kadar değişti?"
|
|
113
|
+
- "SOL fiyatı dolar ve TL cinsinden nedir?"
|
|
114
|
+
|
|
115
|
+
## Araç Parametreleri
|
|
116
|
+
|
|
117
|
+
### `finance_tr_doviz_kur`
|
|
118
|
+
| Parametre | Tip | Zorunlu | Açıklama |
|
|
119
|
+
|-----------|-----|---------|----------|
|
|
120
|
+
| `currency` | `string` | evet | Döviz kodu (USD, EUR, GBP, JPY, CHF vb.) |
|
|
121
|
+
| `date` | `string` | hayır | Tarih (YYYY-MM-DD; belirtilmezse bugünkü kur) |
|
|
122
|
+
|
|
123
|
+
**Not:** TCMB tatil ve hafta sonlarında bülten yayımlamaz. Bu günlere ait kur sorgusu Türkçe hata mesajı döner.
|
|
124
|
+
|
|
125
|
+
### `finance_tr_kripto_fiyat`
|
|
126
|
+
| Parametre | Tip | Zorunlu | Açıklama |
|
|
127
|
+
|-----------|-----|---------|----------|
|
|
128
|
+
| `coin` | `string` | evet | Kripto para kodu (BTC, ETH, SOL vb.) veya CoinGecko ID |
|
|
129
|
+
| `currencies` | `string[]` | hayır | Karşılaştırma para birimleri (varsayılan: `["try", "usd"]`) |
|
|
130
|
+
|
|
131
|
+
## Roadmap
|
|
132
|
+
|
|
133
|
+
| Versiyon | İçerik |
|
|
134
|
+
|----------|--------|
|
|
135
|
+
| **v0.1.0** (mevcut) | Kripto modülü — CoinGecko, TRY+USD, 24s değişim |
|
|
136
|
+
| **v0.2.0** | Borsa modülü — BigPara + Investing fallback |
|
|
137
|
+
| **v0.3.0** | KAP scraping + altın/gümüş emtia |
|
|
138
|
+
| **v1.0.0** | Kişisel finans hesaplayıcıları (kira artışı, kredi taksit, BES) |
|
|
139
|
+
|
|
140
|
+
## Sorun Giderme
|
|
141
|
+
|
|
142
|
+
### Modül çalışmıyor
|
|
143
|
+
|
|
144
|
+
`FINANCE_TR_ENABLED_MODULES` değişkenini kontrol edin. Değer belirtilmezse tüm modüller açık olur.
|
|
145
|
+
|
|
146
|
+
### Zaman aşımı hatası
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
export FINANCE_TR_TIMEOUT_MS=30000
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### TCMB'den veri gelmiyor
|
|
153
|
+
|
|
154
|
+
- İş günü olduğundan ve TCMB'nin bülteni yayımladığından emin olun (genellikle 15:30'dan sonra).
|
|
155
|
+
- Tarih formatının `YYYY-MM-DD` olduğunu kontrol edin.
|
|
156
|
+
- Döviz kodunun TCMB'nin kabul ettiği kodlardan biri olduğunu doğrulayın (USD, EUR, GBP, JPY, CHF vb.).
|
|
157
|
+
|
|
158
|
+
## Katkı
|
|
159
|
+
|
|
160
|
+
Issue açmak için [GitHub Issues](https://github.com/burcineren/pi-finance-tr/issues) sayfasını kullanın. Lütfen şunları belirtin:
|
|
161
|
+
- Hangi modül/araçla ilgili
|
|
162
|
+
- Beklenen ve gerçekleşen davranış
|
|
163
|
+
- Node.js versiyonu
|
|
164
|
+
|
|
165
|
+
### PR akışı
|
|
166
|
+
|
|
167
|
+
1. Repo'yu fork edin
|
|
168
|
+
2. `git checkout -b feat/açıklayıcı-isim`
|
|
169
|
+
3. Değişikliklerinizi yapın
|
|
170
|
+
4. `npm run typecheck && npm run lint && npm test` geçtiğinden emin olun
|
|
171
|
+
5. PR açın — ADIM'lara karşılık gelen commit'ler tercih edilir
|
|
172
|
+
|
|
173
|
+
Detaylar için [CONTRIBUTING.md](CONTRIBUTING.md) dosyasına bakın.
|
|
174
|
+
|
|
175
|
+
## Lisans
|
|
176
|
+
|
|
177
|
+
MIT
|
package/examples.env
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# pi-finance-tr yapılandırma örneği
|
|
2
|
+
# Bu dosyayı kopyalayıp .env olarak kaydedin: cp examples.env .env
|
|
3
|
+
|
|
4
|
+
# Etkinleştirilecek modüller (virgülle ayırın)
|
|
5
|
+
# Boş bırakılırsa tüm modüller açık olur.
|
|
6
|
+
# Geçerli değerler: kur, borsa, emtia, kripto, kap
|
|
7
|
+
# FINANCE_TR_ENABLED_MODULES=kur,borsa,emtia,kripto,kap
|
|
8
|
+
|
|
9
|
+
# TCMB EVDS API anahtarı
|
|
10
|
+
# Borsa ve emtia modülleri için gerekecek (şu an stub, v0.1.0'da aktif olacak)
|
|
11
|
+
# Ücretsiz hesap: https://evds2.tcmb.gov.tr
|
|
12
|
+
# FINANCE_TR_EVDS_KEY=your_evds_key_here
|
|
13
|
+
|
|
14
|
+
# Önbellek TTL (saniye)
|
|
15
|
+
# Varsayılan: 3600 (1 saat)
|
|
16
|
+
# Kur verilerinin ne kadar süre önbellekte tutulacağını belirler.
|
|
17
|
+
# FINANCE_TR_CACHE_TTL_OVERRIDE=3600
|
|
18
|
+
|
|
19
|
+
# User-Agent başlığı
|
|
20
|
+
# Varsayılan: pi-finance-tr/0.0.1-alpha.0
|
|
21
|
+
# FINANCE_TR_USER_AGENT=pi-finance-tr/0.0.1-alpha.0
|
|
22
|
+
|
|
23
|
+
# HTTP istek zaman aşımı (milisaniye)
|
|
24
|
+
# Varsayılan: 15000 (15 saniye)
|
|
25
|
+
# FINANCE_TR_TIMEOUT_MS=15000
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { loadConfig } from '../src/config.ts';
|
|
3
|
+
import type { EnvSource } from '../src/types.ts';
|
|
4
|
+
import { registerKurModule } from '../src/modules/kur/index.ts';
|
|
5
|
+
import { registerBorsaModule } from '../src/modules/borsa/index.ts';
|
|
6
|
+
import { registerEmtiaModule } from '../src/modules/emtia/index.ts';
|
|
7
|
+
import { registerKriptoModule } from '../src/modules/kripto/index.ts';
|
|
8
|
+
import { registerKapModule } from '../src/modules/kap/index.ts';
|
|
9
|
+
import { registerAntigravityModule } from '../src/modules/antigravity/index.ts';
|
|
10
|
+
|
|
11
|
+
interface ExtensionOptions {
|
|
12
|
+
env?: EnvSource;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function financeTrExtension(pi: ExtensionAPI, options?: ExtensionOptions): void {
|
|
16
|
+
const config = loadConfig(options?.env);
|
|
17
|
+
|
|
18
|
+
if (config.enabledModules.has('kur')) {
|
|
19
|
+
registerKurModule(pi, config);
|
|
20
|
+
}
|
|
21
|
+
if (config.enabledModules.has('borsa')) {
|
|
22
|
+
registerBorsaModule(pi, config);
|
|
23
|
+
}
|
|
24
|
+
if (config.enabledModules.has('emtia')) {
|
|
25
|
+
registerEmtiaModule(pi, config);
|
|
26
|
+
}
|
|
27
|
+
if (config.enabledModules.has('kripto')) {
|
|
28
|
+
registerKriptoModule(pi, config);
|
|
29
|
+
}
|
|
30
|
+
if (config.enabledModules.has('kap')) {
|
|
31
|
+
registerKapModule(pi, config);
|
|
32
|
+
}
|
|
33
|
+
if (config.enabledModules.has('antigravity')) {
|
|
34
|
+
registerAntigravityModule(pi, config);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-finance-tr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Türk finans verilerini (TCMB, BIST, KAP, altın, kripto) pi için MCP araçları olarak sunan paket.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi",
|
|
8
|
+
"mcp",
|
|
9
|
+
"finance",
|
|
10
|
+
"turkey",
|
|
11
|
+
"tcmb",
|
|
12
|
+
"bist",
|
|
13
|
+
"kap",
|
|
14
|
+
"doviz",
|
|
15
|
+
"altin"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "burcineren",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/burcineren/pi-finance-tr.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/burcineren/pi-finance-tr#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/burcineren/pi-finance-tr/issues"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions",
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"examples.env"
|
|
34
|
+
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./extensions"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:integration": "RUN_INTEGRATION=1 vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "eslint src extensions test"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
49
|
+
"@sinclair/typebox": "*"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"xml2js": "^0.6.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
56
|
+
"@sinclair/typebox": "^0.34.41",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"@types/xml2js": "^0.4.14",
|
|
59
|
+
"eslint": "^9.0.0",
|
|
60
|
+
"typescript": "^5.9.2",
|
|
61
|
+
"typescript-eslint": "^8.0.0",
|
|
62
|
+
"vitest": "^3.2.4"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class TtlCache<V> {
|
|
2
|
+
private readonly store = new Map<string, { value: V; expiresAt: number }>();
|
|
3
|
+
private readonly defaultTtlMs: number;
|
|
4
|
+
|
|
5
|
+
constructor(defaultTtlSeconds: number) {
|
|
6
|
+
this.defaultTtlMs = defaultTtlSeconds * 1_000;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get(key: string): V | undefined {
|
|
10
|
+
const entry = this.store.get(key);
|
|
11
|
+
if (!entry) return undefined;
|
|
12
|
+
if (Date.now() > entry.expiresAt) {
|
|
13
|
+
this.store.delete(key);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return entry.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set(key: string, value: V, ttlSeconds?: number): void {
|
|
20
|
+
const ttlMs = ttlSeconds !== undefined ? ttlSeconds * 1_000 : this.defaultTtlMs;
|
|
21
|
+
this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
has(key: string): boolean {
|
|
25
|
+
return this.get(key) !== undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear(): void {
|
|
29
|
+
this.store.clear();
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DEFAULT_TIMEOUT_MS, DEFAULT_USER_AGENT, ENABLED_MODULES } from './constants.ts';
|
|
2
|
+
import type { EnabledModule, EnvSource } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export interface FinanceTrConfig {
|
|
5
|
+
enabledModules: Set<EnabledModule>;
|
|
6
|
+
evdsKey?: string;
|
|
7
|
+
cacheTtlOverrideSeconds?: number;
|
|
8
|
+
userAgent: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseEnabledModules(value?: string): Set<EnabledModule> {
|
|
13
|
+
if (!value?.trim()) return new Set(ENABLED_MODULES);
|
|
14
|
+
|
|
15
|
+
const valid = new Set<string>(ENABLED_MODULES);
|
|
16
|
+
const parts = value
|
|
17
|
+
.split(',')
|
|
18
|
+
.map((p) => p.trim().toLowerCase())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
|
|
21
|
+
for (const m of parts) {
|
|
22
|
+
if (!valid.has(m)) {
|
|
23
|
+
throw new Error(`Bilinmeyen modül: ${m}. Geçerli değerler: ${ENABLED_MODULES.join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new Set(parts as EnabledModule[]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseCacheTtl(value?: string): number | undefined {
|
|
31
|
+
if (!value?.trim()) return undefined;
|
|
32
|
+
const parsed = parseInt(value, 10);
|
|
33
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
34
|
+
throw new Error('FINANCE_TR_CACHE_TTL_OVERRIDE pozitif tam sayı olmalıdır');
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseTimeoutMs(value?: string): number {
|
|
40
|
+
if (!value?.trim()) return DEFAULT_TIMEOUT_MS;
|
|
41
|
+
const parsed = parseInt(value, 10);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
43
|
+
throw new Error('FINANCE_TR_TIMEOUT_MS pozitif tam sayı olmalıdır');
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadConfig(env: EnvSource = process.env): FinanceTrConfig {
|
|
49
|
+
return {
|
|
50
|
+
enabledModules: parseEnabledModules(env.FINANCE_TR_ENABLED_MODULES),
|
|
51
|
+
evdsKey: env.FINANCE_TR_EVDS_KEY?.trim() || undefined,
|
|
52
|
+
cacheTtlOverrideSeconds: parseCacheTtl(env.FINANCE_TR_CACHE_TTL_OVERRIDE),
|
|
53
|
+
userAgent: env.FINANCE_TR_USER_AGENT?.trim() || DEFAULT_USER_AGENT,
|
|
54
|
+
timeoutMs: parseTimeoutMs(env.FINANCE_TR_TIMEOUT_MS),
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const ENABLED_MODULES = ['kur', 'borsa', 'emtia', 'kripto', 'kap', 'antigravity'] as const;
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
4
|
+
export const DEFAULT_USER_AGENT = 'pi-finance-tr/0.1.0';
|
|
5
|
+
|
|
6
|
+
export const KUR_CACHE_TTL_SECONDS = 3_600;
|
|
7
|
+
export const TCMB_BASE_URL = 'https://www.tcmb.gov.tr/kurlar';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createAntigravityTool } from './tools.ts';
|
|
4
|
+
|
|
5
|
+
export function registerAntigravityModule(pi: ExtensionAPI, _config: FinanceTrConfig): void {
|
|
6
|
+
pi.registerTool(createAntigravityTool());
|
|
7
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
|
|
4
|
+
const QUOTES = [
|
|
5
|
+
'Borsa yerçekimine meydan okur — ne yazık ki yalnızca düşerken.',
|
|
6
|
+
'Altın ağırdır, portföyünüz ise hafifler.',
|
|
7
|
+
'"Piyasalar mantıksız kalabilir, siz iflas olana kadar." — Keynes',
|
|
8
|
+
'Kripto: yerçekimi yoktur, taban da.',
|
|
9
|
+
'TCMB döviz kurları antigravity değil, ama bazen öyle hissettiriyor.',
|
|
10
|
+
'https://xkcd.com/353/',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function createAntigravityTool() {
|
|
14
|
+
return {
|
|
15
|
+
name: 'finance_tr_antigravity',
|
|
16
|
+
label: 'Antigravity',
|
|
17
|
+
description: 'Finansal yerçekimine meydan okur. import antigravity',
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
wish: Type.Optional(Type.String({ description: 'Bir dilek tut' })),
|
|
20
|
+
}),
|
|
21
|
+
async execute(
|
|
22
|
+
_toolCallId: string,
|
|
23
|
+
params: { wish?: string },
|
|
24
|
+
_signal: AbortSignal | undefined,
|
|
25
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
26
|
+
) {
|
|
27
|
+
if (onUpdate) {
|
|
28
|
+
onUpdate({
|
|
29
|
+
content: [{ type: 'text' as const, text: '🚀 Yerçekimi hesaplanıyor...' }],
|
|
30
|
+
details: undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
|
|
35
|
+
const wish = params.wish ? `\n\nDileğin: "${params.wish}" — piyasa duydu, not aldı.` : '';
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text: `🪂 Antigravity aktif.\n\n${quote}${wish}`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
details: { quote, xkcd: 'https://xkcd.com/353/' },
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createBorsaTool } from './tools.ts';
|
|
4
|
+
|
|
5
|
+
export function registerBorsaModule(pi: ExtensionAPI, _config: FinanceTrConfig): void {
|
|
6
|
+
pi.registerTool(createBorsaTool());
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
|
|
4
|
+
export function createBorsaTool() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'finance_tr_borsa_hisse',
|
|
7
|
+
label: 'BIST Hisse Fiyatı',
|
|
8
|
+
description: 'BIST hisse senedi fiyatı sorgular. (Yakında — v0.1.0)',
|
|
9
|
+
parameters: Type.Object({
|
|
10
|
+
symbol: Type.String({ description: 'Hisse kodu (ör. THYAO, GARAN, AKBNK)' }),
|
|
11
|
+
}),
|
|
12
|
+
async execute(
|
|
13
|
+
_toolCallId: string,
|
|
14
|
+
_params: { symbol: string },
|
|
15
|
+
_signal: AbortSignal | undefined,
|
|
16
|
+
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
17
|
+
) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: 'text' as const,
|
|
22
|
+
text: 'Borsa modülü henüz tamamlanmadı. v0.1.0 sürümünde eklenecek.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
details: {},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createEmtiaTool } from './tools.ts';
|
|
4
|
+
|
|
5
|
+
export function registerEmtiaModule(pi: ExtensionAPI, _config: FinanceTrConfig): void {
|
|
6
|
+
pi.registerTool(createEmtiaTool());
|
|
7
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
|
|
4
|
+
export function createEmtiaTool() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'finance_tr_emtia_fiyat',
|
|
7
|
+
label: 'Emtia Fiyatı',
|
|
8
|
+
description: 'Altın, gümüş ve diğer emtia fiyatlarını sorgular. (Yakında — v0.1.0)',
|
|
9
|
+
parameters: Type.Object({
|
|
10
|
+
symbol: Type.String({ description: 'Emtia kodu (ör. ALTIN, GUMUS, PLATINUM)' }),
|
|
11
|
+
}),
|
|
12
|
+
async execute(
|
|
13
|
+
_toolCallId: string,
|
|
14
|
+
_params: { symbol: string },
|
|
15
|
+
_signal: AbortSignal | undefined,
|
|
16
|
+
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
17
|
+
) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: 'text' as const,
|
|
22
|
+
text: 'Emtia modülü henüz tamamlanmadı. v0.1.0 sürümünde eklenecek.',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
details: {},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createKapTool } from './tools.ts';
|
|
4
|
+
|
|
5
|
+
export function registerKapModule(pi: ExtensionAPI, _config: FinanceTrConfig): void {
|
|
6
|
+
pi.registerTool(createKapTool());
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
|
|
4
|
+
export function createKapTool() {
|
|
5
|
+
return {
|
|
6
|
+
name: 'finance_tr_kap_bildirim',
|
|
7
|
+
label: 'KAP Bildirimi',
|
|
8
|
+
description:
|
|
9
|
+
'Kamuyu Aydınlatma Platformu (KAP) bildirimleri ve özel durum açıklamaları. (Yakında — v0.1.0)',
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
query: Type.String({ description: 'Şirket adı veya hisse kodu (ör. THYAO, Türk Hava)' }),
|
|
12
|
+
limit: Type.Optional(
|
|
13
|
+
Type.Number({ description: 'Maksimum sonuç sayısı (varsayılan 10)', minimum: 1, maximum: 50 }),
|
|
14
|
+
),
|
|
15
|
+
}),
|
|
16
|
+
async execute(
|
|
17
|
+
_toolCallId: string,
|
|
18
|
+
_params: { query: string; limit?: number },
|
|
19
|
+
_signal: AbortSignal | undefined,
|
|
20
|
+
_onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
21
|
+
) {
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text' as const,
|
|
26
|
+
text: 'KAP modülü henüz tamamlanmadı. v0.1.0 sürümünde eklenecek.',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
details: {},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createKriptoService } from './service.ts';
|
|
4
|
+
import { createKriptoFiyatTool } from './tools.ts';
|
|
5
|
+
|
|
6
|
+
export function registerKriptoModule(pi: ExtensionAPI, config: FinanceTrConfig): void {
|
|
7
|
+
const service = createKriptoService(config);
|
|
8
|
+
pi.registerTool(createKriptoFiyatTool(service));
|
|
9
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { TtlCache } from '../../cache.ts';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createHttpClient, HttpError } from '../../utils/http.ts';
|
|
4
|
+
|
|
5
|
+
export interface KriptoFiyatResult {
|
|
6
|
+
coin: string;
|
|
7
|
+
symbol: string;
|
|
8
|
+
fiyatTry: number;
|
|
9
|
+
fiyatUsd: number;
|
|
10
|
+
degisim24sYuzde: number;
|
|
11
|
+
sonGuncelleme: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const KRIPTO_CACHE_TTL_SECONDS = 60;
|
|
15
|
+
|
|
16
|
+
const COINGECKO_BASE = 'https://api.coingecko.com/api/v3';
|
|
17
|
+
|
|
18
|
+
export const COIN_ID_MAP: Record<string, string> = {
|
|
19
|
+
BTC: 'bitcoin',
|
|
20
|
+
ETH: 'ethereum',
|
|
21
|
+
SOL: 'solana',
|
|
22
|
+
BNB: 'binancecoin',
|
|
23
|
+
XRP: 'ripple',
|
|
24
|
+
DOGE: 'dogecoin',
|
|
25
|
+
ADA: 'cardano',
|
|
26
|
+
AVAX: 'avalanche-2',
|
|
27
|
+
DOT: 'polkadot',
|
|
28
|
+
MATIC: 'matic-network',
|
|
29
|
+
LINK: 'chainlink',
|
|
30
|
+
LTC: 'litecoin',
|
|
31
|
+
UNI: 'uniswap',
|
|
32
|
+
ATOM: 'cosmos',
|
|
33
|
+
ETC: 'ethereum-classic',
|
|
34
|
+
XLM: 'stellar',
|
|
35
|
+
ALGO: 'algorand',
|
|
36
|
+
NEAR: 'near',
|
|
37
|
+
SHIB: 'shiba-inu',
|
|
38
|
+
TRX: 'tron',
|
|
39
|
+
BCH: 'bitcoin-cash',
|
|
40
|
+
FIL: 'filecoin',
|
|
41
|
+
HBAR: 'hedera-hashgraph',
|
|
42
|
+
ICP: 'internet-computer',
|
|
43
|
+
VET: 'vechain',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export class UnknownCoinError extends Error {
|
|
47
|
+
constructor(coin: string) {
|
|
48
|
+
const examples = Object.keys(COIN_ID_MAP).slice(0, 8).join(', ');
|
|
49
|
+
super(
|
|
50
|
+
`Bilinmeyen kripto kodu: "${coin}". Desteklenenlerden bazıları: ${examples}. ` +
|
|
51
|
+
'CoinGecko ID de kullanabilirsiniz (ör. "avalanche-2").',
|
|
52
|
+
);
|
|
53
|
+
this.name = 'UnknownCoinError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class RateLimitError extends Error {
|
|
58
|
+
constructor(readonly retryAfterSeconds: number) {
|
|
59
|
+
super(
|
|
60
|
+
`CoinGecko rate limit aşıldı. ${retryAfterSeconds} saniye sonra tekrar deneyin.`,
|
|
61
|
+
);
|
|
62
|
+
this.name = 'RateLimitError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveCoinId(input: string): string {
|
|
67
|
+
const upper = input.toUpperCase().trim();
|
|
68
|
+
return COIN_ID_MAP[upper] ?? input.toLowerCase().trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatNum(n: number): number {
|
|
72
|
+
return Number.isFinite(n) ? n : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface CoinGeckoEntry {
|
|
76
|
+
try?: number;
|
|
77
|
+
usd?: number;
|
|
78
|
+
usd_24h_change?: number;
|
|
79
|
+
try_24h_change?: number;
|
|
80
|
+
last_updated_at?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createKriptoService(config: FinanceTrConfig) {
|
|
84
|
+
const cache = new TtlCache<KriptoFiyatResult>(KRIPTO_CACHE_TTL_SECONDS);
|
|
85
|
+
const http = createHttpClient(config);
|
|
86
|
+
|
|
87
|
+
async function fetchKriptoFiyat(
|
|
88
|
+
coin: string,
|
|
89
|
+
currencies: string[] = ['try', 'usd'],
|
|
90
|
+
): Promise<KriptoFiyatResult> {
|
|
91
|
+
const coinId = resolveCoinId(coin);
|
|
92
|
+
const symbol = coin.toUpperCase().trim();
|
|
93
|
+
const sortedCurrencies = [...currencies].sort();
|
|
94
|
+
const cacheKey = `${coinId}:${sortedCurrencies.join(',')}`;
|
|
95
|
+
|
|
96
|
+
const cached = cache.get(cacheKey);
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
|
|
99
|
+
const vs = sortedCurrencies.join(',');
|
|
100
|
+
const url =
|
|
101
|
+
`${COINGECKO_BASE}/simple/price` +
|
|
102
|
+
`?ids=${coinId}&vs_currencies=${vs}` +
|
|
103
|
+
`&include_24hr_change=true&include_last_updated_at=true`;
|
|
104
|
+
|
|
105
|
+
let response: Response;
|
|
106
|
+
try {
|
|
107
|
+
response = await http.fetchWithRetry(url);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof HttpError && err.status === 429) {
|
|
110
|
+
throw new RateLimitError(60);
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (response.status === 429) {
|
|
116
|
+
const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60', 10);
|
|
117
|
+
throw new RateLimitError(Number.isFinite(retryAfter) ? retryAfter : 60);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = (await response.json()) as Record<string, CoinGeckoEntry>;
|
|
121
|
+
|
|
122
|
+
if (!data[coinId] || Object.keys(data[coinId]).length === 0) {
|
|
123
|
+
throw new UnknownCoinError(coin);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const entry = data[coinId];
|
|
127
|
+
const change =
|
|
128
|
+
entry.usd_24h_change !== undefined
|
|
129
|
+
? entry.usd_24h_change
|
|
130
|
+
: (entry.try_24h_change ?? 0);
|
|
131
|
+
|
|
132
|
+
const result: KriptoFiyatResult = {
|
|
133
|
+
coin: coinId,
|
|
134
|
+
symbol,
|
|
135
|
+
fiyatTry: formatNum(entry.try ?? 0),
|
|
136
|
+
fiyatUsd: formatNum(entry.usd ?? 0),
|
|
137
|
+
degisim24sYuzde: formatNum(change),
|
|
138
|
+
sonGuncelleme: new Date(
|
|
139
|
+
(entry.last_updated_at ?? Math.floor(Date.now() / 1000)) * 1000,
|
|
140
|
+
).toISOString(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
cache.set(cacheKey, result);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { fetchKriptoFiyat };
|
|
148
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import type { createKriptoService } from './service.ts';
|
|
4
|
+
import { UnknownCoinError, RateLimitError, COIN_ID_MAP } from './service.ts';
|
|
5
|
+
|
|
6
|
+
const DISCLAIMER =
|
|
7
|
+
'ℹ️ CoinGecko verileri — bilgilendirme amaçlıdır, yatırım tavsiyesi değildir.';
|
|
8
|
+
|
|
9
|
+
function formatKripto(n: number): string {
|
|
10
|
+
const fractionDigits = n === 0 ? 2 : n < 0.0001 ? 8 : n < 0.01 ? 6 : n < 1 ? 4 : 2;
|
|
11
|
+
return n.toLocaleString('tr-TR', {
|
|
12
|
+
minimumFractionDigits: fractionDigits,
|
|
13
|
+
maximumFractionDigits: fractionDigits,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDegisim(pct: number): string {
|
|
18
|
+
const sign = pct >= 0 ? '+' : '';
|
|
19
|
+
const formatted = Math.abs(pct).toLocaleString('tr-TR', {
|
|
20
|
+
minimumFractionDigits: 2,
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
});
|
|
23
|
+
return `${sign}%${pct >= 0 ? '' : '-'}${formatted}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatError(err: unknown): string {
|
|
27
|
+
if (err instanceof UnknownCoinError) {
|
|
28
|
+
return err.message;
|
|
29
|
+
}
|
|
30
|
+
if (err instanceof RateLimitError) {
|
|
31
|
+
return err.message;
|
|
32
|
+
}
|
|
33
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.message.toLowerCase().includes('abort'))) {
|
|
34
|
+
return "CoinGecko'ya ulaşılamıyor (timeout). İnternet bağlantınızı kontrol edin.";
|
|
35
|
+
}
|
|
36
|
+
if (err instanceof Error) {
|
|
37
|
+
return `Hata: ${err.message}`;
|
|
38
|
+
}
|
|
39
|
+
return 'Beklenmeyen bir hata oluştu.';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createKriptoFiyatTool(service: ReturnType<typeof createKriptoService>) {
|
|
43
|
+
return {
|
|
44
|
+
name: 'finance_tr_kripto_fiyat',
|
|
45
|
+
label: 'Kripto Para Fiyatı',
|
|
46
|
+
description:
|
|
47
|
+
'CoinGecko üzerinden kripto para fiyatları. TRY ve USD karşılığı, 24 saatlik değişim.',
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
coin: Type.String({
|
|
50
|
+
description: `Kripto para kodu (ör. BTC, ETH, SOL) veya CoinGecko ID. Desteklenenler: ${Object.keys(COIN_ID_MAP).join(', ')}`,
|
|
51
|
+
}),
|
|
52
|
+
currencies: Type.Optional(
|
|
53
|
+
Type.Array(Type.String(), {
|
|
54
|
+
description: 'Karşılaştırma para birimleri (varsayılan: ["try", "usd"])',
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
async execute(
|
|
59
|
+
_toolCallId: string,
|
|
60
|
+
params: { coin: string; currencies?: string[] },
|
|
61
|
+
_signal: AbortSignal | undefined,
|
|
62
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
63
|
+
) {
|
|
64
|
+
const symbol = params.coin.toUpperCase();
|
|
65
|
+
|
|
66
|
+
if (onUpdate) {
|
|
67
|
+
onUpdate({
|
|
68
|
+
content: [{ type: 'text' as const, text: `CoinGecko'dan ${symbol} fiyatı alınıyor...` }],
|
|
69
|
+
details: undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = await service.fetchKriptoFiyat(params.coin, params.currencies);
|
|
75
|
+
|
|
76
|
+
const tryStr = result.fiyatTry > 0 ? `${formatKripto(result.fiyatTry)} TRY` : null;
|
|
77
|
+
const usdStr = result.fiyatUsd > 0 ? `${formatKripto(result.fiyatUsd)} USD` : null;
|
|
78
|
+
const parts = [tryStr, usdStr].filter(Boolean).join(' | ');
|
|
79
|
+
const degisim = formatDegisim(result.degisim24sYuzde);
|
|
80
|
+
|
|
81
|
+
const headline = `${result.symbol} (${result.coin}): ${parts} | 24s: ${degisim}`;
|
|
82
|
+
|
|
83
|
+
if (onUpdate) {
|
|
84
|
+
onUpdate({
|
|
85
|
+
content: [{ type: 'text' as const, text: headline }],
|
|
86
|
+
details: undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text' as const, text: `${headline}\n\n${DISCLAIMER}` }],
|
|
92
|
+
details: result,
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = formatError(err);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text' as const, text: msg }],
|
|
98
|
+
details: { error: true },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
3
|
+
import { createKurService } from './service.ts';
|
|
4
|
+
import { createDovizKurTool } from './tools.ts';
|
|
5
|
+
|
|
6
|
+
export function registerKurModule(pi: ExtensionAPI, config: FinanceTrConfig): void {
|
|
7
|
+
const service = createKurService(config);
|
|
8
|
+
pi.registerTool(createDovizKurTool(service));
|
|
9
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { TtlCache } from '../../cache.ts';
|
|
2
|
+
import { KUR_CACHE_TTL_SECONDS, TCMB_BASE_URL } from '../../constants.ts';
|
|
3
|
+
import type { FinanceTrConfig } from '../../config.ts';
|
|
4
|
+
import { createHttpClient, HttpError } from '../../utils/http.ts';
|
|
5
|
+
import { parseXml } from '../../utils/xml.ts';
|
|
6
|
+
|
|
7
|
+
export interface DovizKurResult {
|
|
8
|
+
currency: string;
|
|
9
|
+
date: string;
|
|
10
|
+
alis: number;
|
|
11
|
+
satis: number;
|
|
12
|
+
banknotAlis: number;
|
|
13
|
+
banknotSatis: number;
|
|
14
|
+
capraz?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class UnknownCurrencyError extends Error {
|
|
18
|
+
constructor(currency: string) {
|
|
19
|
+
super(`Bilinmeyen döviz kodu: ${currency}. Örnek geçerli kodlar: USD, EUR, GBP, JPY`);
|
|
20
|
+
this.name = 'UnknownCurrencyError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class TatilGunuError extends Error {
|
|
25
|
+
constructor(label: string) {
|
|
26
|
+
super(`${label} için TCMB kur verisi yok (tatil veya hafta sonu olabilir)`);
|
|
27
|
+
this.name = 'TatilGunuError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TcmbCurrency {
|
|
32
|
+
$: { CrossOrder: string; Kod: string; CurrencyCode: string };
|
|
33
|
+
Unit: string;
|
|
34
|
+
Isim: string;
|
|
35
|
+
CurrencyName: string;
|
|
36
|
+
ForexBuying: string;
|
|
37
|
+
ForexSelling: string;
|
|
38
|
+
BanknoteBuying: string;
|
|
39
|
+
BanknoteSelling: string;
|
|
40
|
+
CrossRateUSD: string;
|
|
41
|
+
CrossRateOther: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TcmbXmlRoot {
|
|
45
|
+
Tarih_Date: {
|
|
46
|
+
$: { Tarih: string; Date: string; Bulten_No: string };
|
|
47
|
+
Currency: TcmbCurrency | TcmbCurrency[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildUrl(date?: string): string {
|
|
52
|
+
if (!date) return `${TCMB_BASE_URL}/today.xml`;
|
|
53
|
+
// YYYY-MM-DD → YYYYMM/DDMMYYYY
|
|
54
|
+
const [year, month, day] = date.split('-');
|
|
55
|
+
return `${TCMB_BASE_URL}/${year}${month}/${day}${month}${year}.xml`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function tcmbDateToIso(tcmbDate: string): string {
|
|
59
|
+
// "14.05.2026" → "2026-05-14"
|
|
60
|
+
const [day, month, year] = tcmbDate.split('.');
|
|
61
|
+
return `${year}-${month}-${day}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseNum(value: string | undefined): number {
|
|
65
|
+
if (!value) return 0;
|
|
66
|
+
const n = parseFloat(value.trim());
|
|
67
|
+
return Number.isNaN(n) ? 0 : n;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pickCrossRate(c: TcmbCurrency): number | undefined {
|
|
71
|
+
const usd = c.CrossRateUSD?.trim();
|
|
72
|
+
if (usd) return parseNum(usd);
|
|
73
|
+
const other = c.CrossRateOther?.trim();
|
|
74
|
+
if (other) return parseNum(other);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createKurService(config: FinanceTrConfig) {
|
|
79
|
+
const ttlSeconds = config.cacheTtlOverrideSeconds ?? KUR_CACHE_TTL_SECONDS;
|
|
80
|
+
const cache = new TtlCache<DovizKurResult>(ttlSeconds);
|
|
81
|
+
const http = createHttpClient(config);
|
|
82
|
+
|
|
83
|
+
async function fetchDovizKur(currency: string, date?: string): Promise<DovizKurResult> {
|
|
84
|
+
const code = currency.toUpperCase().trim();
|
|
85
|
+
const cacheKey = `${code}:${date ?? 'today'}`;
|
|
86
|
+
|
|
87
|
+
const cached = cache.get(cacheKey);
|
|
88
|
+
if (cached) return cached;
|
|
89
|
+
|
|
90
|
+
const url = buildUrl(date);
|
|
91
|
+
let xmlText: string;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const response = await http.fetchWithRetry(url);
|
|
95
|
+
xmlText = await response.text();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
98
|
+
throw new TatilGunuError(date ?? 'bugün');
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parsed = await parseXml<TcmbXmlRoot>(xmlText);
|
|
104
|
+
const raw = parsed.Tarih_Date.Currency;
|
|
105
|
+
const currencies = Array.isArray(raw) ? raw : [raw];
|
|
106
|
+
|
|
107
|
+
const found = currencies.find((c) => c.$.CurrencyCode === code);
|
|
108
|
+
if (!found) throw new UnknownCurrencyError(code);
|
|
109
|
+
|
|
110
|
+
const result: DovizKurResult = {
|
|
111
|
+
currency: code,
|
|
112
|
+
date: tcmbDateToIso(parsed.Tarih_Date.$.Tarih),
|
|
113
|
+
alis: parseNum(found.ForexBuying),
|
|
114
|
+
satis: parseNum(found.ForexSelling),
|
|
115
|
+
banknotAlis: parseNum(found.BanknoteBuying),
|
|
116
|
+
banknotSatis: parseNum(found.BanknoteSelling),
|
|
117
|
+
capraz: pickCrossRate(found),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
cache.set(cacheKey, result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { fetchDovizKur };
|
|
125
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AgentToolUpdateCallback } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import type { createKurService } from './service.ts';
|
|
4
|
+
import { UnknownCurrencyError, TatilGunuError } from './service.ts';
|
|
5
|
+
import { HttpError } from '../../utils/http.ts';
|
|
6
|
+
|
|
7
|
+
const DISCLAIMER =
|
|
8
|
+
'ℹ️ TCMB resmi bülten — bilgilendirme amaçlıdır, yatırım tavsiyesi değildir.';
|
|
9
|
+
|
|
10
|
+
const DESTEKLENEN_KURLAR =
|
|
11
|
+
'USD, EUR, GBP, JPY, CHF, AUD, CAD, DKK, NOK, SEK, SAR, KWD ve diğerleri';
|
|
12
|
+
|
|
13
|
+
function formatTr(n: number): string {
|
|
14
|
+
return n.toLocaleString('tr-TR', { minimumFractionDigits: 4, maximumFractionDigits: 4 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatError(err: unknown, date?: string): string {
|
|
18
|
+
if (err instanceof UnknownCurrencyError) {
|
|
19
|
+
return `Bilinmeyen döviz kodu. Desteklenenler: ${DESTEKLENEN_KURLAR}.`;
|
|
20
|
+
}
|
|
21
|
+
if (err instanceof TatilGunuError) {
|
|
22
|
+
const tarih = date ? `"${date}" tarihinde` : 'bu tarihte';
|
|
23
|
+
return `TCMB ${tarih} bülten yayımlamamış (tatil/hafta sonu olabilir). Önceki iş gününü deneyin.`;
|
|
24
|
+
}
|
|
25
|
+
if (err instanceof HttpError && err.status === 408) {
|
|
26
|
+
return "TCMB'ye ulaşılamıyor (timeout). İnternet bağlantınızı kontrol edin.";
|
|
27
|
+
}
|
|
28
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.message.toLowerCase().includes('abort'))) {
|
|
29
|
+
return "TCMB'ye ulaşılamıyor (timeout). İnternet bağlantınızı kontrol edin.";
|
|
30
|
+
}
|
|
31
|
+
if (err instanceof Error) {
|
|
32
|
+
return `Hata: ${err.message}`;
|
|
33
|
+
}
|
|
34
|
+
return 'Beklenmeyen bir hata oluştu.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createDovizKurTool(service: ReturnType<typeof createKurService>) {
|
|
38
|
+
return {
|
|
39
|
+
name: 'finance_tr_doviz_kur',
|
|
40
|
+
label: 'TCMB Döviz Kuru',
|
|
41
|
+
description:
|
|
42
|
+
"TCMB'nin yayımladığı güncel veya geçmiş döviz kuru. USD, EUR, GBP vb. desteklenir.",
|
|
43
|
+
parameters: Type.Object({
|
|
44
|
+
currency: Type.String({ description: 'Döviz kodu (ör. USD, EUR, GBP, JPY)' }),
|
|
45
|
+
date: Type.Optional(
|
|
46
|
+
Type.String({ description: 'Tarih (YYYY-MM-DD formatında; belirtilmezse bugünkü kur)' }),
|
|
47
|
+
),
|
|
48
|
+
}),
|
|
49
|
+
async execute(
|
|
50
|
+
_toolCallId: string,
|
|
51
|
+
params: { currency: string; date?: string },
|
|
52
|
+
_signal: AbortSignal | undefined,
|
|
53
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
54
|
+
) {
|
|
55
|
+
const code = params.currency.toUpperCase();
|
|
56
|
+
|
|
57
|
+
if (onUpdate) {
|
|
58
|
+
onUpdate({
|
|
59
|
+
content: [{ type: 'text' as const, text: `TCMB'den ${code} kuru alınıyor...` }],
|
|
60
|
+
details: undefined,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await service.fetchDovizKur(params.currency, params.date);
|
|
66
|
+
|
|
67
|
+
const lines = [
|
|
68
|
+
`${result.currency}/TRY — ${result.date}`,
|
|
69
|
+
`Döviz Alış: ${formatTr(result.alis)} TL`,
|
|
70
|
+
`Döviz Satış: ${formatTr(result.satis)} TL`,
|
|
71
|
+
`Banknot Alış: ${formatTr(result.banknotAlis)} TL`,
|
|
72
|
+
`Banknot Satış: ${formatTr(result.banknotSatis)} TL`,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
if (result.capraz !== undefined) {
|
|
76
|
+
lines.push(`Çapraz Kur: ${formatTr(result.capraz)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push('', DISCLAIMER);
|
|
80
|
+
|
|
81
|
+
if (onUpdate) {
|
|
82
|
+
onUpdate({
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: 'text' as const,
|
|
86
|
+
text: `${result.currency}/TRY ${result.date}: ${formatTr(result.satis)} TL (satış)`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
details: undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text' as const, text: lines.join('\n') }],
|
|
95
|
+
details: result,
|
|
96
|
+
};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = formatError(err, params.date);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text' as const, text: msg }],
|
|
101
|
+
details: { error: true },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly status: number,
|
|
4
|
+
message: string,
|
|
5
|
+
) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'HttpError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RETRY_DELAYS_MS = [200, 400, 800];
|
|
12
|
+
|
|
13
|
+
export function createHttpClient(config: { userAgent: string; timeoutMs: number }) {
|
|
14
|
+
const { userAgent, timeoutMs } = config;
|
|
15
|
+
|
|
16
|
+
async function fetchWithRetry(
|
|
17
|
+
url: string,
|
|
18
|
+
opts?: RequestInit,
|
|
19
|
+
retries = 3,
|
|
20
|
+
): Promise<Response> {
|
|
21
|
+
let lastError: unknown;
|
|
22
|
+
|
|
23
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(url, {
|
|
29
|
+
...opts,
|
|
30
|
+
headers: { 'User-Agent': userAgent, ...opts?.headers },
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (response.status >= 400 && response.status < 500) {
|
|
35
|
+
throw new HttpError(response.status, `HTTP ${response.status}: ${url}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (response.status >= 500) {
|
|
39
|
+
throw new HttpError(response.status, `HTTP ${response.status}: ${url}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return response;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
lastError = err;
|
|
48
|
+
if (attempt < retries - 1) {
|
|
49
|
+
await sleep(RETRY_DELAYS_MS[attempt] ?? 800);
|
|
50
|
+
}
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw lastError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { fetchWithRetry };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sleep(ms: number): Promise<void> {
|
|
63
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
64
|
+
}
|