@deerdesign/deertools 1.2.27 → 1.2.32
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 +55 -63
- package/package.json +4 -3
- package/src/baselinker-commands.js +21 -94
- package/src/cli.js +20 -6
- package/src/commands.js +167 -137
- package/src/completion.js +24 -18
- package/src/embedding.js +59 -0
- package/src/rendermind/cli-commands.js +31 -0
- package/src/rendermind/client.js +393 -0
- package/src/rendermind/mcp-server.js +305 -0
- package/src/skill-commands.js +157 -15
- package/src/skills-hub.js +66 -0
- package/src/supplier/cli-commands.js +32 -0
- package/src/supplier/mcp-server.js +360 -0
- package/src/tui.js +24 -39
package/src/skill-commands.js
CHANGED
|
@@ -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
|
-
|
|
274
|
-
const
|
|
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 (!
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|