@deerdesign/deertools 1.2.23 → 1.2.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deerdesign/deertools",
3
- "version": "1.2.23",
3
+ "version": "1.2.25",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
package/src/commands.js CHANGED
@@ -7,6 +7,7 @@ const { getProduct, resolveProducts, ALL_IDS } = require('./registry');
7
7
  const { login, normalizeScope } = require('./login');
8
8
  const { discover, fetchText, apiRequest, explain } = require('./http');
9
9
  const { listOperations, parseFrontmatterName } = require('./openapi');
10
+ const { calculateEstimate, getEstimateMargins } = require('./estimate-client');
10
11
 
11
12
  const out = (s = '') => process.stdout.write(`${s}\n`);
12
13
  const err = (s = '') => process.stderr.write(`${s}\n`);
@@ -214,75 +215,120 @@ async function cmdApi(pos, flags) {
214
215
  out(res.json ? JSON.stringify(res.json, null, flags.raw ? 0 : 2) : res.text);
215
216
  }
216
217
 
217
- async function cmdEstimate(pos, flags) {
218
+ async function cmdEstimate(pos, flags, options = {}) {
218
219
  const sub = pos[0];
220
+ const write = options.out || out;
219
221
 
220
222
  if (sub === 'calculate' || sub === 'calc') {
221
223
  const area = Number(pos[1] || flags.area);
222
- if (!area || isNaN(area)) fail('Użycie: deertools estimate calculate <metraż_m2> [smart_life|modern_life|high_life]', 2);
223
- const pkgs = {
224
- smart_life: { name: 'Smart Life', rate: 1290, flat: 58050, marginPct: 33.1 },
225
- modern_life: { name: 'Modern Life', rate: 1640, flat: 73800, marginPct: 31.2 },
226
- high_life: { name: 'High Life', rate: 1990, flat: 89550, marginPct: 22.7 },
227
- };
228
- const isFlat = area < 45;
224
+ if (!area || isNaN(area)) fail('Użycie: deertools estimate calculate <metraż_m2> [smart_life|modern_life|high_life] [--wet=<liczba>] [--doors=<liczba>] [--basis=avg|max]', 2);
225
+
229
226
  const requested = pos[2] || flags.package;
230
- const selected = requested ? [pkgs[requested] || pkgs.modern_life] : Object.values(pkgs);
231
-
232
- const results = selected.map(p => {
233
- const gross = isFlat ? p.flat : Math.round(area * p.rate);
234
- const net = Math.round(gross / 1.08);
235
- const vat = gross - net;
236
- const profitNet = Math.round(net * (p.marginPct / 100));
237
- return {
238
- package: p.name,
239
- area_m2: area,
240
- price_per_m2_gross: p.rate,
241
- total_gross_pln: gross,
242
- total_net_pln: net,
243
- vat_pln: vat,
244
- is_flat_rate: isFlat,
245
- ref_margin_pct: p.marginPct,
246
- est_profit_net_pln: profitNet,
247
- };
248
- });
249
-
250
- if (flags.json) { out(JSON.stringify(results.length === 1 ? results[0] : results, null, 2)); return; }
251
-
252
- out(`\nKalkulacja wykończenia dla metrażu ${area} m²${isFlat ? ' (stawka ryczałtowa < 45 m²)' : ''}:`);
227
+ const wetInput = flags.wet !== undefined ? Number(flags.wet) : (flags.wetZones !== undefined ? Number(flags.wetZones) : undefined);
228
+ const doorsInput = flags.doors !== undefined ? Number(flags.doors) : undefined;
229
+
230
+ const data = await calculateEstimate({
231
+ area_m2: area,
232
+ package: requested || undefined,
233
+ wet_zones: wetInput,
234
+ doors: doorsInput,
235
+ }, options);
236
+
237
+ const estimates = (data && data.estimates) ? data.estimates : [];
238
+ if (!estimates.length) {
239
+ fail('Nie otrzymano wyników kalkulacji z serwera Kosztorysu.', 4);
240
+ }
241
+
242
+ const results = estimates.map(e => ({
243
+ package: e.package,
244
+ package_key: e.slug,
245
+ slug: e.slug,
246
+ area_m2: e.area_m2,
247
+ price_per_m2_gross: e.price_per_m2_gross,
248
+ is_flat_rate: !!e.is_lump_sum,
249
+ is_lump_sum: !!e.is_lump_sum,
250
+ lump_sum_max_area_m2: e.lump_sum_max_area_m2,
251
+ base_gross_pln: e.base_gross,
252
+ base_net_pln: Math.round((e.base_gross / 1.08) * 100) / 100,
253
+ standard_wet_zones: e.standard_wet_zones,
254
+ wet_zones: e.wet_zones,
255
+ extra_wet_zones: Math.max(0, (e.wet_zones || 0) - (e.standard_wet_zones || 0)),
256
+ wet_surcharge_gross_pln: e.wet_zone_surcharge_gross,
257
+ wet_surcharge_net_pln: Math.round(((e.wet_zone_surcharge_gross || 0) / 1.08) * 100) / 100,
258
+ standard_doors: e.standard_doors,
259
+ doors: (e.standard_doors || 0) + (e.complexity_levels || 0),
260
+ complexity_levels: e.complexity_levels,
261
+ complexity_surcharge_label: e.complexity_surcharge_label || 'Dopłata z tytułu złożoności projektu',
262
+ complexity_surcharge_gross_pln: e.complexity_surcharge_gross,
263
+ complexity_surcharge_net_pln: Math.round(((e.complexity_surcharge_gross || 0) / 1.08) * 100) / 100,
264
+ total_gross_pln: e.total_gross,
265
+ total_net_pln: e.total_net,
266
+ vat_pln: e.vat_pln,
267
+ currency: e.currency || 'PLN',
268
+ max_discount_pct: e.max_discount_pct,
269
+ discount_locked: e.discount_locked,
270
+ source: 'Kosztorys Backend API (estimate-mcp)',
271
+ }));
272
+
273
+ if (flags.json) {
274
+ write(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
275
+ return;
276
+ }
277
+
278
+ const isFlat = results.some(r => r.is_flat_rate);
279
+ write(`\nKalkulacja wykończenia dla metrażu ${area} m² (źródło: Kosztorys API${isFlat ? ', ryczałt do 40 m²' : ''}):`);
253
280
  const fmt = (n) => new Intl.NumberFormat('pl-PL', { maximumFractionDigits: 0 }).format(Math.round(n));
254
281
  for (const r of results) {
255
- out(` ▶ ${r.package.padEnd(14)} ${fmt(r.total_gross_pln)} zł brutto (${fmt(r.total_net_pln)} zł netto + ${fmt(r.vat_pln)} zł VAT)`);
282
+ write(` ▶ ${r.package.padEnd(14)} ${fmt(r.total_gross_pln)} zł brutto (${fmt(r.total_net_pln)} zł netto + ${fmt(r.vat_pln)} zł VAT)`);
283
+ if (r.extra_wet_zones > 0 || r.complexity_levels > 0) {
284
+ write(` - Podstawa: ${fmt(r.base_gross_pln)} zł brutto`);
285
+ if (r.extra_wet_zones > 0) write(` - Dodatkowe strefy mokre (+${r.extra_wet_zones}): +${fmt(r.wet_surcharge_gross_pln)} zł brutto`);
286
+ if (r.complexity_levels > 0) write(` - ${r.complexity_surcharge_label} (+${r.complexity_levels} drzwi): +${fmt(r.complexity_surcharge_gross_pln)} zł brutto`);
287
+ }
256
288
  }
257
289
  return;
258
290
  }
259
291
 
260
292
  if (sub === 'margins') {
261
293
  const area = Number(pos[1] || flags.area || 50);
262
- const pkgs = [
263
- { name: 'Smart Life', rate: 1290, margin: 33.1 },
264
- { name: 'Modern Life', rate: 1640, margin: 31.2 },
265
- { name: 'High Life', rate: 1990, margin: 22.7 },
266
- ];
267
- const isFlat = area < 45;
268
- const rows = pkgs.map(p => {
269
- const gross = isFlat ? (p.rate * 45) : (area * p.rate);
270
- const net = gross / 1.08;
271
- const profit = net * (p.margin / 100);
272
- const cost = net - profit;
273
- return {
274
- package: p.name,
275
- gross: Math.round(gross),
276
- net: Math.round(net),
277
- cost: Math.round(cost),
278
- profit: Math.round(profit),
279
- margin: p.margin,
280
- };
281
- });
282
- if (flags.json) { out(JSON.stringify(rows, null, 2)); return; }
283
- out(`\nAnaliza marż referencyjnych Deer Design dla ${area} m²:`);
294
+
295
+ const data = await getEstimateMargins({
296
+ area_m2: area,
297
+ }, options);
298
+
299
+ const estimates = (data && data.estimates) ? data.estimates : [];
300
+ if (!estimates.length) {
301
+ fail('Nie otrzymano reguł marżowych z serwera Kosztorysu.', 4);
302
+ }
303
+
304
+ const rows = estimates.map(e => ({
305
+ package: e.package,
306
+ slug: e.slug,
307
+ area_m2: e.area_m2,
308
+ base_gross_pln: e.base_gross,
309
+ base_net_pln: e.total_net,
310
+ is_flat_rate: !!e.is_lump_sum,
311
+ std_wet_zones: e.standard_wet_zones,
312
+ std_doors: e.standard_doors,
313
+ wet_surcharge_gross: e.wet_zone_surcharge_gross,
314
+ complexity_surcharge_label: e.complexity_surcharge_label || 'Dopłata z tytułu złożoności projektu',
315
+ complexity_surcharge_gross: e.complexity_surcharge_gross,
316
+ margin_rule_simplified: e.margin_rule_simplified,
317
+ margin_rule_quick: e.margin_rule_quick,
318
+ formula: e.formula,
319
+ note: e.note,
320
+ source: 'Kosztorys Backend API (estimate-mcp)',
321
+ }));
322
+
323
+ if (flags.json) { write(JSON.stringify(rows, null, 2)); return; }
324
+ write(`\nZasady wyliczania marży Deer Design (Nowy standard 2026 z Kosztorysu dla ${area} m²):`);
325
+ write(` • Kosztorys Uproszczony: ${rows[0]?.margin_rule_simplified}`);
326
+ write(` • Kosztorys Szybki: ${rows[0]?.margin_rule_quick}`);
327
+ write(` • Formuła marży: ${rows[0]?.formula}`);
328
+ write(` • Uwaga: ${rows[0]?.note}\n`);
284
329
  for (const r of rows) {
285
- out(` • ${r.package.padEnd(14)} Przychód netto: ${r.net} zł · Koszt: ${r.cost} zł · Zysk: ${r.profit} zł (${r.margin}%)`);
330
+ write(` ▶ ${r.package.padEnd(14)} Podstawa: ${r.base_gross_pln} zł brutto (${r.base_net_pln} zł netto) | Std: ${r.std_wet_zones} strefa mokra, ${r.std_doors} drzwi`);
331
+ write(` Reguła marży: ${r.margin_rule_simplified}`);
286
332
  }
287
333
  return;
288
334
  }
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+ const credentials = require('./credentials');
3
+ const { getProduct } = require('./registry');
4
+
5
+ /**
6
+ * Klient API / MCP dla Kosztorysu Deer Design.
7
+ * Wszystkie kalkulacje (stawki, progi ryczałtowe do 40 m², dopłaty za strefy mokre,
8
+ * dopłaty ze względu na złożoność projektu oraz reguły marżowe) są wykonywane
9
+ * wyłącznie po stronie serwera Kosztorysu (Supabase Edge Function: estimate-mcp).
10
+ * CLI nie zawiera lokalnie zakodowanych tabel cenowych ani algorytmów marży.
11
+ */
12
+
13
+ async function callEstimateMcp(toolName, args = {}, options = {}) {
14
+ const product = getProduct('estimate');
15
+ const found = credentials.getKey(product);
16
+ const apiKey = options.apiKey || (found && found.key);
17
+
18
+ if (!apiKey && !options.allowAnonymous) {
19
+ const err = new Error(
20
+ `Nie jesteś zalogowany do Kosztorysu. Kalkulacja wymaga autoryzacji z serwera DeerTools.\nUruchom: deertools login estimate`
21
+ );
22
+ err.exitCode = 3;
23
+ throw err;
24
+ }
25
+
26
+ const endpoint = options.endpoint || product.mcpEndpoint || `${product.fallbackApiBase}/estimate-mcp`;
27
+ const fetchFn = options.fetchImpl || globalThis.fetch;
28
+
29
+ const payload = {
30
+ jsonrpc: '2.0',
31
+ id: Date.now(),
32
+ method: 'tools/call',
33
+ params: {
34
+ name: toolName,
35
+ arguments: args,
36
+ },
37
+ };
38
+
39
+ const headers = {
40
+ 'Content-Type': 'application/json',
41
+ Accept: 'application/json',
42
+ };
43
+ if (apiKey) {
44
+ headers['x-api-key'] = apiKey;
45
+ }
46
+
47
+ let res;
48
+ try {
49
+ res = await fetchFn(endpoint, {
50
+ method: 'POST',
51
+ headers,
52
+ body: JSON.stringify(payload),
53
+ });
54
+ } catch (netErr) {
55
+ const err = new Error(`Błąd połączenia z serwerem Kosztorysu (${endpoint}): ${netErr.message}`);
56
+ err.exitCode = 4;
57
+ throw err;
58
+ }
59
+
60
+ if (!res.ok) {
61
+ let errBody = '';
62
+ try {
63
+ errBody = await res.text();
64
+ } catch (_) {}
65
+ if (res.status === 401) {
66
+ const err = new Error(
67
+ `Brak dostępu do Kosztorysu (HTTP 401). Klucz wygasł lub jest niepoprawny.\nUruchom ponowne logowanie: deertools login estimate`
68
+ );
69
+ err.exitCode = 3;
70
+ throw err;
71
+ }
72
+ const err = new Error(`Serwer Kosztorysu zwrócił błąd HTTP ${res.status}: ${errBody || res.statusText}`);
73
+ err.exitCode = 4;
74
+ throw err;
75
+ }
76
+
77
+ const json = await res.json();
78
+ if (json.error) {
79
+ const err = new Error(`Błąd kalkulacji Kosztorysu: ${json.error.message || JSON.stringify(json.error)}`);
80
+ err.exitCode = 4;
81
+ throw err;
82
+ }
83
+
84
+ if (json.result?.structuredContent) {
85
+ return json.result.structuredContent;
86
+ }
87
+ if (json.result?.content?.[0]?.text) {
88
+ try {
89
+ return JSON.parse(json.result.content[0].text);
90
+ } catch (_) {
91
+ return json.result.content[0].text;
92
+ }
93
+ }
94
+
95
+ return json.result || {};
96
+ }
97
+
98
+ async function calculateEstimate(params, options = {}) {
99
+ return callEstimateMcp('estimate_calculate_simple', params, options);
100
+ }
101
+
102
+ async function getEstimateMargins(params, options = {}) {
103
+ return callEstimateMcp('estimate_get_margins', params, options);
104
+ }
105
+
106
+ module.exports = {
107
+ callEstimateMcp,
108
+ calculateEstimate,
109
+ getEstimateMargins,
110
+ };
package/src/mcp-stdio.js CHANGED
@@ -5,6 +5,7 @@ const { ALL_PRODUCTS, getProduct } = require('./registry');
5
5
  const { apiRequest } = require('./http');
6
6
  const { checkSkillUpdates, updateSkills } = require('./skill-commands');
7
7
  const skillsHub = require('./skills-hub');
8
+ const { calculateEstimate, getEstimateMargins } = require('./estimate-client');
8
9
 
9
10
  const PROTOCOL_VERSION = '2024-11-05';
10
11
  const SERVER_INFO = { name: 'deertools-stdio-bridge', version: '1.0.5' };
@@ -16,13 +17,19 @@ async function cmdMcpServe(pos, flags) {
16
17
  process.stdout.write(JSON.stringify(msg) + '\n');
17
18
  }
18
19
 
20
+ let pendingCount = 0;
21
+ let isClosed = false;
22
+ let closeResolve = null;
23
+
19
24
  rl.on('line', async (line) => {
20
25
  if (!line.trim()) return;
21
- let req;
22
- try { req = JSON.parse(line); } catch (e) {
23
- send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
24
- return;
25
- }
26
+ pendingCount++;
27
+ try {
28
+ let req;
29
+ try { req = JSON.parse(line); } catch (e) {
30
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
31
+ return;
32
+ }
26
33
 
27
34
  const { id, method, params } = req;
28
35
 
@@ -47,12 +54,25 @@ async function cmdMcpServe(pos, flags) {
47
54
  const tools = [
48
55
  {
49
56
  name: 'estimate_calculate',
50
- description: 'Szybkie wyliczenie kosztu wykończenia mieszkania z podanego metrażu w pakietach Smart Life, Modern Life, High Life.',
57
+ description: 'Szybkie wyliczenie kosztu wykończenia mieszkania z podanego metrażu w pakietach Smart Life, Modern Life, High Life (kalkulacja po stronie serwera Kosztorys).',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ area_m2: { type: 'number', description: 'Metraż mieszkania w m²' },
62
+ package: { type: 'string', enum: ['smart_life', 'modern_life', 'high_life'], description: 'Pakiet wykończeniowy (opcjonalny — bez podania zwraca 3 pakiety)' },
63
+ wet_zones: { type: 'number', description: 'Liczba stref mokrych (np. łazienki, WC)' },
64
+ doors: { type: 'number', description: 'Liczba kompletów drzwi wewnętrznych' },
65
+ },
66
+ required: ['area_m2'],
67
+ },
68
+ },
69
+ {
70
+ name: 'estimate_get_margins',
71
+ description: 'Pobiera reguły wyliczania marży i strukturę kosztorysową Deer Design dla zadanego metrażu z serwera Kosztorys.',
51
72
  inputSchema: {
52
73
  type: 'object',
53
74
  properties: {
54
75
  area_m2: { type: 'number', description: 'Metraż mieszkania w m²' },
55
- package: { type: 'string', enum: ['smart_life', 'modern_life', 'high_life'], description: 'Pakiet wykończeniowy' },
56
76
  },
57
77
  required: ['area_m2'],
58
78
  },
@@ -161,16 +181,16 @@ async function cmdMcpServe(pos, flags) {
161
181
  try {
162
182
  if (toolName === 'estimate_calculate') {
163
183
  const area = Number(args.area_m2);
164
- const pkg = args.package || 'modern_life';
165
- const pkgs = {
166
- smart_life: { name: 'Smart Life', rate: 1290, flat: 58050 },
167
- modern_life: { name: 'Modern Life', rate: 1640, flat: 73800 },
168
- high_life: { name: 'High Life', rate: 1990, flat: 89550 },
169
- };
170
- const p = pkgs[pkg] || pkgs.modern_life;
171
- const isFlat = area < 45;
172
- const gross = isFlat ? p.flat : Math.round(area * p.rate);
173
- const net = Math.round(gross / 1.08);
184
+ const pkg = args.package;
185
+ const wetZones = args.wet_zones !== undefined ? Number(args.wet_zones) : undefined;
186
+ const doors = args.doors !== undefined ? Number(args.doors) : undefined;
187
+
188
+ const data = await calculateEstimate({
189
+ area_m2: area,
190
+ package: pkg,
191
+ wet_zones: wetZones,
192
+ doors: doors,
193
+ });
174
194
 
175
195
  send({
176
196
  jsonrpc: '2.0',
@@ -178,15 +198,26 @@ async function cmdMcpServe(pos, flags) {
178
198
  result: {
179
199
  content: [{
180
200
  type: 'text',
181
- text: JSON.stringify({
182
- package: p.name,
183
- area_m2: area,
184
- total_gross_pln: gross,
185
- total_net_pln: net,
186
- is_flat_rate: isFlat,
187
- currency: 'PLN',
188
- }, null, 2),
201
+ text: JSON.stringify(data, null, 2),
202
+ }],
203
+ structuredContent: data,
204
+ },
205
+ });
206
+ return;
207
+ }
208
+
209
+ if (toolName === 'estimate_get_margins') {
210
+ const area = Number(args.area_m2 || 50);
211
+ const data = await getEstimateMargins({ area_m2: area });
212
+ send({
213
+ jsonrpc: '2.0',
214
+ id,
215
+ result: {
216
+ content: [{
217
+ type: 'text',
218
+ text: JSON.stringify(data, null, 2),
189
219
  }],
220
+ structuredContent: data,
190
221
  },
191
222
  });
192
223
  return;
@@ -383,6 +414,23 @@ async function cmdMcpServe(pos, flags) {
383
414
  }
384
415
 
385
416
  send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
417
+ } finally {
418
+ pendingCount--;
419
+ if (isClosed && pendingCount === 0 && closeResolve) {
420
+ closeResolve();
421
+ }
422
+ }
423
+ });
424
+
425
+ rl.on('close', () => {
426
+ isClosed = true;
427
+ if (pendingCount === 0 && closeResolve) {
428
+ closeResolve();
429
+ }
430
+ });
431
+
432
+ return new Promise((resolve) => {
433
+ closeResolve = resolve;
386
434
  });
387
435
  }
388
436