@deerdesign/deertools 1.0.4 → 1.2.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.
@@ -0,0 +1,386 @@
1
+ 'use strict';
2
+ const http = require('http');
3
+ const url = require('url');
4
+ const { getClient } = require('./client');
5
+ const { getBaseLinkerToken, saveBaseLinkerToken } = require('../credentials');
6
+ const { getRecentAudit } = require('../audit');
7
+ const { searchMethods, loadSchemas } = require('./crawler');
8
+ const { handleMcpMessage } = require('./mcp-server');
9
+
10
+ function renderHtml(port) {
11
+ return `<!DOCTYPE html>
12
+ <html lang="pl">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>DeerTools · BaseLinker MCP Proxy & Hub</title>
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
20
+ <style>
21
+ :root {
22
+ --bg: #0B0F17;
23
+ --card-bg: rgba(22, 29, 43, 0.75);
24
+ --card-border: rgba(255, 255, 255, 0.08);
25
+ --primary: #C5A880;
26
+ --primary-hover: #D4BA94;
27
+ --text: #F1F5F9;
28
+ --text-muted: #94A3B8;
29
+ --success: #10B981;
30
+ --danger: #EF4444;
31
+ --warning: #F59E0B;
32
+ }
33
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Plus Jakarta Sans', sans-serif; }
34
+ body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 32px 20px; }
35
+ .container { max-width: 1080px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
36
+ header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16px; border-bottom: 1px solid var(--card-border); }
37
+ .logo-badge { display: flex; align-items: center; gap: 12px; }
38
+ .logo-badge h1 { font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -0.5px; }
39
+ .badge { background: rgba(197, 168, 128, 0.15); color: var(--primary); padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; border: 1px solid rgba(197, 168, 128, 0.3); }
40
+ .grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 24px; }
41
+ @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
42
+ .card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 16px; padding: 24px; backdrop-filter: blur(12px); }
43
+ .card-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; }
44
+ .form-group { margin-bottom: 16px; }
45
+ label { display: block; font-size: 13px; color: var(--text-muted); margin-bottom: 6px; font-weight: 500; }
46
+ input[type="text"], input[type="password"] {
47
+ width: 100%; background: rgba(11, 15, 23, 0.8); border: 1px solid var(--card-border); border-radius: 8px;
48
+ padding: 10px 14px; color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s;
49
+ }
50
+ input[type="text"]:focus, input[type="password"]:focus { border-color: var(--primary); }
51
+ .btn-row { display: flex; gap: 10px; }
52
+ button {
53
+ padding: 10px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: all 0.2s;
54
+ }
55
+ .btn-primary { background: var(--primary); color: #000; }
56
+ .btn-primary:hover { background: var(--primary-hover); transform: translateY(-1px); }
57
+ .btn-secondary { background: rgba(255, 255, 255, 0.08); color: #fff; border: 1px solid var(--card-border); }
58
+ .btn-secondary:hover { background: rgba(255, 255, 255, 0.15); }
59
+ .status-meter { display: flex; align-items: center; gap: 12px; margin-top: 12px; background: rgba(11, 15, 23, 0.5); padding: 12px; border-radius: 8px; }
60
+ .meter-circle { width: 10px; height: 10px; border-radius: 50%; background: var(--success); }
61
+ .meter-circle.busy { background: var(--warning); }
62
+ .code-box {
63
+ background: #06090e; border: 1px solid var(--card-border); border-radius: 8px; padding: 14px;
64
+ font-family: monospace; font-size: 12px; color: #A5B4FC; overflow-x: auto; white-space: pre; position: relative;
65
+ }
66
+ table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
67
+ th { text-align: left; padding: 8px; color: var(--text-muted); border-bottom: 1px solid var(--card-border); font-weight: 500; }
68
+ td { padding: 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.04); }
69
+ .status-ok { color: var(--success); font-weight: 600; }
70
+ .status-err { color: var(--danger); font-weight: 600; }
71
+ .notice { font-size: 12px; color: var(--text-muted); margin-top: 8px; line-height: 1.4; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="container">
76
+ <header>
77
+ <div class="logo-badge">
78
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#C5A880" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg>
79
+ <h1>DeerTools · BaseLinker Hub</h1>
80
+ <span class="badge">Local MCP Proxy</span>
81
+ </div>
82
+ <div>
83
+ <span style="font-size: 13px; color: var(--text-muted);">Port: <b style="color: #fff;">${port}</b></span>
84
+ </div>
85
+ </header>
86
+
87
+ <div class="grid">
88
+ <!-- Karta konfiguracji tokena -->
89
+ <div class="card">
90
+ <div class="card-title">
91
+ <span>Klucz API BaseLinker (X-BLToken)</span>
92
+ <span id="connStatus" class="badge" style="background: rgba(16,185,129,0.15); color: #10B981;">Sprawdzanie...</span>
93
+ </div>
94
+ <p class="notice" style="margin-bottom: 14px;">Token wygenerujesz w panelu BaseLinker w sekcji <b>Konto / inne &rarr; Moje konto &rarr; API</b>.</p>
95
+ <div class="form-group">
96
+ <label for="tokenInput">Twój X-BLToken</label>
97
+ <input type="password" id="tokenInput" placeholder="Wklej token np. 3001234-45678-ABCDEF...">
98
+ </div>
99
+ <div class="btn-row">
100
+ <button class="btn-primary" onclick="saveToken()">Zapisz token</button>
101
+ <button class="btn-secondary" onclick="testToken()">Testuj połączenie</button>
102
+ </div>
103
+ <div class="status-meter">
104
+ <div id="meterDot" class="meter-circle"></div>
105
+ <div style="font-size: 13px;">
106
+ Limit zapytań: <b id="rateLimitCount">0</b> / 100 req/min
107
+ <span style="color: var(--text-muted); margin-left: 8px;">(odnowienie za <span id="rateLimitReset">60</span>s)</span>
108
+ </div>
109
+ </div>
110
+ <div id="testResult" style="margin-top: 12px; font-size: 13px;"></div>
111
+ </div>
112
+
113
+ <!-- Karta integracji MCP z Cursor / Claude / Antigravity -->
114
+ <div class="card">
115
+ <div class="card-title">
116
+ <span>Konfiguracja w Cursor / Claude Desktop</span>
117
+ </div>
118
+ <p class="notice" style="margin-bottom: 10px;">Dodaj poniższy wpis do <code>~/.cursor/mcp.json</code> lub pliku konfiguracyjnego Claude Desktop:</p>
119
+ <div class="code-box">{
120
+ "mcpServers": {
121
+ "baselinker": {
122
+ "command": "deertools",
123
+ "args": ["baselinker", "mcp"]
124
+ }
125
+ }
126
+ }</div>
127
+ <p class="notice" style="margin-top: 12px;">Dla trybu HTTP / SSE wskaż adres: <code>http://localhost:${port}/sse</code></p>
128
+ </div>
129
+ </div>
130
+
131
+ <!-- Ostatnie wywołania API (Live Traffic) -->
132
+ <div class="card">
133
+ <div class="card-title">
134
+ <span>Monitor zapytań agenta (Live Traffic)</span>
135
+ <button class="btn-secondary" style="padding: 4px 10px; font-size: 11px;" onclick="loadTraffic()">Odśwież</button>
136
+ </div>
137
+ <table>
138
+ <thead>
139
+ <tr>
140
+ <th>Czas</th>
141
+ <th>Metoda BaseLinker</th>
142
+ <th>Czas (ms)</th>
143
+ <th>Status</th>
144
+ <th>Szczegóły</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody id="trafficTable">
148
+ <tr><td colspan="5" style="color: var(--text-muted); text-align: center;">Brak zarejestrowanych zapytań w tej sesji.</td></tr>
149
+ </tbody>
150
+ </table>
151
+ </div>
152
+ </div>
153
+
154
+ <script>
155
+ async function loadStatus() {
156
+ try {
157
+ const res = await fetch('/api/status');
158
+ const data = await res.json();
159
+ const connBadge = document.getElementById('connStatus');
160
+ if (data.hasToken) {
161
+ connBadge.textContent = 'Token aktywny (' + data.tokenMasked + ')';
162
+ connBadge.style.color = '#10B981';
163
+ connBadge.style.background = 'rgba(16,185,129,0.15)';
164
+ } else {
165
+ connBadge.textContent = 'Brak tokenu';
166
+ connBadge.style.color = '#EF4444';
167
+ connBadge.style.background = 'rgba(239,68,68,0.15)';
168
+ }
169
+ document.getElementById('rateLimitCount').textContent = data.rateLimit.used;
170
+ document.getElementById('rateLimitReset').textContent = data.rateLimit.resetInMs;
171
+ const dot = document.getElementById('meterDot');
172
+ if (data.rateLimit.used > 80) dot.className = 'meter-circle busy';
173
+ else dot.className = 'meter-circle';
174
+ } catch (e) {
175
+ console.error(e);
176
+ }
177
+ }
178
+
179
+ async function saveToken() {
180
+ const token = document.getElementById('tokenInput').value.trim();
181
+ if (!token) return alert('Wpisz token!');
182
+ const res = await fetch('/api/token', {
183
+ method: 'POST',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({ token })
186
+ });
187
+ const data = await res.json();
188
+ if (data.ok) {
189
+ alert('Token zapisany pomyślnie w credentials.json!');
190
+ document.getElementById('tokenInput').value = '';
191
+ loadStatus();
192
+ testToken();
193
+ } else {
194
+ alert('Błąd: ' + data.error);
195
+ }
196
+ }
197
+
198
+ async function testToken() {
199
+ const resBox = document.getElementById('testResult');
200
+ resBox.innerHTML = '<span style="color: #94A3B8;">Testowanie połączenia...</span>';
201
+ try {
202
+ const res = await fetch('/api/test', { method: 'POST' });
203
+ const data = await res.json();
204
+ if (data.ok) {
205
+ const invCount = data.inventories ? data.inventories.length : 0;
206
+ resBox.innerHTML = '<span class="status-ok">✓ Połączenie udane! Znaleziono katalogów w BaseLinker: ' + invCount + '</span>';
207
+ } else {
208
+ resBox.innerHTML = '<span class="status-err">✗ Błąd połączenia: ' + data.error + '</span>';
209
+ }
210
+ } catch (err) {
211
+ resBox.innerHTML = '<span class="status-err">✗ Błąd sieci: ' + err.message + '</span>';
212
+ }
213
+ loadStatus();
214
+ loadTraffic();
215
+ }
216
+
217
+ async function loadTraffic() {
218
+ try {
219
+ const res = await fetch('/api/traffic');
220
+ const data = await res.json();
221
+ const tbody = document.getElementById('trafficTable');
222
+ if (!data.items || data.items.length === 0) {
223
+ tbody.innerHTML = '<tr><td colspan="5" style="color: var(--text-muted); text-align: center;">Brak zapytań.</td></tr>';
224
+ return;
225
+ }
226
+ tbody.innerHTML = data.items.map(i => \`
227
+ <tr>
228
+ <td>\${new Date(i.timestamp).toLocaleTimeString()}</td>
229
+ <td><b>\${i.method}</b></td>
230
+ <td>\${i.durationMs} ms</td>
231
+ <td><span class="\${i.ok ? 'status-ok' : 'status-err'}">\${i.ok ? 'SUCCESS' : 'ERROR'}</span></td>
232
+ <td style="color: var(--text-muted); font-size: 12px;">\${i.details || ''}</td>
233
+ </tr>
234
+ \`).join('');
235
+ } catch (e) {}
236
+ }
237
+
238
+ loadStatus();
239
+ loadTraffic();
240
+ setInterval(loadStatus, 5000);
241
+ setInterval(loadTraffic, 8000);
242
+ </script>
243
+ </body>
244
+ </html>`;
245
+ }
246
+
247
+ function startBaseLinkerServer(port = 3456) {
248
+ const sseClients = new Set();
249
+
250
+ const server = http.createServer(async (req, res) => {
251
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
252
+ const pathname = parsedUrl.pathname;
253
+
254
+ // CORS
255
+ res.setHeader('Access-Control-Allow-Origin', '*');
256
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
257
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-BLToken, Authorization');
258
+
259
+ if (req.method === 'OPTIONS') {
260
+ res.writeHead(204);
261
+ res.end();
262
+ return;
263
+ }
264
+
265
+ // Web Dashboard UI
266
+ if (pathname === '/' || pathname === '/index.html') {
267
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
268
+ res.end(renderHtml(port));
269
+ return;
270
+ }
271
+
272
+ // Status endpoint
273
+ if (pathname === '/api/status' && req.method === 'GET') {
274
+ const token = getBaseLinkerToken();
275
+ const client = getClient();
276
+ const rate = client.getRateLimitStatus();
277
+ res.writeHead(200, { 'Content-Type': 'application/json' });
278
+ res.end(JSON.stringify({
279
+ hasToken: !!token,
280
+ tokenMasked: token ? `${token.slice(0, 8)}...${token.slice(-4)}` : null,
281
+ rateLimit: rate
282
+ }));
283
+ return;
284
+ }
285
+
286
+ // Save token endpoint
287
+ if (pathname === '/api/token' && req.method === 'POST') {
288
+ let body = '';
289
+ req.on('data', chunk => body += chunk);
290
+ req.on('end', () => {
291
+ try {
292
+ const { token } = JSON.parse(body);
293
+ if (!token) throw new Error('Brak tokenu');
294
+ saveBaseLinkerToken(token);
295
+ res.writeHead(200, { 'Content-Type': 'application/json' });
296
+ res.end(JSON.stringify({ ok: true }));
297
+ } catch (err) {
298
+ res.writeHead(400, { 'Content-Type': 'application/json' });
299
+ res.end(JSON.stringify({ ok: false, error: err.message }));
300
+ }
301
+ });
302
+ return;
303
+ }
304
+
305
+ // Test token connection endpoint
306
+ if (pathname === '/api/test' && req.method === 'POST') {
307
+ const client = getClient();
308
+ const result = await client.testConnection();
309
+ res.writeHead(200, { 'Content-Type': 'application/json' });
310
+ res.end(JSON.stringify(result));
311
+ return;
312
+ }
313
+
314
+ // Traffic / Audit log endpoint
315
+ if (pathname === '/api/traffic' && req.method === 'GET') {
316
+ const items = getRecentAudit(25).filter(i => i.service === 'baselinker');
317
+ res.writeHead(200, { 'Content-Type': 'application/json' });
318
+ res.end(JSON.stringify({ items }));
319
+ return;
320
+ }
321
+
322
+ // Search methods endpoint
323
+ if (pathname === '/api/methods' && req.method === 'GET') {
324
+ const q = parsedUrl.searchParams.get('q') || '';
325
+ const cat = parsedUrl.searchParams.get('cat') || null;
326
+ const methods = searchMethods(q, cat);
327
+ res.writeHead(200, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ count: methods.length, methods }));
329
+ return;
330
+ }
331
+
332
+ // Remote MCP via SSE
333
+ if (pathname === '/sse') {
334
+ res.writeHead(200, {
335
+ 'Content-Type': 'text/event-stream',
336
+ 'Cache-Control': 'no-cache',
337
+ 'Connection': 'keep-alive'
338
+ });
339
+ const clientId = Date.now().toString(36) + Math.random().toString(36).slice(2);
340
+ sseClients.add(res);
341
+
342
+ res.write(`event: endpoint\ndata: /messages?sessionId=${clientId}\n\n`);
343
+
344
+ req.on('close', () => {
345
+ sseClients.delete(res);
346
+ });
347
+ return;
348
+ }
349
+
350
+ // Remote MCP messages handler
351
+ if (pathname === '/messages' && req.method === 'POST') {
352
+ let body = '';
353
+ req.on('data', chunk => body += chunk);
354
+ req.on('end', async () => {
355
+ try {
356
+ const jsonRpcMsg = JSON.parse(body);
357
+ const response = await handleMcpMessage(jsonRpcMsg);
358
+ if (response) {
359
+ for (const sseRes of sseClients) {
360
+ sseRes.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`);
361
+ }
362
+ }
363
+ res.writeHead(202);
364
+ res.end('Accepted');
365
+ } catch (err) {
366
+ res.writeHead(400, { 'Content-Type': 'application/json' });
367
+ res.end(JSON.stringify({ error: err.message }));
368
+ }
369
+ });
370
+ return;
371
+ }
372
+
373
+ res.writeHead(404);
374
+ res.end('Not Found');
375
+ });
376
+
377
+ server.listen(port, () => {
378
+ console.log(`\n✓ BaseLinker Hub & MCP Proxy uruchomiony na: http://localhost:${port}`);
379
+ console.log(` - Web UI: http://localhost:${port}`);
380
+ console.log(` - MCP SSE: http://localhost:${port}/sse\n`);
381
+ });
382
+
383
+ return server;
384
+ }
385
+
386
+ module.exports = { startBaseLinkerServer };
@@ -0,0 +1,180 @@
1
+ 'use strict';
2
+ const { getClient } = require('./baselinker/client');
3
+ const { getBaseLinkerToken, saveBaseLinkerToken } = require('./credentials');
4
+ const { startBaseLinkerServer } = require('./baselinker/ui');
5
+ const { runBaseLinkerMcpServer } = require('./baselinker/mcp-server');
6
+ const { syncSchema, searchMethods, loadSchemas } = require('./baselinker/crawler');
7
+ const { handleCatalogToolCall } = require('./baselinker/catalog-tools');
8
+ const { formatTable, c } = require('./formatters');
9
+
10
+ async function cmdBaseLinker(pos, flags) {
11
+ const sub = pos.shift() || 'help';
12
+
13
+ if (sub === 'help') {
14
+ process.stdout.write(`
15
+ DeerTools BaseLinker — MCP Proxy, Hub & Catalog Engine
16
+
17
+ Komendy:
18
+ deertools baselinker auth [token] Zapisuje token API BaseLinker (X-BLToken) w credentials.json.
19
+ deertools baselinker test Testuje połączenie z BaseLinker API i pobiera listę katalogów.
20
+ deertools baselinker serve [--port 3456] Uruchamia lokalny serwer Web UI z dashboardem i proxy MCP SSE.
21
+ deertools baselinker mcp [--all] Uruchamia serwer MCP stdio dla Cursor / Claude Desktop.
22
+ deertools baselinker sync-schema Pobiera i aktualizuje bazę wszystkich 204 metod z api.baselinker.com.
23
+ deertools baselinker search <fraza> Przeszukuje bazę 204 metod BaseLinkera i zwraca ich parametry.
24
+ deertools baselinker call <metoda> [json] Wywołuje dowolną metodę BaseLinkera bezpośrednio z terminala.
25
+ deertools baselinker catalog search <id> Wyszukuje produkty w katalogu BaseLinker.
26
+ deertools baselinker catalog get <inv> <p> Pobiera pełne dane produktu (w tym relacje, cechy, galerię).
27
+
28
+ Zmienne środowiskowe:
29
+ BASELINKER_TOKEN lub BL_TOKEN Klucz API BaseLinkera (ma pierwszeństwo przed plikiem).
30
+ `);
31
+ return;
32
+ }
33
+
34
+ if (sub === 'auth') {
35
+ let token = pos.shift() || flags.token;
36
+ if (!token && process.stdin.isTTY) {
37
+ const readline = require('readline');
38
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
39
+ token = await new Promise(res => rl.question('Podaj klucz API BaseLinker (X-BLToken): ', ans => {
40
+ rl.close();
41
+ res(ans.trim());
42
+ }));
43
+ }
44
+ if (!token) throw new Error('Nie podano tokenu. Użycie: deertools baselinker auth <X-BLToken>');
45
+
46
+ saveBaseLinkerToken(token);
47
+ process.stdout.write(`${c.green}✓ Zapisano token BaseLinker w credentials.json (uprawnienia 0600).${c.reset}\n`);
48
+
49
+ // Szybki test
50
+ process.stdout.write('Testowanie autoryzacji...\n');
51
+ const client = getClient(token);
52
+ const test = await client.testConnection();
53
+ if (test.ok) {
54
+ process.stdout.write(`${c.green}✓ Połączenie udane! Znaleziono ${test.inventories.length} katalog(ów) w BaseLinkerze.${c.reset}\n`);
55
+ } else {
56
+ process.stdout.write(`${c.red}✗ Uwaga: Serwer zwrócił błąd: ${test.error}${c.reset}\n`);
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (sub === 'test') {
62
+ const token = getBaseLinkerToken();
63
+ if (!token) {
64
+ throw new Error('Brak zapisanego tokenu BaseLinker. Wpisz: deertools baselinker auth <token>');
65
+ }
66
+ const client = getClient(token);
67
+ process.stdout.write('Testowanie połączenia z https://api.baselinker.com/connector.php...\n');
68
+ const res = await client.testConnection();
69
+ if (flags.json) {
70
+ process.stdout.write(JSON.stringify(res, null, 2) + '\n');
71
+ return;
72
+ }
73
+ if (res.ok) {
74
+ process.stdout.write(`${c.green}✓ Sukces: Połączenie aktywne.${c.reset}\n`);
75
+ process.stdout.write(`Znalezione katalogi (Inventories):\n`);
76
+ for (const inv of res.inventories) {
77
+ process.stdout.write(` - [ID ${inv.inventory_id}] ${inv.name} (język domyślny: ${inv.default_language})\n`);
78
+ }
79
+ } else {
80
+ process.stdout.write(`${c.red}✗ Błąd: ${res.error}${c.reset}\n`);
81
+ }
82
+ return;
83
+ }
84
+
85
+ if (sub === 'serve') {
86
+ const port = parseInt(flags.port || 3456, 10);
87
+ startBaseLinkerServer(port);
88
+ // Trzymaj proces przy życiu
89
+ return new Promise(() => {});
90
+ }
91
+
92
+ if (sub === 'mcp') {
93
+ runBaseLinkerMcpServer({ mode: flags.all ? 'all' : 'catalog' });
94
+ return;
95
+ }
96
+
97
+ if (sub === 'sync-schema') {
98
+ process.stdout.write('Pobieranie dokumentacji z https://api.baselinker.com/...\n');
99
+ await syncSchema((done, total, method) => {
100
+ process.stdout.write(`Pobrano metody: ${done}/${total} (${method})\r`);
101
+ });
102
+ process.stdout.write(`\n${c.green}✓ Pomyślnie zsynchronizowano bazę metod BaseLinkera!${c.reset}\n`);
103
+ return;
104
+ }
105
+
106
+ if (sub === 'search') {
107
+ const q = pos.join(' ');
108
+ const results = searchMethods(q);
109
+ if (flags.json) {
110
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
111
+ return;
112
+ }
113
+ process.stdout.write(`\nZnaleziono ${results.length} metod pasujących do „${q}”:\n\n`);
114
+ for (const r of results.slice(0, 15)) {
115
+ process.stdout.write(`${c.bold}${c.cyan}${r.name}${c.reset} [${r.category}]\n`);
116
+ process.stdout.write(` ${(r.description || '').slice(0, 100)}...\n`);
117
+ if (r.parameters && Object.keys(r.parameters).length > 0) {
118
+ process.stdout.write(` Parametry: ${Object.keys(r.parameters).join(', ')}\n`);
119
+ }
120
+ process.stdout.write('\n');
121
+ }
122
+ return;
123
+ }
124
+
125
+ if (sub === 'call') {
126
+ const method = pos.shift();
127
+ if (!method) throw new Error('Podaj nazwę metody: deertools baselinker call <metoda> [json]');
128
+ let params = {};
129
+ if (pos[0]) {
130
+ try { params = JSON.parse(pos.join(' ')); } catch (e) {
131
+ throw new Error('Nieprawidłowy JSON parametrów: ' + e.message);
132
+ }
133
+ }
134
+ const client = getClient();
135
+ const result = await client.call(method, params);
136
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
137
+ return;
138
+ }
139
+
140
+ if (sub === 'catalog') {
141
+ const catSub = pos.shift() || 'search';
142
+ if (catSub === 'search') {
143
+ const invId = parseInt(pos.shift() || flags.inventory || 0, 10);
144
+ const query = pos.join(' ') || flags.query || '';
145
+ if (!invId) throw new Error('Podaj inventory_id: deertools baselinker catalog search <inventory_id> [query]');
146
+ const res = await handleCatalogToolCall('bl_catalog_search_products', {
147
+ inventory_id: invId,
148
+ filter_name: query
149
+ });
150
+ if (flags.json) {
151
+ process.stdout.write(JSON.stringify(res, null, 2) + '\n');
152
+ return;
153
+ }
154
+ const products = res.products ? Object.values(res.products) : [];
155
+ process.stdout.write(`\nZnaleziono ${products.length} produktów w katalogu ID ${invId}:\n\n`);
156
+ for (const p of products.slice(0, 20)) {
157
+ process.stdout.write(`- [ID ${p.id || p.product_id}] SKU: ${p.sku || '-'} | EAN: ${p.ean || '-'} | ${p.name}\n`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ if (catSub === 'get') {
163
+ const invId = parseInt(pos.shift() || flags.inventory, 10);
164
+ const prodId = parseInt(pos.shift() || flags.product, 10);
165
+ if (!invId || !prodId) throw new Error('Użycie: deertools baselinker catalog get <inventory_id> <product_id>');
166
+ const product = await handleCatalogToolCall('bl_catalog_get_product', {
167
+ inventory_id: invId,
168
+ product_id: prodId,
169
+ include_relations: true,
170
+ include_channels_media: true
171
+ });
172
+ process.stdout.write(JSON.stringify(product, null, 2) + '\n');
173
+ return;
174
+ }
175
+ }
176
+
177
+ throw new Error(`Nieznana podkomenda baselinker: ${sub}. Wpisz "deertools baselinker help".`);
178
+ }
179
+
180
+ module.exports = { cmdBaseLinker };
package/src/cli.js CHANGED
@@ -23,13 +23,33 @@ function parseArgs(argv) {
23
23
 
24
24
  async function main(argv) {
25
25
  const { pos, flags } = parseArgs(argv);
26
- const cmd = pos.shift() || 'help';
26
+
27
+ if (flags.profile) {
28
+ const cred = require('./credentials');
29
+ cred.setProfile(String(flags.profile));
30
+ }
31
+
27
32
  const table = {
28
33
  login: c.cmdLogin, logout: c.cmdLogout, whoami: c.cmdWhoami, docs: c.cmdDocs,
29
34
  endpoints: c.cmdEndpoints, api: c.cmdApi, estimate: c.cmdEstimate, skill: c.cmdSkill,
35
+ mcp: c.cmdMcp, doctor: c.cmdDoctor, audit: c.cmdAudit, profile: c.cmdProfile,
36
+ completion: c.cmdCompletion, catalog: c.cmdCatalog, deliveries: c.cmdDeliveries,
37
+ supplier: c.cmdSupplier, rendermind: c.cmdRendermind, baselinker: c.cmdBaseLinker,
30
38
  };
39
+
40
+ if (pos.length === 0 && !flags.help && !flags.version && process.stdout.isTTY) {
41
+ const { runInteractiveMenu } = require('./tui');
42
+ await runInteractiveMenu(async (args, f) => {
43
+ const subCmd = args.shift();
44
+ if (table[subCmd]) await table[subCmd](args, f);
45
+ });
46
+ return 0;
47
+ }
48
+
49
+ const cmd = pos.shift() || 'help';
50
+
31
51
  if (cmd === 'help' || cmd === '--help' || flags.help) { process.stdout.write(c.HELP); return 0; }
32
- if (cmd === '--version' || cmd === 'version') { process.stdout.write(`${require('../package.json').version}\n`); return 0; }
52
+ if (cmd === '--version' || cmd === 'version' || flags.version) { process.stdout.write(`${require('../package.json').version}\n`); return 0; }
33
53
  if (!table[cmd]) { process.stderr.write(`Nieznana komenda „${cmd}”.\n${c.HELP}`); return 2; }
34
54
  try {
35
55
  await table[cmd](pos, flags);