@deerdesign/deertools 1.2.36 → 1.3.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,592 @@
1
+ 'use strict';
2
+ const readline = require('readline');
3
+ const client = require('./client');
4
+
5
+ const PROTOCOL_VERSION = '2024-11-05';
6
+ const SERVER_INFO = { name: 'deer-estimate-mcp', version: '3.0.0' };
7
+
8
+ const ESTIMATE_TOOLS = [
9
+ {
10
+ name: 'estimate_list_projects',
11
+ description: 'Pobiera listę projektów Deer Design z backendu dev.deergroup.pl/api/projects z opcją wyszukiwania po numerze/nazwie, filtrowania i stronicowania.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ search: { type: 'string', description: 'Fraza wyszukiwania (np. numer umowy "3407", "4622", nazwa "Ursus", ulica).' },
16
+ page: { type: 'number', description: 'Numer strony (1..N). Domyślnie 1.' },
17
+ pageSize: { type: 'number', description: 'Rozmiar strony (1..100). Domyślnie 20.' },
18
+ projectType: { type: 'string', description: 'Typ projektu (np. "MIESZKANIE", "DOM", "KOMERCYJNY").' },
19
+ propertyStatus: { type: 'string', description: 'Status nieruchomości.' },
20
+ sort: { type: 'string', description: 'Klucz sortowania (np. "createdOn:desc", "id:desc").' }
21
+ }
22
+ }
23
+ },
24
+ {
25
+ name: 'estimate_get_project',
26
+ description: 'Pobiera szczegółowe dane projektu z dev.deergroup.pl/api/projects/:id wraz z powiązanymi dokumentami i kosztorysami.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ required: ['projectId'],
30
+ properties: {
31
+ projectId: { type: 'string', description: 'Identyfikator projektu (np. "707").' }
32
+ }
33
+ }
34
+ },
35
+ {
36
+ name: 'estimate_list_documents',
37
+ description: 'Pobiera listę dokumentów kosztorysowych dla wskazanego projektu z dev.deergroup.pl/api/documents.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ projectId: { type: 'string', description: 'ID projektu do filtrowania dokumentów (np. "707").' },
42
+ search: { type: 'string', description: 'Szukanie po nazwie lub numerze dokumentu.' },
43
+ page: { type: 'number', description: 'Numer strony (domyślnie 1).' },
44
+ pageSize: { type: 'number', description: 'Liczba pozycji (domyślnie 20).' },
45
+ sort: { type: 'string', description: 'Sortowanie (domyślnie "addedAt:desc").' }
46
+ }
47
+ }
48
+ },
49
+ {
50
+ name: 'estimate_get_document',
51
+ description: 'Pobiera pełny dokument kosztorysu z dev.deergroup.pl/api/documents/:id (lub skrócone podsumowanie summaryOnly=true), w tym kompletną tablicę pozycji (estimateItems), obmiary, pakiet i metadane.',
52
+ inputSchema: {
53
+ type: 'object',
54
+ required: ['documentId'],
55
+ properties: {
56
+ documentId: { type: 'string', description: 'Identyfikator dokumentu kosztorysu (np. "1401").' },
57
+ summaryOnly: { type: 'boolean', description: 'Gdy true, zwraca zwięzłe podsumowanie pakietu, pokoi, redukcji, niewypełnionych slotów i kwot bez pełnego zrzutu JSON.' }
58
+ }
59
+ }
60
+ },
61
+ {
62
+ name: 'estimate_create_document',
63
+ description: 'Tworzy od podstaw nowy dokument kosztorysu (POST /documents) dla wybranego projektu na dev.deergroup.pl/api, inicjalizując obmiary pomieszczeń, wybrany pakiet (SMART / MODERN / HIGH), pełny wzorzec (templatesSnapshot) oraz generując wszystkie pozycje pakietowe.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ required: ['projectId'],
67
+ properties: {
68
+ projectId: { type: 'string', description: 'ID projektu (np. "707").' },
69
+ name: { type: 'string', description: 'Nazwa kosztorysu (opcjonalna — domyślnie generowana z nazwy pakietu i projektu).' },
70
+ package: { type: 'string', enum: ['SMART', 'MODERN', 'HIGH', 'smart', 'modern', 'high'], description: 'Pakiet wykończeniowy (domyślnie SMART).' },
71
+ rooms: {
72
+ type: 'array',
73
+ description: 'Lista pomieszczeń z obmiarami (id, name, area, perimeter, height, kitchenAreaShare, openings). Jeśli pominięta, tworzony jest domyślny układ.',
74
+ items: {
75
+ type: 'object',
76
+ properties: {
77
+ name: { type: 'string', description: 'Nazwa pomieszczenia (np. "Hall", "Salon+kuchnia", "Sypialnia 1", "Łazienka").' },
78
+ area: { type: 'number', description: 'Powierzchnia podłogi w m².' },
79
+ perimeter: { type: 'number', description: 'Obwód pomieszczenia w mb.' },
80
+ height: { type: 'number', description: 'Wysokość pomieszczenia w m (domyślnie 2.7).' },
81
+ kitchenAreaShare: { type: 'number', description: 'Udział powierzchni aneksu kuchennego w m² (dla "Salon+kuchnia").' },
82
+ openings: { type: 'array', description: 'Lista otworów drzwiowych/okiennych ({ kind: "door"|"external_door", width: 0.9, count: 1 }).', items: { type: 'object' } }
83
+ },
84
+ required: ['name', 'area']
85
+ }
86
+ },
87
+ variantAnswers: { type: 'object', description: 'Opcjonalne odpowiedzi na pytania ankietowe wariantów.' }
88
+ }
89
+ }
90
+ },
91
+ {
92
+ name: 'estimate_search_catalog',
93
+ description: 'Wyszukuje produkty i usługi w oficjalnym Katalogu Kosztorysu (Catalog 5 na dev.deergroup.pl/api/catalogs/5/items). Obsługuje tryb zablokowanych filtrów pakietowych/slotowych (gdy podano documentId + slotRowId lub filtry pakietu i limitu ceny) oraz tryb odblokowany (unlockFilters: true) z automatycznym wyliczeniem flag inPackageScope, withinPriceLimit, requiresReductionAndReplacement oraz powiązanych usług montażowych (relatedServices).',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ search: { type: 'string', description: 'Fraza wyszukiwania (nazwa produktu/usługi, SKU, EAN).' },
98
+ type: { type: 'string', enum: ['product', 'service', 'bundle'], description: 'Typ pozycji (product lub service).' },
99
+ categoryPathPrefix: { type: 'string', description: 'Ścieżka kategorii w Katalogu 5 (np. "Produkty|Wykończenie wnętrz|Płytki ceramiczne i gresy|Płytki podłogowe|Podłogowe — format duży").' },
100
+ packageFilter: { type: 'string', enum: ['smart', 'modern', 'high', 'all'], description: 'Filtr pakietu (smart, modern, high lub all).' },
101
+ priceMin: { type: 'number', description: 'Minimalna cena zakupu netto za jednostkę kosztorysową (purchasePricePerPricingUnitFrom).' },
102
+ priceMax: { type: 'number', description: 'Maksymalny limit ceny zakupu netto za jednostkę kosztorysową (purchasePricePerPricingUnitTo).' },
103
+ unit: { type: 'string', description: 'Jednostka kosztorysowa do porównania limitu ceny (np. "m2", "mb", "szt").' },
104
+ documentId: { type: 'string', description: 'Opcjonalne ID dokumentu kosztorysu — pozwala automatycznie pobrać filtry ze slotu (slotRowId).' },
105
+ slotRowId: { type: 'string', description: 'Opcjonalne ID wiersza/slotu w kosztorysie — automatycznie odczytuje jego categoryPath, priceMax, unit i pakiet.' },
106
+ unlockFilters: { type: 'boolean', description: 'Gdy true, zdejmuje blokadę pakietu i limitu ceny (szuka w całym katalogu) i oznacza które pozycje mieszczą się w pakiecie, a które wymagają redukcji/dopłaty.' },
107
+ includeRelated: { type: 'boolean', description: 'Czy zwracać powiązane usługi montażowe (domyślnie true).' },
108
+ page: { type: 'number', description: 'Numer strony (domyślnie 1).' },
109
+ pageSize: { type: 'number', description: 'Liczba wyników na stronę (domyślnie 20).' }
110
+ }
111
+ }
112
+ },
113
+ {
114
+ name: 'estimate_get_catalog_item',
115
+ description: 'Pobiera szczegółową kartę produktu lub usługi z Katalogu 5 (po ID lub SKU) wraz z przeliczeniem ceny sprzedaży netto/brutto, pełnych opakowań (packaging) oraz listą powiązanych usług montażowych (relatedItems).',
116
+ inputSchema: {
117
+ type: 'object',
118
+ required: ['itemIdOrSku'],
119
+ properties: {
120
+ itemIdOrSku: { type: 'string', description: 'ID pozycji w Katalogu 5 (np. "24481") lub kod SKU (np. "28052670", "DdUsl463").' },
121
+ markupPct: { type: 'number', description: 'Narzut pakietowy w % do wyliczenia ceny sprzedaży, gdy brak salePrice (domyślnie 57 dla Smart, 43 dla Modern, 35 dla High).' }
122
+ }
123
+ }
124
+ },
125
+ {
126
+ name: 'estimate_list_template_suggestions',
127
+ description: 'Zwraca zdeduplikowaną listę podpowiedzi usług i slotów produktowych ze wzorca pakietowego (SMART / MODERN / HIGH) z templatesSnapshot (odpowiednik zakładki "Wzorzec pakietu" w kosztorysie).',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ documentId: { type: 'string', description: 'Opcjonalne ID dokumentu kosztorysu.' },
132
+ package: { type: 'string', enum: ['SMART', 'MODERN', 'HIGH', 'smart', 'modern', 'high'], description: 'Pakiet wzorca (domyślnie SMART lub pakiet z dokumentu).' },
133
+ kind: { type: 'string', enum: ['service', 'product'], description: 'Filtr rodzaju pozycji ze wzorca (service lub product).' },
134
+ search: { type: 'string', description: 'Fraza wyszukiwania po nazwie, SKU lub kategorii.' }
135
+ }
136
+ }
137
+ },
138
+ {
139
+ name: 'estimate_assign_catalog_item',
140
+ description: 'Przypisuje produkt lub usługę z Katalogu 5 (po catalogItemId lub sku) do wskazanego slotu/wiersza (rowId lub slotKey) albo dodaje jako nową pozycję w pomieszczeniu (group). Automatycznie przelicza pełne opakowania (packaging, billedAreaM2), ceny netto/brutto i usługi powiązane (includeRelatedServices). Gdy wybrany produkt przekracza limit cenowy slotu (priceMax) lub asReplacement=true, automatycznie oznacza pozycję pakietową jako Redukcję (cancelled: true) i wstawia pod nią powiązany wiersz zamiennika z dopłatą!',
141
+ inputSchema: {
142
+ type: 'object',
143
+ required: ['documentId'],
144
+ properties: {
145
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
146
+ catalogItemId: { type: 'string', description: 'ID produktu lub usługi z Katalogu 5.' },
147
+ sku: { type: 'string', description: 'Kod SKU produktu lub usługi z Katalogu 5 (alternatywnie do catalogItemId).' },
148
+ rowId: { type: 'string', description: 'ID wiersza/slotu w kosztorysie do wypełnienia lub podmiany.' },
149
+ slotKey: { type: 'string', description: 'Klucz slotu lub nazwa slotu (alternatywnie do rowId).' },
150
+ group: { type: 'string', description: 'Nazwa pomieszczenia/grupy (gdy dodajemy nową pozycję bez rowId/slotKey).' },
151
+ quantity: { type: 'number', description: 'Opcjonalna ilość (gdy pominięta, brana jest ilość obmiarowa ze slotu).' },
152
+ includeRelatedServices: { type: 'boolean', description: 'Gdy true, automatycznie dopina powiązane usługi montażowe z relatedItems (jeśli nie ma ich jeszcze w slocie).' },
153
+ fillAllMatchingSlots: { type: 'boolean', description: 'Gdy true ("Uzupełnij wszystkie"), wstawia ten sam produkt także do wszystkich pozostałych pustych slotów tej samej kategorii w kosztorysie.' },
154
+ asReplacement: { type: 'boolean', description: 'Gdy true, zawsze redukuje pozycję pakietową (cancelled: true) i wstawia wybrany produkt jako zamiennik z dopłatą.' },
155
+ autoReduceOnExceed: { type: 'boolean', description: 'Gdy true (domyślnie), przekroczenie limitu priceMax automatycznie tworzy redukcję pozycji pakietowej i wstawia zamiennik.' },
156
+ forceInSlot: { type: 'boolean', description: 'Gdy true, wstawia produkt bezpośrednio w slot nawet powyżej limitu priceMax (bez tworzenia osobnego wiersza redukcji).' },
157
+ dryRun: { type: 'boolean', description: 'Symulacja bez zapisu do bazy.' }
158
+ }
159
+ }
160
+ },
161
+ {
162
+ name: 'estimate_reduce_or_replace_item',
163
+ description: 'Zarządza redukcjami ("rezygnacjami" klienta z pozycji pakietowych) oraz zamiennikami w kosztorysie. Obsługuje akcje: "reduce" (oznacza pozycję i opcjonalnie usługi w slocie jako cancelled: true, cancelledManual: true), "restore" (cofa redukcję i odłącza zamiennik), "replace" (redukuje pozycję pakietową i wstawia pod nią wiersz zamiennika z Katalogu 5 lub własny), "remove_permanently" (usuwa pozycję i dopisuje klucz do removedBindingKeys).',
164
+ inputSchema: {
165
+ type: 'object',
166
+ required: ['documentId', 'action'],
167
+ properties: {
168
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
169
+ action: { type: 'string', enum: ['reduce', 'restore', 'replace', 'remove_permanently'], description: 'Rodzaj operacji na pozycji.' },
170
+ itemId: { type: 'string', description: 'ID wiersza w kosztorysie.' },
171
+ slotKey: { type: 'string', description: 'Klucz slotKey lub nazwa pozycji (alternatywnie do itemId).' },
172
+ roomGroup: { type: 'string', description: 'Opcjonalny filtr nazwy pokoju/grupy przy szukaniu po slotKey (np. "Łazienka", "Kuchnia").' },
173
+ includeSlotServices: { type: 'boolean', description: 'Gdy true, redukuje/przywraca jednocześnie wszystkie usługi i produkty przypięte do tego samego slotu w tym pokoju.' },
174
+ replacementCatalogItemId: { type: 'string', description: 'Dla action="replace": ID produktu/usługi z Katalogu 5 jako zamiennika.' },
175
+ replacementSku: { type: 'string', description: 'Dla action="replace": SKU produktu/usługi z Katalogu 5 jako zamiennika.' },
176
+ replacementItem: {
177
+ type: 'object',
178
+ description: 'Dla action="replace" bez pozycji katalogowej: własne parametry zamiennika ({ name, quantity, unit, unitPrice, purchasePrice, vatRate, SKU }).'
179
+ },
180
+ dryRun: { type: 'boolean', description: 'Symulacja bez zapisu do bazy.' }
181
+ }
182
+ }
183
+ },
184
+ {
185
+ name: 'estimate_autofill_template_slots',
186
+ description: 'Automatycznie uzupełnia wszystkie puste sloty wzorcowe (isTemplate: true) w kosztorysie dopasowanymi produktami i usługami z Katalogu 5 zgodnie z wybranym pakietem (smart/modern/high) i limitami cenowymi priceMax. Uzupełnia też brakujące metadane pakietu (selectedPackageName, unitPriceM2) i przelicza totalPrices.',
187
+ inputSchema: {
188
+ type: 'object',
189
+ required: ['documentId'],
190
+ properties: {
191
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
192
+ documentName: { type: 'string', description: 'Opcjonalna nowa nazwa dokumentu kosztorysu.' },
193
+ dryRun: { type: 'boolean', description: 'Gdy true, symuluje uzupełnienie bez zapisu do bazy.' }
194
+ }
195
+ }
196
+ },
197
+ {
198
+ name: 'estimate_get_rooms_and_measurements',
199
+ description: 'Pobiera pełną listę pomieszczeń i obmiarów (roomsAndMeasurements: powierzchnia, obwód, wysokość, otwory drzwiowe/okienne), łączny metraż (totalArea) oraz mapowania pokoi na szablony pakietowe (estimateBindings.roomMappings, w tym udział aneksu kuchennego areaShare).',
200
+ inputSchema: {
201
+ type: 'object',
202
+ required: ['documentId'],
203
+ properties: {
204
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' }
205
+ }
206
+ }
207
+ },
208
+ {
209
+ name: 'estimate_update_rooms_and_measurements',
210
+ description: 'Edytuje obmiary i pomieszczenia w kosztorysie (dodawanie nowych pokoi, zmiana powierzchni area, obwodu perimeter, wysokości height, otworów drzwiowych openings, udziału aneksu kuchennego kitchenAreaShare, usuwanie pokoi). Automatycznie aktualizuje mapowania roomMappings, podpis (signature) oraz przelicza ilości i ceny wszystkich pozycji w kosztorysie powiązanych z obmiarami!',
211
+ inputSchema: {
212
+ type: 'object',
213
+ required: ['documentId'],
214
+ properties: {
215
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
216
+ rooms: {
217
+ type: 'array',
218
+ description: 'Opcjonalna pełna nowa lista pokoi (zastępuje dotychczasową listę).',
219
+ items: { type: 'object' }
220
+ },
221
+ updateRooms: {
222
+ type: 'array',
223
+ description: 'Lista aktualizacji istniejących pokoi (dopasowanie po id lub name), np. [{ name: "Łazienka", area: 5.2, perimeter: 9.4, height: 2.75 }].',
224
+ items: {
225
+ type: 'object',
226
+ properties: {
227
+ id: { type: 'string', description: 'ID pokoju.' },
228
+ name: { type: 'string', description: 'Nazwa pokoju do dopasowania.' },
229
+ newName: { type: 'string', description: 'Nowa nazwa pokoju (przy zmianie nazwy).' },
230
+ area: { type: 'number', description: 'Nowa powierzchnia podłogi w m².' },
231
+ perimeter: { type: 'number', description: 'Nowy obwód w mb.' },
232
+ height: { type: 'number', description: 'Nowa wysokość w m.' },
233
+ kitchenAreaShare: { type: 'number', description: 'Udział powierzchni aneksu kuchennego w m².' },
234
+ openings: { type: 'array', description: 'Lista otworów drzwiowych/okiennych.', items: { type: 'object' } }
235
+ }
236
+ }
237
+ },
238
+ addRooms: {
239
+ type: 'array',
240
+ description: 'Lista nowych pokoi do dodania ({ name, area, perimeter, height, kitchenAreaShare, openings }).',
241
+ items: { type: 'object' }
242
+ },
243
+ removeRoomIds: {
244
+ type: 'array',
245
+ description: 'Lista ID lub nazw pokoi do usunięcia.',
246
+ items: { type: 'string' }
247
+ },
248
+ balconyArea: { type: 'number', description: 'Powierzchnia balkonu/tarasu w m².' },
249
+ recalculateItems: { type: 'boolean', description: 'Czy automatycznie przeliczyć ilości i kwoty pozycji w kosztorysie na podstawie nowych obmiarów (domyślnie true).' },
250
+ dryRun: { type: 'boolean', description: 'Symulacja bez zapisu do bazy.' }
251
+ }
252
+ }
253
+ },
254
+ {
255
+ name: 'estimate_get_package_and_variants',
256
+ description: 'Pobiera aktualnie wybrany pakiet kosztorysu (Smart / Modern / High), dostępne pakiety oraz listę wszystkich pytań ankietowych wariantów (np. format płytek, rodzaj podłogi, prysznic vs wanna, przedział metrażu) wraz z dostępnymi opcjami i aktualnymi odpowiedziami (variantAnswers).',
257
+ inputSchema: {
258
+ type: 'object',
259
+ required: ['documentId'],
260
+ properties: {
261
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' }
262
+ }
263
+ }
264
+ },
265
+ {
266
+ name: 'estimate_set_package_and_variants',
267
+ description: 'Zmienia pakiet kosztorysu (SMART / MODERN / HIGH) i/lub odpowiedzi ankietowe wariantów (variantAnswers). Automatycznie przemapowuje pomieszczenia na nowy pakiet, uzgadnia i przebudowuje pozycje wzorcowe z zachowaniem wcześniej wybranych produktów i ręcznych redukcji (preserveChosenProducts: true), aktualizuje finishStandard, estimateBindings oraz przelicza sumy totalPrices.',
268
+ inputSchema: {
269
+ type: 'object',
270
+ required: ['documentId'],
271
+ properties: {
272
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
273
+ package: { type: 'string', enum: ['SMART', 'MODERN', 'HIGH', 'smart', 'modern', 'high', 'smart_life', 'modern_life', 'high_life'], description: 'Docelowy pakiet wykończeniowy.' },
274
+ variantAnswers: {
275
+ type: 'object',
276
+ description: 'Słownik odpowiedzi na pytania wariantowe (kluczem może być pełny answerKey lub treść pytania, np. { "Brodzik z kabiną, odpływ liniowy czy wanna?": "Kabina z odpływem liniowym" }).'
277
+ },
278
+ preserveChosenProducts: { type: 'boolean', description: 'Czy zachować produkty już dobrane w slotach przy zmianie pakietu/wariantu (domyślnie true).' },
279
+ resetReductions: { type: 'boolean', description: 'Gdy true, przywraca wcześniej zredukowane pozycje pakietowe i czyści removedBindingKeys.' },
280
+ dryRun: { type: 'boolean', description: 'Symulacja bez zapisu do bazy.' }
281
+ }
282
+ }
283
+ },
284
+ {
285
+ name: 'estimate_update_all_items',
286
+ description: 'Nadpisuje WSZYSTKIE pozycje kosztorysu (estimateItems) w dokumencie na dev.deergroup.pl. Obsługuje kompletny model danych, tworzy automatyczny backup i przelicza totalPrices (z pominięciem pozycji zredukowanych cancelled: true).',
287
+ inputSchema: {
288
+ type: 'object',
289
+ required: ['documentId', 'items'],
290
+ properties: {
291
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
292
+ items: {
293
+ type: 'array',
294
+ description: 'Pełna tablica obiektów CostEstimateItem do zapisania w dataJson.estimatePanelData.estimateItems.',
295
+ items: { type: 'object' }
296
+ },
297
+ recalculateTotals: { type: 'boolean', description: 'Czy automatycznie przeliczyć sumy kosztów netto/brutto (domyślnie true).' },
298
+ dryRun: { type: 'boolean', description: 'Gdy true, zwraca podsumowanie zmian bez zapisywania w bazie.' }
299
+ }
300
+ }
301
+ },
302
+ {
303
+ name: 'estimate_patch_items',
304
+ description: 'Atomowa modyfikacja wybranych pozycji kosztorysu (dodanie nowych, aktualizacja istniejących wg id/slotKey, usunięcie) bez naruszania pozostałych wierszy.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ required: ['documentId'],
308
+ properties: {
309
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
310
+ add: { type: 'array', description: 'Tablica nowych pozycji do wstawienia do kosztorysu.', items: { type: 'object' } },
311
+ update: { type: 'array', description: 'Tablica aktualizacji pozycji (dopasowanie po id lub binding.slotKey).', items: { type: 'object' } },
312
+ remove: { type: 'array', description: 'Tablica ID pozycji do usunięcia z kosztorysu.', items: { type: 'string' } },
313
+ dryRun: { type: 'boolean', description: 'Gdy true, symuluje operację bez zapisu do bazy.' }
314
+ }
315
+ }
316
+ },
317
+ {
318
+ name: 'estimate_set_item_binding',
319
+ description: 'Zmienia status pakietowy i rozliczeniowy pozycji kosztorysu (INCLUDED / SURCHARGE / REDUCTION / CUSTOM) oraz opcjonalną kwotę dopłaty ponad pakiet.',
320
+ inputSchema: {
321
+ type: 'object',
322
+ required: ['documentId', 'itemId', 'binding'],
323
+ properties: {
324
+ documentId: { type: 'string', description: 'ID dokumentu kosztorysu.' },
325
+ itemId: { type: 'string', description: 'ID pozycji lub klucz slotKey.' },
326
+ binding: {
327
+ type: 'string',
328
+ enum: ['INCLUDED', 'SURCHARGE', 'REDUCTION', 'CUSTOM'],
329
+ description: 'Nowy status powiązania z pakietem.'
330
+ },
331
+ surchargeAmountNet: { type: 'number', description: 'Kwota dopłaty netto (dla SURCHARGE).' },
332
+ dryRun: { type: 'boolean', description: 'Gdy true, symuluje operację.' }
333
+ }
334
+ }
335
+ },
336
+ {
337
+ name: 'estimate_get_packages',
338
+ description: 'Zwraca oficjalne pakiety Deer Design (Smart Life, Modern Life, High Life) wraz z ich specyfikacją, narzutami i limitami cenowymi.',
339
+ inputSchema: {
340
+ type: 'object',
341
+ properties: {
342
+ year: { type: 'number', description: 'Rok cennika (domyślnie 2026).' }
343
+ }
344
+ }
345
+ },
346
+ {
347
+ name: 'estimate_calculate_simple',
348
+ description: 'Szybkie przeliczenie szacunkowej wartości wykończenia dla zadanego metrażu i wybranego pakietu.',
349
+ inputSchema: {
350
+ type: 'object',
351
+ required: ['area_m2', 'package'],
352
+ properties: {
353
+ area_m2: { type: 'number', description: 'Powierzchnia mieszkania w m².' },
354
+ package: {
355
+ type: 'string',
356
+ enum: ['smart', 'modern', 'high', 'Smart Life', 'Modern Life', 'High Life'],
357
+ description: 'Wybrany pakiet wykończeniowy.'
358
+ }
359
+ }
360
+ }
361
+ }
362
+ ];
363
+
364
+ const PACKAGES_DATA = {
365
+ year: 2026,
366
+ packages: [
367
+ {
368
+ id: 'smart',
369
+ selectedPackageId: 'smart_life',
370
+ packageUuid: '21f4865a-3694-47a4-8f95-6ea67d9f4e40',
371
+ name: 'Smart Life',
372
+ unitPriceM2Template: 1290,
373
+ markupPct: 57,
374
+ pricePerM2Net: 1850,
375
+ description: 'Optymalny pakiet bazowy z kompletnym wyposażeniem łazienki, podłogami, drzwiami i malowaniem.',
376
+ typicalGrossM2: 1998,
377
+ tileLimitPriceNet: 65
378
+ },
379
+ {
380
+ id: 'modern',
381
+ selectedPackageId: 'modern_life',
382
+ packageUuid: '919e7e6f-f840-4198-ad7d-fb666ac91764',
383
+ name: 'Modern Life',
384
+ unitPriceM2Template: 1640,
385
+ markupPct: 43,
386
+ pricePerM2Net: 2450,
387
+ description: 'Najpopularniejszy standard wykończenia o podwyższonej estetyce i bogatszej gamie materiałowej.',
388
+ typicalGrossM2: 2646,
389
+ tileLimitPriceNet: 90
390
+ },
391
+ {
392
+ id: 'high',
393
+ selectedPackageId: 'high_life',
394
+ packageUuid: '8de495c0-e52b-414b-8f41-89d28c026ffa',
395
+ name: 'High Life',
396
+ unitPriceM2Template: 1990,
397
+ markupPct: 35,
398
+ pricePerM2Net: 3200,
399
+ description: 'Standard premium z materiałami wielkoformatowymi, armaturą podtynkową i najwyższą klasą wykończenia.',
400
+ typicalGrossM2: 3456,
401
+ tileLimitPriceNet: 130
402
+ }
403
+ ]
404
+ };
405
+
406
+ async function handleMcpMessage(msg) {
407
+ if (!msg || typeof msg !== 'object') return null;
408
+ const { id, method, params } = msg;
409
+
410
+ if (method === 'initialize') {
411
+ return {
412
+ jsonrpc: '2.0',
413
+ id,
414
+ result: {
415
+ protocolVersion: PROTOCOL_VERSION,
416
+ capabilities: { tools: {} },
417
+ serverInfo: SERVER_INFO
418
+ }
419
+ };
420
+ }
421
+
422
+ if (method === 'notifications/initialized') {
423
+ return null;
424
+ }
425
+
426
+ if (method === 'tools/list') {
427
+ return {
428
+ jsonrpc: '2.0',
429
+ id,
430
+ result: { tools: ESTIMATE_TOOLS }
431
+ };
432
+ }
433
+
434
+ if (method === 'tools/call') {
435
+ const toolName = params && params.name;
436
+ const args = (params && params.arguments) || {};
437
+
438
+ try {
439
+ let data;
440
+
441
+ if (toolName === 'estimate_list_projects') {
442
+ data = await client.listProjects(args);
443
+ } else if (toolName === 'estimate_get_project') {
444
+ data = await client.getProject(args.projectId);
445
+ } else if (toolName === 'estimate_list_documents') {
446
+ data = await client.listDocuments(args);
447
+ } else if (toolName === 'estimate_get_document') {
448
+ data = await client.getDocument(args.documentId, { summaryOnly: !!args.summaryOnly });
449
+ } else if (toolName === 'estimate_create_document') {
450
+ data = await client.createEstimateDocument(args);
451
+ } else if (toolName === 'estimate_search_catalog') {
452
+ data = await client.searchEstimateCatalog(args);
453
+ } else if (toolName === 'estimate_get_catalog_item') {
454
+ data = await client.getEstimateCatalogItem(args.itemIdOrSku, { markupPct: args.markupPct });
455
+ } else if (toolName === 'estimate_list_template_suggestions') {
456
+ data = await client.listTemplateSuggestions(args);
457
+ } else if (toolName === 'estimate_assign_catalog_item') {
458
+ data = await client.assignCatalogItemToEstimate(args.documentId, args);
459
+ } else if (toolName === 'estimate_reduce_or_replace_item') {
460
+ data = await client.reduceOrReplaceItem(args.documentId, args);
461
+ } else if (toolName === 'estimate_autofill_template_slots') {
462
+ data = await client.autoFillTemplateSlots(args.documentId, args);
463
+ } else if (toolName === 'estimate_get_rooms_and_measurements') {
464
+ data = await client.getRoomsAndMeasurements(args.documentId);
465
+ } else if (toolName === 'estimate_update_rooms_and_measurements') {
466
+ data = await client.updateRoomsAndMeasurements(args.documentId, args);
467
+ } else if (toolName === 'estimate_get_package_and_variants') {
468
+ data = await client.getPackageAndVariants(args.documentId);
469
+ } else if (toolName === 'estimate_set_package_and_variants') {
470
+ data = await client.setPackageAndVariants(args.documentId, args);
471
+ } else if (toolName === 'estimate_update_all_items') {
472
+ data = await client.updateAllEstimateItems(args.documentId, args.items, {
473
+ recalculateTotals: args.recalculateTotals !== false,
474
+ dryRun: !!args.dryRun
475
+ });
476
+ } else if (toolName === 'estimate_patch_items') {
477
+ data = await client.patchEstimateItems(args.documentId, {
478
+ add: args.add,
479
+ update: args.update,
480
+ remove: args.remove,
481
+ dryRun: !!args.dryRun
482
+ });
483
+ } else if (toolName === 'estimate_set_item_binding') {
484
+ data = await client.setItemBinding(args.documentId, args.itemId, {
485
+ binding: args.binding,
486
+ surchargeAmountNet: args.surchargeAmountNet,
487
+ dryRun: !!args.dryRun
488
+ });
489
+ } else if (toolName === 'estimate_get_packages') {
490
+ data = PACKAGES_DATA;
491
+ } else if (toolName === 'estimate_calculate_simple') {
492
+ const area = Number(args.area_m2) || 0;
493
+ const pkgName = String(args.package).toLowerCase();
494
+ let rate = 2450;
495
+ let matchedPkg = 'Modern Life';
496
+ if (pkgName.includes('smart')) { rate = 1850; matchedPkg = 'Smart Life'; }
497
+ else if (pkgName.includes('high')) { rate = 3200; matchedPkg = 'High Life'; }
498
+
499
+ const totalNet = Math.round(area * rate * 100) / 100;
500
+ const totalGross = Math.round(totalNet * 1.08 * 100) / 100;
501
+ data = {
502
+ area_m2: area,
503
+ package: matchedPkg,
504
+ rate_per_m2_net: rate,
505
+ total_estimate_net: totalNet,
506
+ total_estimate_gross_8pct: totalGross
507
+ };
508
+ } else {
509
+ return {
510
+ jsonrpc: '2.0',
511
+ id,
512
+ error: { code: -32601, message: `Nieznane narzędzie: ${toolName}` }
513
+ };
514
+ }
515
+
516
+ return {
517
+ jsonrpc: '2.0',
518
+ id,
519
+ result: {
520
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
521
+ }
522
+ };
523
+ } catch (e) {
524
+ return {
525
+ jsonrpc: '2.0',
526
+ id,
527
+ error: { code: -32000, message: e.message }
528
+ };
529
+ }
530
+ }
531
+
532
+ return {
533
+ jsonrpc: '2.0',
534
+ id,
535
+ error: { code: -32601, message: `Nieobsługiwana metoda: ${method}` }
536
+ };
537
+ }
538
+
539
+ function runEstimateMcpServer() {
540
+ return new Promise((resolve) => {
541
+ let pendingCount = 0;
542
+ let isClosed = false;
543
+ let closeResolve = null;
544
+
545
+ const rl = readline.createInterface({
546
+ input: process.stdin,
547
+ output: process.stdout,
548
+ terminal: false
549
+ });
550
+
551
+ rl.on('line', async (line) => {
552
+ const trimmed = line.trim();
553
+ if (!trimmed) return;
554
+ pendingCount++;
555
+ try {
556
+ const msg = JSON.parse(trimmed);
557
+ const resp = await handleMcpMessage(msg);
558
+ if (resp) {
559
+ process.stdout.write(JSON.stringify(resp) + '\n');
560
+ }
561
+ } catch (e) {
562
+ process.stdout.write(JSON.stringify({
563
+ jsonrpc: '2.0',
564
+ id: null,
565
+ error: { code: -32700, message: `Parse error: ${e.message}` }
566
+ }) + '\n');
567
+ } finally {
568
+ pendingCount--;
569
+ if (isClosed && pendingCount === 0 && closeResolve) {
570
+ closeResolve();
571
+ }
572
+ }
573
+ });
574
+
575
+ rl.on('close', () => {
576
+ isClosed = true;
577
+ if (pendingCount === 0) {
578
+ resolve();
579
+ } else {
580
+ closeResolve = resolve;
581
+ }
582
+ });
583
+ process.on('SIGINT', () => { rl.close(); resolve(); });
584
+ process.on('SIGTERM', () => { rl.close(); resolve(); });
585
+ });
586
+ }
587
+
588
+ module.exports = {
589
+ ESTIMATE_TOOLS,
590
+ handleMcpMessage,
591
+ runEstimateMcpServer
592
+ };
package/src/skills-hub.js CHANGED
@@ -491,10 +491,20 @@ async function getDocs(slug = null) {
491
491
  }
492
492
 
493
493
  async function submitPromotion(skillSlug, proposedGlobalSlug, justification) {
494
+ // Supabase edge function oczekuje UUID skilla (skill_id)
495
+ let skillId = skillSlug;
496
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(skillSlug);
497
+ if (!isUuid) {
498
+ const info = await getSkill(skillSlug);
499
+ if (info && info.data && info.data.skill && info.data.skill.id) {
500
+ skillId = info.data.skill.id;
501
+ }
502
+ }
503
+
494
504
  return hubRequest('/promotions', {
495
505
  method: 'POST',
496
506
  body: {
497
- skill_slug: skillSlug,
507
+ skill_id: skillId,
498
508
  proposed_global_slug: proposedGlobalSlug,
499
509
  justification,
500
510
  },