wormclaude 1.0.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/README.md +147 -0
- package/dist/agent.js +61 -0
- package/dist/api.js +163 -0
- package/dist/auth.js +108 -0
- package/dist/cli.js +851 -0
- package/dist/commands.js +540 -0
- package/dist/compact.js +53 -0
- package/dist/i18n.js +177 -0
- package/dist/learn.js +47 -0
- package/dist/links.js +31 -0
- package/dist/mcp.js +104 -0
- package/dist/memory.js +135 -0
- package/dist/skills.js +275 -0
- package/dist/tasks.js +63 -0
- package/dist/theme.js +11 -0
- package/dist/tips.js +60 -0
- package/dist/toolSummary.js +24 -0
- package/dist/tools.js +1136 -0
- package/dist/usage.js +71 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# WormClaude CLI
|
|
2
|
+
|
|
3
|
+
Qwen 2.5 tabanlı, OpenAI-uyumlu backend'e bağlanan uçbirim (ink TUI) AI kodlama/güvenlik asistanı.
|
|
4
|
+
Claude Code'un araç/komut/agent mimarisinin **yerel, bulut-bağımsız** uyarlamasıdır.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 📁 Dosya dosya: ne eklendi
|
|
9
|
+
|
|
10
|
+
### Yeni dosyalar (`src/`) — bu oturumda sıfırdan eklendi
|
|
11
|
+
|
|
12
|
+
| Dosya | Ne yapar |
|
|
13
|
+
|-------|----------|
|
|
14
|
+
| `agent.ts` | Yeniden kullanılabilir tool-calling döngüsü (`runAgentLoop`) — alt-agent çekirdeği. `completeText()` tek-seferlik yanıt yardımcısı. |
|
|
15
|
+
| `commands.ts` | 18 slash komutu (`/help /clear /compact /context /cost /config /model /diff /commit /review /init /memory /doctor /mcp /tasks /kill /dream /export /resume /quit`). |
|
|
16
|
+
| `tasks.ts` | Arka plan görev kayıt defteri (registry) — shell/agent/dream görevleri, durum/çıktı izleme, EventEmitter. |
|
|
17
|
+
| `mcp.ts` | MCP istemcisi (resmi `@modelcontextprotocol/sdk`). `.wormclaude/mcp.json`'dan sunucu okur, araçları OpenAI şemasına çevirir. |
|
|
18
|
+
| `usage.ts` | Billing/token ölçümü — model-bazlı maliyet, oturum+kümülatif sayaç, bütçe limiti, `.wormclaude/usage.json`. |
|
|
19
|
+
| `compact.ts` | Oto-compact: bağlam eşiği + 9-bölümlü özet promptu + `isContextError` (reactive compact tetiği). |
|
|
20
|
+
| `memory.ts` | Oto-hafıza: SessionMemory tetiği + autoDream gate + PID kilidi. `.wormclaude/memory.md`. |
|
|
21
|
+
| `toolSummary.ts` | Araç-grubu özeti — 2+ araç sonrası tek satır git-commit tarzı etiket. |
|
|
22
|
+
| `tips.ts` | Spinner/footer ipuçları (14 ipucu, cooldown'lı, `.wormclaude/tips.json`). |
|
|
23
|
+
|
|
24
|
+
### Değiştirilen dosyalar (mevcuttu, genişletildi)
|
|
25
|
+
|
|
26
|
+
| Dosya | Eklenen |
|
|
27
|
+
|-------|---------|
|
|
28
|
+
| `tools.ts` | Claude Code araç açıklamaları (birebir); `Agent` + `TaskOutput` araçları; `Bash run_in_background`; MCP yönlendirme; **paralel araç çalıştırma** (`executeToolCalls`); **per-tool metadata** (`TOOL_META`: readOnly/concurrencySafe/needsPermission/validate). |
|
|
29
|
+
| `api.ts` | `usage` yakalama (`stream_options.include_usage`); **retry/backoff** (`fetchWithRetry`, jitter); **abort/interrupt** (AbortSignal). |
|
|
30
|
+
| `cli.tsx` | Komut sistemi bağlama; footer task pill; MCP otomatik bağlanma; oto-compact + oto-hafıza tetikleri; usage kaydı; **izin dialogu**; **bütçe/maxTurns/Esc-kes**; **reactive compact**; tip + araç özeti gösterimi. |
|
|
31
|
+
| `theme.ts` | _(değişmedi)_ |
|
|
32
|
+
|
|
33
|
+
### Backend (`C:\Users\potur\Desktop\modelegitim\api\server.py`)
|
|
34
|
+
|
|
35
|
+
| Eklenen | Açıklama |
|
|
36
|
+
|---------|----------|
|
|
37
|
+
| `POST /v1/chat/completions` | OpenAI-uyumlu gateway: streaming + **tools geçişi** + **usage**. Ollama'yı sarar. |
|
|
38
|
+
| `GET /v1/models` | Model listesi. |
|
|
39
|
+
| Model takma adı | `wormclaude` → `qwen2.5:7b` (env `WORMCLAUDE_DEFAULT_MODEL`). |
|
|
40
|
+
| Opsiyonel auth | `WORMCLAUDE_API_KEYS` env'i set edilirse Bearer key zorunlu. |
|
|
41
|
+
| Billing log | `api/billing.jsonl` — istek başına `{ts, key, model, prompt_tokens, completion_tokens}`. |
|
|
42
|
+
|
|
43
|
+
> Not: Eski route'lar (`/chat`, `/chat/stream`) korundu; sadece `/v1/*` eklendi.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## ⚙️ Özellikler (tümü çalışır, test edildi)
|
|
48
|
+
|
|
49
|
+
- **Araçlar:** Bash, Read, Write, Edit, Glob, Grep, WebFetch (Claude Code birebir açıklamalar) + Agent, TaskOutput
|
|
50
|
+
- **Çoklu-agent:** alt-agent (`Agent`), arka plan görevleri (`run_in_background`), koordinatör, dream
|
|
51
|
+
- **MCP:** stdio/SSE/HTTP sunucu desteği
|
|
52
|
+
- **Paralel araç çalıştırma:** salt-okunur araçlar eşzamanlı (limit 10), yazanlar sıralı
|
|
53
|
+
- **İzin sistemi:** Bash/Write/Edit öncesi onay (Evet / Evet-hep / Hayır)
|
|
54
|
+
- **Billing:** token/maliyet ölçümü + oturum bütçe limiti
|
|
55
|
+
- **Retry/backoff:** 408/409/429/5xx/529 + bağlantı hataları
|
|
56
|
+
- **Oto-compact + reactive compact:** bağlam dolunca / taşınca otomatik özetle
|
|
57
|
+
- **Oto-hafıza:** arka planda kalıcı bilgi çıkarımı
|
|
58
|
+
- **Araç özeti + ipuçları:** UX cilası
|
|
59
|
+
- **Interrupt:** çalışırken Esc ile kes
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚫 Dosya dosya: ne EKLENMEDİ (ve neden)
|
|
64
|
+
|
|
65
|
+
Claude Code kaynağından (`Desktop\tools`, `Desktop\wormclaudegenel`) bilerek atlananlar — hepsi Anthropic bulutuna/hesabına/SDK'sına bağlı, yerelde karşılığı yok:
|
|
66
|
+
|
|
67
|
+
### `services/` — atlananlar
|
|
68
|
+
| Klasör/dosya | Neden eklenmedi |
|
|
69
|
+
|--------------|-----------------|
|
|
70
|
+
| `analytics/` (datadog, growthbook, sink...) | Anthropic telemetrisi |
|
|
71
|
+
| `api/` (claude.ts, bootstrap, usage, overage, referral) | Anthropic bulut API + faturalandırma _(mantığı: retry+billing uyarlandı)_ |
|
|
72
|
+
| `oauth/` | Anthropic hesabı/OAuth |
|
|
73
|
+
| `mcp/` (auth, claudeai, xaa, officialRegistry...) | Bulut-bağımlı MCP _(çekirdek MCP istemcisi yeniden yazıldı)_ |
|
|
74
|
+
| `plugins/` | Eklenti/marketplace sistemi |
|
|
75
|
+
| `policyLimits/`, `remoteManagedSettings/`, `settingsSync/`, `teamMemorySync/` | Kurumsal/uzak senkronizasyon |
|
|
76
|
+
| `voice*` (4 dosya) | Harici STT servisi |
|
|
77
|
+
| `rateLimit*`, `mockRateLimits`, `claudeAiLimits*` | Anthropic abonelik limitleri |
|
|
78
|
+
| `notifier`, `diagnosticTracking`, `internalLogging`, `vcr`, `preventSleep` | İç altyapı/OS |
|
|
79
|
+
| `lsp/` (7 dosya) | Kod dil-sunucusu — opsiyonel, kurulmadı |
|
|
80
|
+
| `MagicDocs/`, `AgentSummary/`, `PromptSuggestion/` (+speculation), `awaySummary` | İncelendi, orta/düşük değer → opsiyonel bırakıldı |
|
|
81
|
+
|
|
82
|
+
### `cli/` (giriş/transport katmanı) — tamamı atlandı
|
|
83
|
+
`transports/` (WebSocket/SSE/remote), `handlers/` (auth/mcp/plugins/agents), `print.ts`, `structuredIO.ts`, `remoteIO.ts`, `update.ts` → Anthropic uzak agent/SDK protokolü, yerelde gereksiz.
|
|
84
|
+
|
|
85
|
+
### `tasks/` — kısmen
|
|
86
|
+
| Eklenen | Atlanan |
|
|
87
|
+
|---------|---------|
|
|
88
|
+
| LocalShellTask (arka plan Bash), LocalAgentTask (alt-agent), DreamTask (hafıza) → `tasks.ts`'e uyarlandı | `RemoteAgentTask` (uzak bulut), `InProcessTeammateTask` (AsyncLocalStorage takım izolasyonu), `coordinator/` (analytics-bağımlı) |
|
|
89
|
+
|
|
90
|
+
### Çekirdek motor (`query.ts`, `QueryEngine.ts`, `Tool.ts`, `tools.ts`) — incelendi
|
|
91
|
+
| Uyarlandı | Atlandı (opsiyonel) |
|
|
92
|
+
|-----------|---------------------|
|
|
93
|
+
| Agent döngüsü, paralel araç, per-tool arayüz, izin, bütçe/maxTurns/interrupt, reactive compact | Streaming tool executor, stop hooks, max-output-token escalation (8K→64K) |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🔧 Ortam değişkenleri
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
WORMCLAUDE_BASE_URL=http://127.0.0.1:8788/v1 # backend
|
|
101
|
+
WORMCLAUDE_API_KEY=wormclaude # backend key
|
|
102
|
+
WORMCLAUDE_MODEL=wormclaude # model (backend qwen2.5:7b'ye çözer)
|
|
103
|
+
WORMCLAUDE_MAX_TURNS=25 # tur limiti
|
|
104
|
+
WORMCLAUDE_BUDGET_USD=0 # oturum maliyet tavanı (0=sınırsız)
|
|
105
|
+
WORMCLAUDE_CONTEXT_WINDOW=32768 # oto-compact için pencere
|
|
106
|
+
WORMCLAUDE_MAX_RETRIES=5 # bağlantı retry
|
|
107
|
+
WORMCLAUDE_MAX_TOOL_CONCURRENCY=10 # paralel araç limiti
|
|
108
|
+
# Backend tarafı:
|
|
109
|
+
WORMCLAUDE_DEFAULT_MODEL=qwen2.5:7b # takma ad hedefi
|
|
110
|
+
WORMCLAUDE_API_KEYS=key1,key2 # auth (boşsa kapalı)
|
|
111
|
+
OLLAMA_NUM_CTX=8192 # Ollama bağlam
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🚀 Kurulum & çalıştırma
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Backend (ayrı pencere)
|
|
120
|
+
powershell -File C:\Users\potur\Desktop\modelegitim\api\BASLAT_API.ps1
|
|
121
|
+
|
|
122
|
+
# CLI
|
|
123
|
+
cd C:\Users\potur\Desktop\wormclaude-cli-ts
|
|
124
|
+
npm install
|
|
125
|
+
npm run build
|
|
126
|
+
wormclaude # veya: npm run dev
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### MCP sunucu eklemek
|
|
130
|
+
`.wormclaude/mcp.json`:
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"mcpServers": {
|
|
134
|
+
"filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:\\proje\\yol"] }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
Sonra CLI'da `/mcp` ile gör, `/mcp reconnect` ile yeniden bağla.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 💰 Ticari (aylık abonelik) için hazır kancalar
|
|
143
|
+
|
|
144
|
+
1. **Auth aç:** `WORMCLAUDE_API_KEYS="musteri1,musteri2"` — her müşteriye anahtar
|
|
145
|
+
2. **Fiyat koy:** `usage.setPricing("qwen2.5:7b", { in: 0.5, out: 1.5 })` — `/cost` USD gösterir
|
|
146
|
+
3. **Bütçe tavanı:** `WORMCLAUDE_BUDGET_USD=5` — oturum maliyeti aşınca durur
|
|
147
|
+
4. **Faturalandır:** `api/billing.jsonl`'i aylık topla (server-tarafı = güvenilir kaynak)
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Faz 2 — Yeniden kullanılabilir tool-calling döngüsü (alt-agent çekirdeği).
|
|
2
|
+
// Ana CLI döngüsünden bağımsız; alt-agent'lar (Agent aracı) ve dream görevleri
|
|
3
|
+
// bunu headless olarak kullanır. Aynı OpenAI-uyumlu backend'e gider.
|
|
4
|
+
import { streamChat } from './api.js';
|
|
5
|
+
import { executeToolCalls } from './tools.js';
|
|
6
|
+
// Tek seferlik tam metin yanıt (araç yok). compact/memory/komutlar kullanır.
|
|
7
|
+
export async function completeText(messages, config) {
|
|
8
|
+
let out = '';
|
|
9
|
+
for await (const ev of streamChat(messages, [], config)) {
|
|
10
|
+
if (ev.type === 'text')
|
|
11
|
+
out += ev.text;
|
|
12
|
+
else if (ev.type === 'error')
|
|
13
|
+
return `[hata: ${ev.error}]`;
|
|
14
|
+
}
|
|
15
|
+
return out.trim();
|
|
16
|
+
}
|
|
17
|
+
export async function runAgentLoop(opts) {
|
|
18
|
+
const { config, tools, executeTool } = opts;
|
|
19
|
+
const messages = opts.messages;
|
|
20
|
+
const maxIters = opts.maxIters ?? 12;
|
|
21
|
+
let finalText = '';
|
|
22
|
+
let iter = 0;
|
|
23
|
+
for (; iter < maxIters; iter++) {
|
|
24
|
+
if (opts.signal?.aborted)
|
|
25
|
+
break;
|
|
26
|
+
let assistantText = '';
|
|
27
|
+
let toolCalls = [];
|
|
28
|
+
for await (const ev of streamChat(messages, tools, config)) {
|
|
29
|
+
if (ev.type === 'text') {
|
|
30
|
+
assistantText += ev.text;
|
|
31
|
+
opts.hooks?.onText?.(ev.text);
|
|
32
|
+
}
|
|
33
|
+
else if (ev.type === 'done') {
|
|
34
|
+
toolCalls = ev.toolCalls;
|
|
35
|
+
}
|
|
36
|
+
else if (ev.type === 'error') {
|
|
37
|
+
assistantText += `\n[hata: ${ev.error}]`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const asMsg = { role: 'assistant', content: assistantText || '' };
|
|
41
|
+
if (toolCalls.length) {
|
|
42
|
+
asMsg.tool_calls = toolCalls.map((t) => ({
|
|
43
|
+
id: t.id, type: 'function', function: { name: t.name, arguments: t.args || '{}' },
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
messages.push(asMsg);
|
|
47
|
+
if (assistantText.trim())
|
|
48
|
+
finalText = assistantText.trim();
|
|
49
|
+
if (!toolCalls.length)
|
|
50
|
+
break;
|
|
51
|
+
// Paralel çalıştırma (salt-okunur araçlar eşzamanlı). Sonuçlar orijinal sırada.
|
|
52
|
+
const results = await executeToolCalls(toolCalls, {
|
|
53
|
+
onStart: (c) => opts.hooks?.onTool?.(c.name, undefined),
|
|
54
|
+
onResult: (c, _i, res) => opts.hooks?.onToolResult?.(c.name, res.args, res.ok, res.output),
|
|
55
|
+
});
|
|
56
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
57
|
+
messages.push({ role: 'tool', tool_call_id: toolCalls[i].id, content: results[i].output.slice(0, 8000) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { finalText, messages, iters: iter };
|
|
61
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Backend istemci — OpenAI-uyumlu /v1/chat/completions (streaming + tools).
|
|
2
|
+
// Dayanıklılık: bağlantı kurulumunda exponential backoff + jitter retry.
|
|
3
|
+
// Ölçüm: usage (token) bilgisini 'done' olayında döndürür (billing için).
|
|
4
|
+
import { loadStored, DEFAULT_BASE_URL } from './auth.js';
|
|
5
|
+
export function loadConfig() {
|
|
6
|
+
const stored = loadStored();
|
|
7
|
+
return {
|
|
8
|
+
baseUrl: process.env.WORMCLAUDE_BASE_URL || stored.baseUrl || DEFAULT_BASE_URL,
|
|
9
|
+
apiKey: process.env.WORMCLAUDE_API_KEY || stored.apiKey || '',
|
|
10
|
+
model: process.env.WORMCLAUDE_MODEL || stored.model || 'wormclaude',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
// ── Retry/backoff (api/withRetry.ts'den uyarlandı) ────────────────────────────
|
|
14
|
+
const MAX_RETRIES = Number(process.env.WORMCLAUDE_MAX_RETRIES) || 5;
|
|
15
|
+
const BASE_DELAY_MS = 500;
|
|
16
|
+
const MAX_DELAY_MS = 30000;
|
|
17
|
+
const RETRYABLE_STATUS = new Set([408, 409, 429, 500, 502, 503, 504, 529]);
|
|
18
|
+
const RETRYABLE_CODES = new Set(['ECONNRESET', 'EPIPE', 'ECONNREFUSED', 'ETIMEDOUT', 'UND_ERR_SOCKET']);
|
|
19
|
+
function backoff(attempt) {
|
|
20
|
+
const base = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
|
|
21
|
+
return base + Math.random() * 0.25 * base; // jitter %25
|
|
22
|
+
}
|
|
23
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
// Sadece BAĞLANTI kurulurken retry edilir (stream başladıktan sonra retry edilmez
|
|
25
|
+
// — aksi halde kısmi metin tekrar gelir). HTTP başlamadan önceki hatalar güvenli.
|
|
26
|
+
async function fetchWithRetry(url, init) {
|
|
27
|
+
let lastErr;
|
|
28
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(url, init);
|
|
31
|
+
if (res.ok)
|
|
32
|
+
return res;
|
|
33
|
+
if (!RETRYABLE_STATUS.has(res.status) || attempt === MAX_RETRIES)
|
|
34
|
+
return res;
|
|
35
|
+
try {
|
|
36
|
+
await res.body?.cancel();
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
if (e?.name === 'AbortError')
|
|
42
|
+
throw e; // kullanıcı kesti → retry etme
|
|
43
|
+
lastErr = e;
|
|
44
|
+
const code = e?.cause?.code || e?.code || '';
|
|
45
|
+
if (!RETRYABLE_CODES.has(code) && attempt === MAX_RETRIES)
|
|
46
|
+
throw e;
|
|
47
|
+
if (attempt === MAX_RETRIES)
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
await sleep(backoff(attempt));
|
|
51
|
+
}
|
|
52
|
+
throw lastErr ?? new Error('retry exhausted');
|
|
53
|
+
}
|
|
54
|
+
export async function* streamChat(messages, tools, config, signal) {
|
|
55
|
+
const body = {
|
|
56
|
+
model: config.model,
|
|
57
|
+
messages,
|
|
58
|
+
stream: true,
|
|
59
|
+
temperature: 0.3,
|
|
60
|
+
stream_options: { include_usage: true }, // son chunk'ta usage döner
|
|
61
|
+
};
|
|
62
|
+
if (tools && tools.length)
|
|
63
|
+
body.tools = tools;
|
|
64
|
+
let res;
|
|
65
|
+
try {
|
|
66
|
+
res = await fetchWithRetry(`${config.baseUrl}/chat/completions`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
signal,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (signal?.aborted || e?.name === 'AbortError') {
|
|
78
|
+
yield { type: 'done', toolCalls: [] };
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
yield { type: 'error', error: `Backend'e ulaşılamadı (${config.baseUrl}): ${e?.message || e}` };
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok || !res.body) {
|
|
85
|
+
let detail = '';
|
|
86
|
+
try {
|
|
87
|
+
detail = await res.text();
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
yield { type: 'error', error: `HTTP ${res.status}: ${detail.slice(0, 300)}` };
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const reader = res.body.getReader();
|
|
94
|
+
const decoder = new TextDecoder();
|
|
95
|
+
let buf = '';
|
|
96
|
+
const toolAcc = {};
|
|
97
|
+
let usage;
|
|
98
|
+
while (true) {
|
|
99
|
+
if (signal?.aborted) {
|
|
100
|
+
try {
|
|
101
|
+
await reader.cancel();
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
let chunk;
|
|
107
|
+
try {
|
|
108
|
+
chunk = await reader.read();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
if (signal?.aborted || e?.name === 'AbortError')
|
|
112
|
+
break;
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
115
|
+
const { done, value } = chunk;
|
|
116
|
+
if (done)
|
|
117
|
+
break;
|
|
118
|
+
buf += decoder.decode(value, { stream: true });
|
|
119
|
+
const lines = buf.split('\n');
|
|
120
|
+
buf = lines.pop() || '';
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const t = line.trim();
|
|
123
|
+
if (!t.startsWith('data:'))
|
|
124
|
+
continue;
|
|
125
|
+
const data = t.slice(5).trim();
|
|
126
|
+
if (data === '[DONE]')
|
|
127
|
+
continue;
|
|
128
|
+
let j;
|
|
129
|
+
try {
|
|
130
|
+
j = JSON.parse(data);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (j.usage) {
|
|
136
|
+
usage = {
|
|
137
|
+
input: j.usage.prompt_tokens || 0,
|
|
138
|
+
output: j.usage.completion_tokens || 0,
|
|
139
|
+
cacheRead: j.usage.prompt_tokens_details?.cached_tokens || 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const delta = j.choices?.[0]?.delta;
|
|
143
|
+
if (!delta)
|
|
144
|
+
continue;
|
|
145
|
+
if (delta.content)
|
|
146
|
+
yield { type: 'text', text: delta.content };
|
|
147
|
+
if (delta.tool_calls) {
|
|
148
|
+
for (const tc of delta.tool_calls) {
|
|
149
|
+
const idx = tc.index ?? 0;
|
|
150
|
+
if (!toolAcc[idx])
|
|
151
|
+
toolAcc[idx] = { id: tc.id || `call_${idx}`, name: '', args: '' };
|
|
152
|
+
if (tc.id)
|
|
153
|
+
toolAcc[idx].id = tc.id;
|
|
154
|
+
if (tc.function?.name)
|
|
155
|
+
toolAcc[idx].name += tc.function.name;
|
|
156
|
+
if (tc.function?.arguments)
|
|
157
|
+
toolAcc[idx].args += tc.function.arguments;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
yield { type: 'done', toolCalls: Object.values(toolAcc), usage };
|
|
163
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// CLI kimlik: ~/.wormclaude/config.json okuma/yazma + tarayıcı device-login akışı.
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.wormclaude');
|
|
7
|
+
export const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
// Müşteriler bunu değiştirmesin diye varsayılan public API.
|
|
9
|
+
export const DEFAULT_BASE_URL = 'https://api.wormclaude.ai/v1';
|
|
10
|
+
export function loadStored() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function saveStored(cfg) {
|
|
19
|
+
try {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
process.stderr.write('Yapılandırma kaydedilemedi: ' + (e?.message || e) + '\n');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function clearStored() {
|
|
28
|
+
try {
|
|
29
|
+
fs.rmSync(CONFIG_PATH);
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
// baseUrl ".../v1" → device endpoint'leri için kök (".../v1" olmadan)
|
|
34
|
+
function apiOrigin(baseUrl) {
|
|
35
|
+
return baseUrl.replace(/\/v1\/?$/, '');
|
|
36
|
+
}
|
|
37
|
+
function openBrowser(url) {
|
|
38
|
+
try {
|
|
39
|
+
let child;
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
child = spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
|
|
42
|
+
}
|
|
43
|
+
else if (process.platform === 'darwin') {
|
|
44
|
+
child = spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
48
|
+
}
|
|
49
|
+
// spawn hatası async 'error' event'i olarak gelir; yutmazsak süreç çöker.
|
|
50
|
+
child.on('error', () => { });
|
|
51
|
+
child.unref();
|
|
52
|
+
}
|
|
53
|
+
catch { /* sessiz geç */ }
|
|
54
|
+
}
|
|
55
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
56
|
+
export async function deviceLogin() {
|
|
57
|
+
const base = process.env.WORMCLAUDE_BASE_URL || loadStored().baseUrl || DEFAULT_BASE_URL;
|
|
58
|
+
const origin = apiOrigin(base);
|
|
59
|
+
let start;
|
|
60
|
+
try {
|
|
61
|
+
const r = await fetch(`${origin}/cli/device/start`, { method: 'POST' });
|
|
62
|
+
if (!r.ok)
|
|
63
|
+
throw new Error('HTTP ' + r.status);
|
|
64
|
+
start = await r.json();
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
process.stderr.write('Giriş başlatılamadı: ' + (e?.message || e) + '\n');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const url = start.verification_uri_complete || start.verification_uri;
|
|
71
|
+
const userCode = start.user_code;
|
|
72
|
+
const deviceCode = start.device_code;
|
|
73
|
+
process.stdout.write('\n');
|
|
74
|
+
process.stdout.write(' Tarayıcıda açın ve giriş yapın:\n');
|
|
75
|
+
process.stdout.write(' \x1b[36m' + url + '\x1b[0m\n\n');
|
|
76
|
+
process.stdout.write(' Doğrulama kodu: \x1b[1m' + userCode + '\x1b[0m\n\n');
|
|
77
|
+
openBrowser(url);
|
|
78
|
+
const pollMs = Math.max(2, Number(start.interval) || 3) * 1000;
|
|
79
|
+
const deadline = Date.now() + (Number(start.expires_in) || 600) * 1000;
|
|
80
|
+
process.stdout.write(' Onay bekleniyor');
|
|
81
|
+
while (Date.now() < deadline) {
|
|
82
|
+
await sleep(pollMs);
|
|
83
|
+
process.stdout.write('.');
|
|
84
|
+
let d;
|
|
85
|
+
try {
|
|
86
|
+
const r = await fetch(`${origin}/cli/device/poll?device_code=${encodeURIComponent(deviceCode)}`);
|
|
87
|
+
d = await r.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue; // geçici ağ hatası — tekrar dene
|
|
91
|
+
}
|
|
92
|
+
if (d.status === 'approved') {
|
|
93
|
+
saveStored({
|
|
94
|
+
baseUrl: d.base_url || base,
|
|
95
|
+
apiKey: d.api_key,
|
|
96
|
+
model: d.model || 'wormclaude',
|
|
97
|
+
});
|
|
98
|
+
process.stdout.write('\n\n \x1b[32m✓ Giriş başarılı!\x1b[0m Artık `wormclaude` yazarak kullanabilirsiniz.\n\n');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (d.status === 'expired') {
|
|
102
|
+
process.stdout.write('\n\n Kodun süresi doldu. Tekrar deneyin: wormclaude login\n\n');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write('\n\n Süre doldu. Tekrar deneyin: wormclaude login\n\n');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|