@deerdesign/deertools 1.2.27 → 1.2.31

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.
@@ -6,6 +6,7 @@ const skillsHub = require('./skills-hub');
6
6
  const { parseFrontmatterName } = require('./openapi');
7
7
  const { resolveProducts } = require('./registry');
8
8
  const { fetchText } = require('./http');
9
+ const embedding = require('./embedding');
9
10
 
10
11
  const { compareSemver } = require('./updater');
11
12
 
@@ -269,22 +270,142 @@ function inspectSkillForAgents(agents, skill, lock) {
269
270
  };
270
271
  }
271
272
 
273
+ /**
274
+ * Sprawdza czy dany skill pasuje do zapytania tekstowego.
275
+ * Przeszukuje kompleksowo:
276
+ * 1. Nazwę (slug, full_slug, title)
277
+ * 2. Opis (summary / description - pełny tekst opisu z SKILL.md)
278
+ * 3. Tagi (zarówno całe tagi, jak i dopasowania cząstkowe, np. 'matrycowy' w 'cennik-matrycowy')
279
+ * 4. Kategorie (nazwa kategorii oraz jej slug)
280
+ * 5. Namespace oraz autora (owner_name, owner_email)
281
+ */
282
+ function matchSkillQuery(skill, query) {
283
+ if (!query) return true;
284
+ const q = String(query).toLowerCase().trim();
285
+ if (!q) return true;
286
+
287
+ // 1. Nazwa, slug, full_slug, title
288
+ if (skill.slug && String(skill.slug).toLowerCase().includes(q)) return true;
289
+ if (skill.full_slug && String(skill.full_slug).toLowerCase().includes(q)) return true;
290
+ if (skill.title && String(skill.title).toLowerCase().includes(q)) return true;
291
+
292
+ // 2. Opis (summary / description)
293
+ if (skill.summary && String(skill.summary).toLowerCase().includes(q)) return true;
294
+ if (skill.description && String(skill.description).toLowerCase().includes(q)) return true;
295
+
296
+ // 3. Tagi (zarówno dopasowanie pełne, jak i podciąg wewnątrz tagu)
297
+ if (Array.isArray(skill.tags)) {
298
+ for (const tag of skill.tags) {
299
+ if (String(tag).toLowerCase().includes(q)) return true;
300
+ }
301
+ }
302
+
303
+ // 4. Kategoria
304
+ if (skill.categories) {
305
+ if (skill.categories.name && String(skill.categories.name).toLowerCase().includes(q)) return true;
306
+ if (skill.categories.slug && String(skill.categories.slug).toLowerCase().includes(q)) return true;
307
+ }
308
+ if (skill.category && typeof skill.category === 'string' && skill.category.toLowerCase().includes(q)) return true;
309
+
310
+ // 5. Namespace i właściciel
311
+ if (skill.namespace && String(skill.namespace).toLowerCase().includes(q)) return true;
312
+ if (skill.owner_name && String(skill.owner_name).toLowerCase().includes(q)) return true;
313
+ if (skill.owner_email && String(skill.owner_email).toLowerCase().includes(q)) return true;
314
+
315
+ return false;
316
+ }
317
+
272
318
  async function cmdSkillList(flags, out, err) {
273
- if (!flags.json) out('Pobieram listę skilli z DeerSkills Hub...');
274
- const res = await skillsHub.listSkills({
275
- query: flags.query,
276
- tag: flags.tag,
277
- category: flags.category,
278
- scope: flags.scope,
279
- limit: flags.limit || 100,
280
- });
319
+ const searchQuery = flags.query ? String(flags.query).trim() : '';
320
+ const isSearch = Boolean(searchQuery);
281
321
 
282
- if (!res.ok) {
283
- err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
284
- return;
322
+ if (!flags.json) {
323
+ if (isSearch) {
324
+ out(`Wyszukuję skille w DeerSkills Hub dla frazy „${searchQuery}” (hybrydowo: wektory + tekst)...`);
325
+ } else {
326
+ out('Pobieram listę skilli z DeerSkills Hub...');
327
+ }
328
+ }
329
+
330
+ let items = [];
331
+
332
+ if (isSearch) {
333
+ // 1. Serwerowe wyszukiwanie wektorowo-hybrydowe (PostgreSQL pgvector + trigram + keyword boost)
334
+ let queryEmbedding = null;
335
+ if (flags.semantic) {
336
+ if (!flags.json) out('Generuję wektor semantyczny zapytania...');
337
+ queryEmbedding = embedding.computeEmbedding(searchQuery);
338
+ }
339
+
340
+ const hybridRes = await skillsHub.searchSkillsHybrid({
341
+ queryText: searchQuery,
342
+ queryEmbedding,
343
+ threshold: queryEmbedding ? 0.1 : 0.05,
344
+ count: flags.limit || 50,
345
+ });
346
+
347
+ if (hybridRes.ok && Array.isArray(hybridRes.data)) {
348
+ items = hybridRes.data;
349
+
350
+ // 2. Jeśli zapytanie tekstowe dało 0 wyników, a dostępne jest lokalne generowanie wektorów — ponów semantycznie
351
+ if (items.length === 0 && !flags.semantic && embedding.isEmbeddingAvailable()) {
352
+ const autoEmb = embedding.computeEmbedding(searchQuery);
353
+ if (autoEmb) {
354
+ const semRes = await skillsHub.searchSkillsHybrid({
355
+ queryText: searchQuery,
356
+ queryEmbedding: autoEmb,
357
+ threshold: 0.12,
358
+ count: flags.limit || 50,
359
+ });
360
+ if (semRes.ok && Array.isArray(semRes.data) && semRes.data.length > 0) {
361
+ items = semRes.data;
362
+ }
363
+ }
364
+ }
365
+
366
+ if (flags.tag) {
367
+ items = items.filter((s) => Array.isArray(s.tags) && s.tags.includes(flags.tag));
368
+ }
369
+ if (flags.category) {
370
+ const cat = String(flags.category).toLowerCase();
371
+ items = items.filter((s) => s.category_slug === cat || (s.category_name && s.category_name.toLowerCase().includes(cat)));
372
+ }
373
+ if (flags.scope) {
374
+ items = items.filter((s) => s.scope === flags.scope);
375
+ }
376
+ } else {
377
+ // Fallback do pobierania pełnej listy i lokalnego przeszukiwania
378
+ const res = await skillsHub.listSkills({
379
+ tag: flags.tag,
380
+ category: flags.category,
381
+ scope: flags.scope,
382
+ limit: flags.limit || 200,
383
+ });
384
+
385
+ if (!res.ok) {
386
+ err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
387
+ return;
388
+ }
389
+
390
+ const allItems = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
391
+ items = allItems.filter((s) => matchSkillQuery(s, searchQuery));
392
+ }
393
+ } else {
394
+ const res = await skillsHub.listSkills({
395
+ tag: flags.tag,
396
+ category: flags.category,
397
+ scope: flags.scope,
398
+ limit: flags.limit || 200,
399
+ });
400
+
401
+ if (!res.ok) {
402
+ err(`✗ Błąd pobierania skilli (${res.status}): ${res.data?.error || 'Brak dostępu'}`);
403
+ return;
404
+ }
405
+
406
+ items = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
285
407
  }
286
408
 
287
- const items = Array.isArray(res.data) ? res.data : (res.data?.skills || []);
288
409
  const agents = detectAgents({
289
410
  dir: flags.dir,
290
411
  agent: flags.agent,
@@ -297,6 +418,9 @@ async function cmdSkillList(flags, out, err) {
297
418
  const insp = inspectSkillForAgents(agents, s, lock);
298
419
  return {
299
420
  ...s,
421
+ similarity: s.similarity != null ? s.similarity : null,
422
+ vector_score: s.vector_score != null ? s.vector_score : null,
423
+ text_score: s.text_score != null ? s.text_score : null,
300
424
  overall_status: insp.overallStatus,
301
425
  agents: Object.fromEntries(
302
426
  Object.entries(insp.agents).map(([id, data]) => [
@@ -328,9 +452,22 @@ async function cmdSkillList(flags, out, err) {
328
452
  };
329
453
  }
330
454
 
331
- out(`\n=== Dostępne Skille w DeerSkills Hub (${items.length}) ===\n`);
455
+ const titleHeader = isSearch
456
+ ? `=== Wyniki wyszukiwania w DeerSkills Hub dla „${searchQuery}” (${items.length}) ===`
457
+ : `=== Dostępne Skille w DeerSkills Hub (${items.length}) ===`;
458
+ out(`\n${titleHeader}\n`);
459
+
460
+ if (items.length === 0 && isSearch) {
461
+ out(` Nie znaleziono żadnego skilla pasującego do frazy „${searchQuery}”.`);
462
+ out(` (Przeszukano: wektory semantyczne, nazwy/slugi, opisy [summary], tagi oraz kategorie).`);
463
+ out(`\n💡 Wskazówka: Uruchom \x1b[36mdeertools skill list\x1b[0m aby zobaczyć wszystkie dostępne skille.`);
464
+ out('─'.repeat(78) + '\n');
465
+ return;
466
+ }
467
+
332
468
  for (const s of items) {
333
- const badge = s.scope === 'global' ? '★ GLOBAL' : `@${s.namespace}`;
469
+ const namespace = s.namespace || (s.full_slug?.startsWith('@') ? s.full_slug.split('/')[0].slice(1) : '');
470
+ const badge = s.scope === 'global' ? '★ GLOBAL' : (namespace ? `@${namespace}` : (s.scope === 'team' ? '[Team]' : '[User]'));
334
471
  const latestVer = s.latest_version ? `v${s.latest_version}` : 'v1.0.0';
335
472
  const insp = inspectSkillForAgents(agents, s, lock);
336
473
 
@@ -342,8 +479,12 @@ async function cmdSkillList(flags, out, err) {
342
479
  }
343
480
 
344
481
  const agentBadges = agents.map((a) => insp.agents[a.id].badge).join(' ');
482
+ const simBadge = (s.similarity != null && isSearch)
483
+ ? `\x1b[36m[Trafność: ${Math.round(s.similarity * 100)}%]\x1b[0m`
484
+ : '';
345
485
 
346
- out(` • ${s.full_slug.padEnd(36)} ${latestVer.padEnd(10)} ${badge.padEnd(14)} ${insp.overallBadge}`);
486
+ const fullSlug = s.full_slug || s.slug || '';
487
+ out(` • ${fullSlug.padEnd(36)} ${latestVer.padEnd(10)} ${badge.padEnd(14)} ${insp.overallBadge}${simBadge ? ' ' + simBadge : ''}`);
347
488
  out(` Agenci: ${agentBadges}`);
348
489
  if (s.summary) out(` ${s.summary.slice(0, 95)}...`);
349
490
  if (s.tags && s.tags.length) out(` Tagi: ${s.tags.join(', ')}`);
@@ -1239,4 +1380,5 @@ module.exports = {
1239
1380
  updateSkills,
1240
1381
  scanLocalSkills,
1241
1382
  inspectSkillForAgents,
1383
+ matchSkillQuery,
1242
1384
  };
package/src/skills-hub.js CHANGED
@@ -8,6 +8,8 @@ const { parseFrontmatterName } = require('./openapi');
8
8
  const { compareSemver } = require('./updater');
9
9
 
10
10
  const DEFAULT_API_BASE = 'https://ezspmlsnmxuhtzvwrnoj.supabase.co/functions/v1/skills-api';
11
+ const DEFAULT_SUPABASE_URL = 'https://ezspmlsnmxuhtzvwrnoj.supabase.co';
12
+ const DEFAULT_ANON_KEY = 'sb_publishable_y8iokK-V2X6A1HZb94g_pw_Le0bm3f2';
11
13
  const SECRET_PATTERNS = [
12
14
  { name: 'DeerTools Live API Key', regex: /deer_[a-z]+_live_[A-Za-z0-9_]{20,}/g },
13
15
  { name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9_]{30,}/g },
@@ -87,6 +89,67 @@ async function listSkills(params = {}) {
87
89
  return hubRequest(`/skills${qs ? `?${qs}` : ''}`);
88
90
  }
89
91
 
92
+ function getSupabaseUrl() {
93
+ return process.env.DEERTOOLS_SUPABASE_URL || DEFAULT_SUPABASE_URL;
94
+ }
95
+
96
+ function getSupabaseAnonKey() {
97
+ return process.env.DEERTOOLS_SUPABASE_KEY || DEFAULT_ANON_KEY;
98
+ }
99
+
100
+ /**
101
+ * Serwerowe wyszukiwanie wektorowo-hybrydowe (PostgreSQL pgvector + trigram + keyword boost).
102
+ */
103
+ async function searchSkillsHybrid({ queryText = '', queryEmbedding = null, threshold = 0.05, count = 50 } = {}) {
104
+ const base = getSupabaseUrl().replace(/\/+$/, '');
105
+ const url = `${base}/rest/v1/rpc/match_skills_hybrid`;
106
+ const anonKey = getSupabaseAnonKey();
107
+ const userKey = getApiKey();
108
+
109
+ const body = {
110
+ query_text: String(queryText || ''),
111
+ query_embedding: queryEmbedding || null,
112
+ match_threshold: threshold,
113
+ match_count: count,
114
+ };
115
+ if (userKey) {
116
+ body.caller_api_key = userKey;
117
+ }
118
+
119
+ const headers = {
120
+ 'Content-Type': 'application/json',
121
+ 'apikey': anonKey,
122
+ 'Authorization': `Bearer ${anonKey}`,
123
+ 'User-Agent': 'deertools-cli',
124
+ };
125
+
126
+ try {
127
+ const res = await fetch(url, {
128
+ method: 'POST',
129
+ headers,
130
+ body: JSON.stringify(body),
131
+ });
132
+ let data = null;
133
+ try {
134
+ data = await res.json();
135
+ } catch (_) {
136
+ data = null;
137
+ }
138
+ return {
139
+ ok: res.ok,
140
+ status: res.status,
141
+ data,
142
+ };
143
+ } catch (err) {
144
+ return {
145
+ ok: false,
146
+ status: 500,
147
+ data: null,
148
+ error: err.message,
149
+ };
150
+ }
151
+ }
152
+
90
153
  async function getSkill(slug, version = null) {
91
154
  const ver = typeof version === 'object' && version !== null ? version.version : version;
92
155
  const qs = ver ? `?version=${encodeURIComponent(ver)}` : '';
@@ -472,8 +535,11 @@ async function submitReview(slug) {
472
535
  module.exports = {
473
536
  getApiBase,
474
537
  getApiKey,
538
+ getSupabaseUrl,
539
+ getSupabaseAnonKey,
475
540
  scanForSecrets,
476
541
  listSkills,
542
+ searchSkillsHybrid,
477
543
  getSkill,
478
544
  pushSkill,
479
545
  bumpVersion,
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const { runSupplierMcpServer } = require('./mcp-server');
3
+
4
+ function out(msg) { process.stdout.write(msg + '\n'); }
5
+
6
+ async function cmdSupplier(pos) {
7
+ const sub = pos[0];
8
+
9
+ if (sub === 'mcp') {
10
+ return runSupplierMcpServer();
11
+ }
12
+
13
+ out(`Użycie: deertools supplier mcp
14
+
15
+ Zgodnie ze standardem architektonicznym DeerTools CLI:
16
+ - Operacje biznesowe na rabatach, narzutach i dostawcach wykonuje Agent AI wyłącznie przez serwer MCP (deer-supplier).
17
+ - Dostępne narzędzia MCP:
18
+ * supplier_list_suppliers
19
+ * supplier_get_discounts
20
+ * supplier_get_markups
21
+ * supplier_get_sales_discounts
22
+ * supplier_evaluate_batch
23
+ * supplier_list_rules
24
+ * supplier_list_taxonomy
25
+ * supplier_list_configurations
26
+
27
+ Aby zainstalować serwer MCP w Cursor / Antigravity, uruchom:
28
+ deertools mcp install
29
+ `);
30
+ }
31
+
32
+ module.exports = { cmdSupplier };
@@ -0,0 +1,360 @@
1
+ 'use strict';
2
+ const readline = require('readline');
3
+ const credentials = require('../credentials');
4
+ const { getProduct } = require('../registry');
5
+
6
+ const PROTOCOL_VERSION = '2024-11-05';
7
+ const SERVER_INFO = { name: 'deer-supplier-mcp', version: '1.0.0' };
8
+ const SUPPLIER_API_BASE = 'https://hubnxvzxdgocbtmkfdsz.supabase.co/functions/v1/public-api';
9
+
10
+ function getSupplierKey() {
11
+ const p = getProduct('supplier');
12
+ const cred = credentials.getKey(p);
13
+ if (cred && cred.key) return cred.key;
14
+ if (process.env.SUPPLIER_API_KEY) return process.env.SUPPLIER_API_KEY;
15
+ if (process.env.DEERTOOLS_SUPPLIER_KEY) return process.env.DEERTOOLS_SUPPLIER_KEY;
16
+ return null;
17
+ }
18
+
19
+ function ensureSupplierAuth() {
20
+ const key = getSupplierKey();
21
+ if (!key) {
22
+ throw new Error('Brak klucza API do Supplier Hub. Zaloguj się poleceniem: deertools login supplier');
23
+ }
24
+ return key;
25
+ }
26
+
27
+ async function callSupplierApi(method, path, { query = {}, body = undefined } = {}) {
28
+ const apiKey = ensureSupplierAuth();
29
+ const url = new URL(SUPPLIER_API_BASE + path);
30
+ for (const [k, v] of Object.entries(query)) {
31
+ if (v !== undefined && v !== null && v !== '') {
32
+ url.searchParams.set(k, String(v));
33
+ }
34
+ }
35
+ const headers = {
36
+ 'x-api-key': apiKey,
37
+ 'Accept': 'application/json'
38
+ };
39
+ let payload;
40
+ if (body !== undefined) {
41
+ headers['Content-Type'] = 'application/json';
42
+ payload = JSON.stringify(body);
43
+ }
44
+ const res = await fetch(url.toString(), { method, headers, body: payload });
45
+ const text = await res.text();
46
+ let json = null;
47
+ try { json = JSON.parse(text); } catch (_) {}
48
+ if (!res.ok) {
49
+ throw new Error(`Supplier Hub API (${res.status}): ${(json && json.error) || text}`);
50
+ }
51
+ return json !== null ? json : text;
52
+ }
53
+
54
+ const SUPPLIER_TOOLS = [
55
+ {
56
+ name: 'supplier_list_suppliers',
57
+ description: 'Wyszukuje dostawców dla producenta i lokalizacji (kod pocztowy, miasto lub województwo) w DeerTools Supplier Hub.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ manufacturer: {
62
+ type: 'string',
63
+ description: 'Nazwa, external_id lub UUID producenta (np. "Flügger", "LAUFEN", "Roca").'
64
+ },
65
+ postal_code: {
66
+ type: 'string',
67
+ description: 'Kod pocztowy w formacie XX-XXX (np. "00-001", "30-001").'
68
+ },
69
+ city: {
70
+ type: 'string',
71
+ description: 'Nazwa miasta/gminy (np. "Warszawa", "Kraków").'
72
+ },
73
+ voivodeship: {
74
+ type: 'string',
75
+ description: 'Nazwa województwa.'
76
+ },
77
+ pick: {
78
+ type: 'string',
79
+ enum: ['random', 'first', 'all'],
80
+ default: 'all',
81
+ description: 'Sposób zwracania dopasowanych dostawców (domyślnie all).'
82
+ }
83
+ },
84
+ required: ['manufacturer']
85
+ }
86
+ },
87
+ {
88
+ name: 'supplier_get_discounts',
89
+ description: 'Dopasowuje aktywne rabaty zakupowe w Supplier Hub dla producenta, kolekcji, kategorii lub produktu.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ manufacturer: { type: 'string', description: 'Nazwa, external_id lub UUID producenta.' },
94
+ supplier: { type: 'string', description: 'Opcjonalna nazwa lub UUID dostawcy.' },
95
+ collection: { type: 'string', description: 'Opcjonalna nazwa kolekcji.' },
96
+ category: { type: 'string', description: 'Opcjonalna nazwa kategorii.' },
97
+ tag: { type: 'string', description: 'Opcjonalny tag produktu.' },
98
+ product_id: { type: 'string', description: 'Opcjonalny identyfikator lub SKU produktu.' },
99
+ best_only: { type: 'boolean', default: false, description: 'Czy zwrócić wyłącznie najwyżej dopasowany rabat.' }
100
+ },
101
+ required: ['manufacturer']
102
+ }
103
+ },
104
+ {
105
+ name: 'supplier_get_markups',
106
+ description: 'Dopasowuje narzuty cenowe w Supplier Hub dla producenta, kolekcji, kategorii lub produktu.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ manufacturer: { type: 'string', description: 'Nazwa, external_id lub UUID producenta.' },
111
+ supplier: { type: 'string', description: 'Opcjonalna nazwa lub UUID dostawcy.' },
112
+ collection: { type: 'string', description: 'Opcjonalna nazwa kolekcji.' },
113
+ category: { type: 'string', description: 'Opcjonalna nazwa kategorii.' },
114
+ tag: { type: 'string', description: 'Opcjonalny tag produktu.' },
115
+ product_id: { type: 'string', description: 'Opcjonalny identyfikator lub SKU produktu.' },
116
+ best_only: { type: 'boolean', default: false, description: 'Czy zwrócić wyłącznie najlepiej dopasowany narzut.' }
117
+ },
118
+ required: ['manufacturer']
119
+ }
120
+ },
121
+ {
122
+ name: 'supplier_get_sales_discounts',
123
+ description: 'Dopasowuje aktywne promocje sprzedaży w Supplier Hub dla producenta, kolekcji lub produktu.',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ manufacturer: { type: 'string', description: 'Nazwa, external_id lub UUID producenta.' },
128
+ supplier: { type: 'string', description: 'Opcjonalna nazwa lub UUID dostawcy.' },
129
+ collection: { type: 'string', description: 'Opcjonalna nazwa kolekcji.' },
130
+ category: { type: 'string', description: 'Opcjonalna nazwa kategorii.' },
131
+ tag: { type: 'string', description: 'Opcjonalny tag produktu.' },
132
+ product_id: { type: 'string', description: 'Opcjonalny identyfikator lub SKU produktu.' },
133
+ best_only: { type: 'boolean', default: false, description: 'Czy zwrócić wyłącznie najlepiej dopasowaną promocję.' }
134
+ },
135
+ required: ['manufacturer']
136
+ }
137
+ },
138
+ {
139
+ name: 'supplier_evaluate_batch',
140
+ description: 'Wsadowe dopasowanie rabatów zakupowych, narzutów lub promocji sprzedaży dla maksymalnie 500 produktów jednego producenta.',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ type: {
145
+ type: 'string',
146
+ enum: ['discounts', 'markups', 'sales-discounts'],
147
+ default: 'discounts',
148
+ description: 'Typ wyliczanych reguł: discounts (rabaty zakupowe), markups (narzuty), sales-discounts (promocje).'
149
+ },
150
+ manufacturer: { type: 'string', description: 'Nazwa, external_id lub UUID producenta.' },
151
+ best_only: { type: 'boolean', default: true, description: 'Czy zwracać tylko najlepsze dopasowanie per pozycja.' },
152
+ items: {
153
+ type: 'array',
154
+ description: 'Lista pozycji do przeliczenia (każda z unikalnym kluczem key).',
155
+ items: {
156
+ type: 'object',
157
+ properties: {
158
+ key: { type: 'string' },
159
+ product_id: { type: 'string' },
160
+ collection: { type: 'string' },
161
+ category: { type: 'string' },
162
+ tag: { type: 'string' }
163
+ },
164
+ required: ['key']
165
+ }
166
+ }
167
+ },
168
+ required: ['manufacturer', 'items']
169
+ }
170
+ },
171
+ {
172
+ name: 'supplier_list_rules',
173
+ description: 'Pobiera listę reguł cenowych (discounts, sales-discounts lub markups) wraz z warunkami.',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ type: {
178
+ type: 'string',
179
+ enum: ['discounts', 'markups', 'sales-discounts'],
180
+ default: 'discounts',
181
+ description: 'Typ reguł cenowych.'
182
+ },
183
+ limit: { type: 'number', default: 50, description: 'Limit wyników (domyślnie 50).' },
184
+ include_conditions: { type: 'boolean', default: true, description: 'Czy dołączyć warunki reguł.' }
185
+ }
186
+ }
187
+ },
188
+ {
189
+ name: 'supplier_list_taxonomy',
190
+ description: 'Wyszukuje pozycje taksonomii w Supplier Hub (collections, categories lub tags).',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ type: {
195
+ type: 'string',
196
+ enum: ['collections', 'categories', 'tags'],
197
+ default: 'collections',
198
+ description: 'Typ taksonomii.'
199
+ },
200
+ search: { type: 'string', description: 'Fraza wyszukiwania.' },
201
+ limit: { type: 'number', default: 50, description: 'Limit wyników.' }
202
+ }
203
+ }
204
+ },
205
+ {
206
+ name: 'supplier_list_configurations',
207
+ description: 'Pobiera konfiguracje powiązań dostawca–producent z Supplier Hub.',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {}
211
+ }
212
+ }
213
+ ];
214
+
215
+ async function handleMcpMessage(msg) {
216
+ if (!msg || typeof msg !== 'object') return null;
217
+ const { id, method, params } = msg;
218
+
219
+ if (method === 'initialize') {
220
+ return {
221
+ jsonrpc: '2.0',
222
+ id,
223
+ result: {
224
+ protocolVersion: PROTOCOL_VERSION,
225
+ capabilities: { tools: {} },
226
+ serverInfo: SERVER_INFO
227
+ }
228
+ };
229
+ }
230
+
231
+ if (method === 'notifications/initialized' || method === 'initialized') {
232
+ return null;
233
+ }
234
+
235
+ if (method === 'ping') {
236
+ return { jsonrpc: '2.0', id, result: {} };
237
+ }
238
+
239
+ if (method === 'tools/list') {
240
+ return {
241
+ jsonrpc: '2.0',
242
+ id,
243
+ result: { tools: SUPPLIER_TOOLS }
244
+ };
245
+ }
246
+
247
+ if (method === 'tools/call') {
248
+ const toolName = params?.name;
249
+ const args = params?.arguments || {};
250
+
251
+ try {
252
+ let data;
253
+ if (toolName === 'supplier_list_suppliers') {
254
+ data = await callSupplierApi('GET', '/suppliers', {
255
+ query: {
256
+ manufacturer: args.manufacturer,
257
+ postal_code: args.postal_code,
258
+ city: args.city,
259
+ voivodeship: args.voivodeship,
260
+ pick: args.pick || 'all'
261
+ }
262
+ });
263
+ } else if (toolName === 'supplier_get_discounts') {
264
+ data = await callSupplierApi('GET', '/discounts', { query: args });
265
+ } else if (toolName === 'supplier_get_markups') {
266
+ data = await callSupplierApi('GET', '/markups', { query: args });
267
+ } else if (toolName === 'supplier_get_sales_discounts') {
268
+ data = await callSupplierApi('GET', '/sales-discounts', { query: args });
269
+ } else if (toolName === 'supplier_evaluate_batch') {
270
+ const type = args.type || 'discounts';
271
+ data = await callSupplierApi('POST', `/${type}/evaluate`, {
272
+ body: {
273
+ manufacturer: args.manufacturer,
274
+ best_only: args.best_only !== false,
275
+ items: args.items
276
+ }
277
+ });
278
+ } else if (toolName === 'supplier_list_rules') {
279
+ const type = args.type || 'discounts';
280
+ data = await callSupplierApi('GET', `/rules/${type}`, {
281
+ query: {
282
+ limit: args.limit || 50,
283
+ include_conditions: args.include_conditions !== false
284
+ }
285
+ });
286
+ } else if (toolName === 'supplier_list_taxonomy') {
287
+ const type = args.type || 'collections';
288
+ data = await callSupplierApi('GET', `/taxonomy/${type}`, {
289
+ query: { search: args.search, limit: args.limit || 50 }
290
+ });
291
+ } else if (toolName === 'supplier_list_configurations') {
292
+ data = await callSupplierApi('GET', '/supplier-configurations');
293
+ } else {
294
+ return {
295
+ jsonrpc: '2.0',
296
+ id,
297
+ error: { code: -32601, message: `Nieznane narzędzie: ${toolName}` }
298
+ };
299
+ }
300
+
301
+ return {
302
+ jsonrpc: '2.0',
303
+ id,
304
+ result: {
305
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
306
+ }
307
+ };
308
+ } catch (e) {
309
+ return {
310
+ jsonrpc: '2.0',
311
+ id,
312
+ error: { code: -32000, message: e.message }
313
+ };
314
+ }
315
+ }
316
+
317
+ return {
318
+ jsonrpc: '2.0',
319
+ id,
320
+ error: { code: -32601, message: `Nieobsługiwana metoda: ${method}` }
321
+ };
322
+ }
323
+
324
+ function runSupplierMcpServer() {
325
+ return new Promise((resolve) => {
326
+ const rl = readline.createInterface({
327
+ input: process.stdin,
328
+ output: process.stdout,
329
+ terminal: false
330
+ });
331
+
332
+ rl.on('line', async (line) => {
333
+ const trimmed = line.trim();
334
+ if (!trimmed) return;
335
+ try {
336
+ const msg = JSON.parse(trimmed);
337
+ const resp = await handleMcpMessage(msg);
338
+ if (resp) {
339
+ process.stdout.write(JSON.stringify(resp) + '\n');
340
+ }
341
+ } catch (e) {
342
+ process.stdout.write(JSON.stringify({
343
+ jsonrpc: '2.0',
344
+ id: null,
345
+ error: { code: -32700, message: `Parse error: ${e.message}` }
346
+ }) + '\n');
347
+ }
348
+ });
349
+
350
+ rl.on('close', () => resolve());
351
+ process.on('SIGINT', () => { rl.close(); resolve(); });
352
+ process.on('SIGTERM', () => { rl.close(); resolve(); });
353
+ });
354
+ }
355
+
356
+ module.exports = {
357
+ SUPPLIER_TOOLS,
358
+ handleMcpMessage,
359
+ runSupplierMcpServer
360
+ };