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 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
+ [![CI](https://github.com/burcineren/pi-finance-tr/actions/workflows/ci.yml/badge.svg)](https://github.com/burcineren/pi-finance-tr/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](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
+ }
@@ -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,7 @@
1
+ export function createBorsaService(_config: unknown) {
2
+ return {
3
+ fetchHisse(_symbol: string): never {
4
+ throw new Error('Not yet implemented — see ROADMAP.md');
5
+ },
6
+ };
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,7 @@
1
+ export function createEmtiaService(_config: unknown) {
2
+ return {
3
+ fetchEmtia(_symbol: string): never {
4
+ throw new Error('Not yet implemented — see ROADMAP.md');
5
+ },
6
+ };
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,7 @@
1
+ export function createKapService(_config: unknown) {
2
+ return {
3
+ fetchBildirim(_query: string): never {
4
+ throw new Error('Not yet implemented — see ROADMAP.md');
5
+ },
6
+ };
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,4 @@
1
+ import type { ENABLED_MODULES } from './constants.ts';
2
+
3
+ export type EnabledModule = (typeof ENABLED_MODULES)[number];
4
+ export type EnvSource = Record<string, string | undefined>;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import xml2js from 'xml2js';
2
+
3
+ export async function parseXml<T>(xmlString: string): Promise<T> {
4
+ return xml2js.parseStringPromise(xmlString, { explicitArray: false }) as Promise<T>;
5
+ }