@deerdesign/deertools 1.2.31 → 1.2.33

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 CHANGED
@@ -179,12 +179,103 @@ deertools team create "Interior Designers" --slug designers # Create new organiz
179
179
 
180
180
  # 9. AI Agent MCP Registry & Configuration:
181
181
  deertools mcp list # List available MCP servers across the ecosystem
182
- deertools mcp config # Generate Remote MCP JSON config for Cursor, Claude, Antigravity
183
- deertools mcp install # Automatically write MCP config to local AI agents
182
+ deertools mcp config # Print JSON MCP configuration for manual pasting
183
+ deertools mcp install # Automatically write & update MCP config across ALL 7 agent targets
184
+ deertools doctor # Verify MCP connectivity, auth tokens and client config files
184
185
  ```
185
186
 
186
187
  ---
187
188
 
189
+ ## AI Agent MCP Configuration & Tooling (Cursor, Antigravity, Claude)
190
+
191
+ `@deerdesign/deertools` provides 7 official MCP servers covering the complete Deer Design ecosystem:
192
+ 1. `deer-estimate`: Floor plan PDF analysis, kitchen detection, interior cost calculations.
193
+ 2. `deer-catalog`: 17k+ finishing materials (Smart/Modern/High Life), technical specs, filtering.
194
+ 3. `deer-deliveries`: Site delivery tracking, supplier shipments, warehouse WZ generation.
195
+ 4. `deer-supplier`: Supplier orders, purchase discounts, sales markups, commercial terms.
196
+ 5. `deer-rendermind`: Interior rendering, pCon CAD visualization, product photorealistic renders.
197
+ 6. `deer-baselinker`: Full BaseLinker catalog CRUD, pricing batch updates, Smart Cache.
198
+ 7. `deer-skills`: DeerSkills Hub registry, skill search, download, and review pipelines.
199
+
200
+ ### 1. Automatic Zero-Config Setup (Recommended)
201
+
202
+ You don't need to manually edit JSON files. Simply log in or run:
203
+
204
+ ```bash
205
+ deertools login all # Automatically logs in and syncs MCP configs across all clients
206
+ # or at any time:
207
+ deertools mcp install # Re-generates and writes configuration to all 7 targets
208
+ ```
209
+
210
+ `deertools mcp install` automatically discovers and writes to all 7 supported AI environments:
211
+
212
+ | Target Environment | Configuration Path | Supported Tools |
213
+ | :--- | :--- | :--- |
214
+ | **Workspace Standard** | `<project-root>/.mcp.json` | Claude Code, Claude Desktop Cowork, Windsurf, Zed |
215
+ | **Claude Desktop (macOS)** | `~/Library/Application Support/Claude/claude_desktop_config.json` | Claude Desktop native MCP |
216
+ | **Claude Code CLI** | `~/.claude.json` | Claude Code terminal assistant |
217
+ | **Cursor IDE (Global)** | `~/.cursor/mcp.json` | Cursor Agent & Composer |
218
+ | **Cursor Workspace** | `<project-root>/.cursor/mcp.json` | Cursor project-scoped |
219
+ | **Antigravity Workspace** | `<project-root>/.agents/mcp_config.json` | Google Antigravity / Gemini Agent |
220
+ | **Antigravity Global** | `~/.gemini/config/mcp_config.json` | Google Antigravity global profile |
221
+
222
+ Verify all clients at any time with:
223
+ ```bash
224
+ deertools doctor
225
+ ```
226
+
227
+ ---
228
+
229
+ ### 2. Manual Configuration & Claude Integration Details
230
+
231
+ If configuring manually or adding to existing setups:
232
+
233
+ #### A. Project-level `.mcp.json` (Recommended for Claude Code & Cowork)
234
+ Create or edit `.mcp.json` in your project root:
235
+
236
+ ```json
237
+ {
238
+ "mcpServers": {
239
+ "deer-rendermind": {
240
+ "command": "/Users/<your-user>/.local/bin/deertools",
241
+ "args": ["rendermind", "mcp"],
242
+ "env": {
243
+ "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/<your-user>/.local/bin"
244
+ }
245
+ },
246
+ "deer-baselinker": {
247
+ "command": "/Users/<your-user>/.local/bin/deertools",
248
+ "args": ["baselinker", "mcp"],
249
+ "env": {
250
+ "PATH": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/<your-user>/.local/bin"
251
+ }
252
+ },
253
+ "deer-catalog": {
254
+ "url": "https://ftzaobeixglnssemugjf.supabase.co/functions/v1/catalog-mcp",
255
+ "headers": {
256
+ "x-api-key": "<your-catalog-key>",
257
+ "Authorization": "Bearer <your-catalog-key>"
258
+ }
259
+ },
260
+ "deer-estimate": {
261
+ "url": "https://vlemuxlubrowuuxpgllk.supabase.co/functions/v1/estimate-mcp",
262
+ "headers": {
263
+ "x-api-key": "<your-estimate-key>",
264
+ "Authorization": "Bearer <your-estimate-key>"
265
+ }
266
+ }
267
+ }
268
+ }
269
+ ```
270
+
271
+ > [!IMPORTANT]
272
+ > **Key Rules for Claude Desktop & Claude Code:**
273
+ > 1. **PATH Inheritance on macOS**: Desktop GUI applications (like Claude Desktop) do NOT inherit your terminal's shell `$PATH` (`~/.local/bin`, `/usr/local/bin`). Always specify the absolute binary path (e.g. `~/.local/bin/deertools`) and include the `env.PATH` block as shown above so `node` and child processes can be spawned without errors.
274
+ > 2. **Project Trust Dialog**: When opening a workspace with a project-level `.mcp.json`, Claude displays a security consent prompt (*"Allow MCP servers from .mcp.json?"*). You must click **Allow / Always Allow** to activate the tools.
275
+ > 3. **Session Restart Required**: MCP server tools are registered strictly when a conversation session is initialized. If you configure MCP while a session is already active, start a **New Chat (`Cmd + N`)** or restart the Claude application (`Cmd + Q`). MCP tools cannot be hot-reloaded into an ongoing conversation.
276
+
277
+ ---
278
+
188
279
  ## BaseLinker MCP Proxy & Catalog Management
189
280
 
190
281
  `@deerdesign/deertools` includes a built-in, local **BaseLinker MCP Proxy & Hub** providing deep catalog management:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deerdesign/deertools",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -0,0 +1,306 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { getClient } = require('./client');
6
+
7
+ const CACHE_DIR = path.join(os.homedir(), '.deertools', 'cache');
8
+ const TTL_HOURS = 24;
9
+
10
+ // Zdefiniowane stałe katalogu Kosztorys Deer Design
11
+ const DEER_KOSZTORYS_CATALOG_ID = 81186;
12
+ const ROOT_CAT_USLUGI = 5578342;
13
+ const ROOT_CAT_PRODUKTY = 8769388;
14
+ const DEFAULT_PRICE_GROUP_ID = '49162'; // Domyślna grupa cenowa (PLN brutto)
15
+
16
+ function ensureCacheDir() {
17
+ if (!fs.existsSync(CACHE_DIR)) {
18
+ fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
19
+ }
20
+ }
21
+
22
+ function getCachePath(inventoryId) {
23
+ ensureCacheDir();
24
+ return path.join(CACHE_DIR, `inventory_${inventoryId}.json`);
25
+ }
26
+
27
+ function getMetadataPath(inventoryId) {
28
+ ensureCacheDir();
29
+ return path.join(CACHE_DIR, `inventory_${inventoryId}.meta.json`);
30
+ }
31
+
32
+ let inMemoryCache = {};
33
+
34
+ /**
35
+ * Wczytuje cache dla danego katalogu (z pamięci podręcznej lub z dysku).
36
+ */
37
+ function loadCache(inventoryId = DEER_KOSZTORYS_CATALOG_ID) {
38
+ const invId = Number(inventoryId) || DEER_KOSZTORYS_CATALOG_ID;
39
+ if (inMemoryCache[invId]) {
40
+ return inMemoryCache[invId];
41
+ }
42
+
43
+ const filePath = getCachePath(invId);
44
+ if (!fs.existsSync(filePath)) {
45
+ return null;
46
+ }
47
+
48
+ try {
49
+ const raw = fs.readFileSync(filePath, 'utf8');
50
+ const data = JSON.parse(raw);
51
+ inMemoryCache[invId] = data;
52
+ return data;
53
+ } catch (err) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Zapisuje cache na dysk i do pamięci.
60
+ */
61
+ function saveCache(inventoryId, cacheData) {
62
+ const invId = Number(inventoryId);
63
+ ensureCacheDir();
64
+ const filePath = getCachePath(invId);
65
+ fs.writeFileSync(filePath, JSON.stringify(cacheData), { encoding: 'utf8', mode: 0o600 });
66
+ inMemoryCache[invId] = cacheData;
67
+ }
68
+
69
+ /**
70
+ * Buduje mapę kategorii z przynależnością do gałęzi (Usługi vs Produkty).
71
+ */
72
+ function buildCategoryHierarchy(categories = []) {
73
+ const catMap = {};
74
+ for (const c of categories) {
75
+ catMap[c.category_id] = { ...c, branch: 'Inne' };
76
+ }
77
+
78
+ function getRootBranch(catId) {
79
+ let curr = catMap[catId];
80
+ let depth = 0;
81
+ while (curr && depth < 10) {
82
+ if (curr.category_id === ROOT_CAT_USLUGI || curr.parent_id === ROOT_CAT_USLUGI) {
83
+ return 'Usługi';
84
+ }
85
+ if (curr.category_id === ROOT_CAT_PRODUKTY || curr.parent_id === ROOT_CAT_PRODUKTY) {
86
+ return 'Produkty';
87
+ }
88
+ if (curr.parent_id === 0) {
89
+ if (curr.name?.toLowerCase().includes('usług') || curr.name?.toLowerCase().includes('uslug')) return 'Usługi';
90
+ if (curr.name?.toLowerCase().includes('produkt')) return 'Produkty';
91
+ return curr.name;
92
+ }
93
+ curr = catMap[curr.parent_id];
94
+ depth++;
95
+ }
96
+ return 'Inne';
97
+ }
98
+
99
+ for (const c of categories) {
100
+ catMap[c.category_id].branch = getRootBranch(c.category_id);
101
+ }
102
+
103
+ return catMap;
104
+ }
105
+
106
+ /**
107
+ * Pełna lub przyrostowa synchronizacja produktów z BaseLinker do lokalnego cache.
108
+ */
109
+ async function syncCatalogCache(inventoryId = DEER_KOSZTORYS_CATALOG_ID, force = false, client = null) {
110
+ const invId = Number(inventoryId) || DEER_KOSZTORYS_CATALOG_ID;
111
+ const existing = loadCache(invId);
112
+ const now = Date.now();
113
+
114
+ if (!force && existing && existing.synced_at) {
115
+ const ageHours = (now - existing.synced_at) / (1000 * 3600);
116
+ if (ageHours < TTL_HOURS) {
117
+ return {
118
+ cached: true,
119
+ age_hours: Math.round(ageHours * 10) / 10,
120
+ total_products: Object.keys(existing.products || {}).length,
121
+ synced_at: new Date(existing.synced_at).toISOString(),
122
+ message: `Cache dla katalogu ${invId} jest aktualny (${Math.round(ageHours)}h temu). Użyj force=true aby wymusić odświeżenie.`
123
+ };
124
+ }
125
+ }
126
+
127
+ const bl = client || getClient();
128
+
129
+ // 1. Pobierz kategorie
130
+ const catRes = await bl.call('getInventoryCategories', { inventory_id: invId });
131
+ const rawCategories = catRes.categories || [];
132
+ const categories = buildCategoryHierarchy(rawCategories);
133
+
134
+ // 2. Pobierz produkty strona po stronie
135
+ const products = {};
136
+ let page = 1;
137
+ let hasMore = true;
138
+
139
+ while (hasMore) {
140
+ const listRes = await bl.call('getInventoryProductsList', {
141
+ inventory_id: invId,
142
+ page
143
+ });
144
+
145
+ const pageProducts = listRes.products || {};
146
+ const count = Object.keys(pageProducts).length;
147
+ if (count === 0) {
148
+ hasMore = false;
149
+ break;
150
+ }
151
+
152
+ for (const [id, p] of Object.entries(pageProducts)) {
153
+ const catId = p.category_id || 0;
154
+ const catObj = categories[catId];
155
+ const branch = catObj?.branch || (p.sku?.startsWith('DdUsl') ? 'Usługi' : 'Produkty');
156
+ const priceBrutto = p.prices?.[DEFAULT_PRICE_GROUP_ID] ?? Object.values(p.prices || {})[0] ?? 0;
157
+
158
+ products[id] = {
159
+ id: Number(id),
160
+ sku: p.sku || '',
161
+ ean: p.ean || '',
162
+ name: p.name || '',
163
+ category_id: catId,
164
+ category_name: catObj?.name || '',
165
+ branch,
166
+ price_brutto: Number(priceBrutto) || 0,
167
+ stock: p.stock || {}
168
+ };
169
+ }
170
+
171
+ if (count < 1000) {
172
+ hasMore = false;
173
+ } else {
174
+ page++;
175
+ }
176
+ }
177
+
178
+ const cachePayload = {
179
+ inventory_id: invId,
180
+ synced_at: now,
181
+ total_products: Object.keys(products).length,
182
+ total_categories: rawCategories.length,
183
+ categories,
184
+ products
185
+ };
186
+
187
+ saveCache(invId, cachePayload);
188
+
189
+ return {
190
+ cached: false,
191
+ total_products: Object.keys(products).length,
192
+ total_categories: rawCategories.length,
193
+ synced_at: new Date(now).toISOString(),
194
+ message: `Pomyślnie zsynchronizowano katalog ${invId}: ${Object.keys(products).length} pozycji.`
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Błyskawiczne przeszukiwanie lokalnego cache produktów (2-5ms).
200
+ */
201
+ function searchCatalogCache(inventoryId = DEER_KOSZTORYS_CATALOG_ID, opts = {}) {
202
+ const invId = Number(inventoryId) || DEER_KOSZTORYS_CATALOG_ID;
203
+ const cache = loadCache(invId);
204
+ if (!cache || !cache.products) {
205
+ return null; // Brak cache, fallback do live API
206
+ }
207
+
208
+ const {
209
+ query = '',
210
+ filter_name = '',
211
+ filter_sku = '',
212
+ filter_ean = '',
213
+ filter_category_id = null,
214
+ branch = 'wszystko', // 'wszystko' | 'uslugi' | 'produkty'
215
+ page = 1,
216
+ limit = 50
217
+ } = opts;
218
+
219
+ const searchTokens = (query || filter_name || '')
220
+ .toLowerCase()
221
+ .trim()
222
+ .split(/\s+/)
223
+ .filter(t => t.length > 0);
224
+
225
+ const skuQuery = (filter_sku || '').toLowerCase().trim();
226
+ const eanQuery = (filter_ean || '').trim();
227
+ const targetCatId = filter_category_id ? Number(filter_category_id) : null;
228
+ const targetBranch = branch ? branch.toLowerCase() : 'wszystko';
229
+
230
+ const results = [];
231
+
232
+ for (const p of Object.values(cache.products)) {
233
+ // Filtrowanie gałęzi
234
+ if (targetBranch === 'uslugi' || targetBranch === 'usługi') {
235
+ if (p.branch !== 'Usługi' && !p.sku?.startsWith('DdUsl')) continue;
236
+ } else if (targetBranch === 'produkty') {
237
+ if (p.branch === 'Usługi' || p.sku?.startsWith('DdUsl')) continue;
238
+ }
239
+
240
+ // Filtrowanie kategorii
241
+ if (targetCatId && p.category_id !== targetCatId) {
242
+ continue;
243
+ }
244
+
245
+ // Filtrowanie SKU
246
+ if (skuQuery && !p.sku.toLowerCase().includes(skuQuery)) {
247
+ continue;
248
+ }
249
+
250
+ // Filtrowanie EAN
251
+ if (eanQuery && !p.ean.includes(eanQuery)) {
252
+ continue;
253
+ }
254
+
255
+ // Filtrowanie po tokenach słownych (każde słowo musi pasować w nazwie lub SKU)
256
+ if (searchTokens.length > 0) {
257
+ const haystack = `${p.name} ${p.sku} ${p.category_name}`.toLowerCase();
258
+ const allMatch = searchTokens.every(token => haystack.includes(token));
259
+ if (!allMatch) continue;
260
+ }
261
+
262
+ results.push(p);
263
+ }
264
+
265
+ // Paginacja
266
+ const startIndex = (page - 1) * limit;
267
+ const paginated = results.slice(startIndex, startIndex + limit);
268
+
269
+ return {
270
+ from_cache: true,
271
+ cache_synced_at: new Date(cache.synced_at).toISOString(),
272
+ total_found: results.length,
273
+ page,
274
+ limit,
275
+ total_pages: Math.ceil(results.length / limit) || 1,
276
+ products: paginated
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Aktualizacja ceny w lokalnym cache po udanej operacji write-through w BaseLinker.
282
+ */
283
+ function updateProductPriceInCache(inventoryId = DEER_KOSZTORYS_CATALOG_ID, productId, priceGroupId, newPrice) {
284
+ const invId = Number(inventoryId) || DEER_KOSZTORYS_CATALOG_ID;
285
+ const cache = loadCache(invId);
286
+ if (!cache || !cache.products) return;
287
+
288
+ const p = cache.products[productId];
289
+ if (p) {
290
+ if (String(priceGroupId) === DEFAULT_PRICE_GROUP_ID || !priceGroupId) {
291
+ p.price_brutto = Number(newPrice);
292
+ }
293
+ saveCache(invId, cache);
294
+ }
295
+ }
296
+
297
+ module.exports = {
298
+ DEER_KOSZTORYS_CATALOG_ID,
299
+ ROOT_CAT_USLUGI,
300
+ ROOT_CAT_PRODUKTY,
301
+ loadCache,
302
+ saveCache,
303
+ syncCatalogCache,
304
+ searchCatalogCache,
305
+ updateProductPriceInCache
306
+ };
@@ -1,6 +1,12 @@
1
1
  'use strict';
2
2
  const { getClient } = require('./client');
3
3
  const { searchMethods, getMethodSchema } = require('./crawler');
4
+ const {
5
+ DEER_KOSZTORYS_CATALOG_ID,
6
+ syncCatalogCache,
7
+ searchCatalogCache,
8
+ updateProductPriceInCache
9
+ } = require('./cache');
4
10
 
5
11
  /**
6
12
  * Definicje narzędzi MCP dla zaawansowanego zarządzania katalogiem BaseLinker.
@@ -20,21 +26,39 @@ const CATALOG_MCP_TOOLS = [
20
26
  required: ['inventory_id', 'product_id']
21
27
  }
22
28
  },
29
+ {
30
+ name: 'bl_catalog_sync_cache',
31
+ description: 'Pobiera i odświeża lokalny szybki cache produktów i kategorii wybranego katalogu BaseLinker (np. 81186 Kosztorys) do błyskawicznego przeszukiwania lokalnego (2-5ms) bez zużywania limitów API (100 req/min).',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ inventory_id: { type: 'integer', default: 81186, description: 'ID katalogu BaseLinker (domyślnie 81186 Kosztorys).' },
36
+ force: { type: 'boolean', default: false, description: 'Czy wymusić pełne pobranie nawet jeśli cache jest świeży (< 24h).' }
37
+ }
38
+ }
39
+ },
23
40
  {
24
41
  name: 'bl_catalog_search_products',
25
- description: 'Wyszukuje i listuje produkty z katalogu BaseLinker wg nazwy, SKU, EAN, kategorii lub producenta.',
42
+ description: 'Błyskawiczne wyszukiwanie i filtrowanie produktów/usług z katalogu BaseLinker (lokalny cache 2ms lub live API). Obsługuje wyszukiwanie frazowe, podział na gałąź Usługi (DdUsl) vs Produkty oraz filtry SKU/kategorii.',
26
43
  inputSchema: {
27
44
  type: 'object',
28
45
  properties: {
29
- inventory_id: { type: 'integer', description: 'ID katalogu BaseLinker.' },
30
- filter_category_id: { type: 'integer', description: 'Opcjonalne filtrowanie po ID kategorii.' },
46
+ inventory_id: { type: 'integer', default: 81186, description: 'ID katalogu BaseLinker (domyślnie 81186 Kosztorys).' },
47
+ query: { type: 'string', description: 'Dowolna fraza wyszukiwania (szuka po słowach w nazwie, SKU i kategorii).' },
48
+ branch: {
49
+ type: 'string',
50
+ enum: ['wszystko', 'uslugi', 'produkty'],
51
+ default: 'wszystko',
52
+ description: 'Filtruj gałąź katalogu: "uslugi" (gałąź Usługi ID 5578342 / SKU DdUsl), "produkty" (gałąź Produkty ID 8769388 / armatura, płytki, podłogi), "wszystko".'
53
+ },
54
+ filter_category_id: { type: 'integer', description: 'Opcjonalne filtrowanie po dokładnym ID kategorii.' },
31
55
  filter_manufacturer_id: { type: 'integer', description: 'Opcjonalne filtrowanie po ID producenta.' },
32
- filter_sku: { type: 'string', description: 'Opcjonalne wyszukiwanie po SKU produktu.' },
56
+ filter_sku: { type: 'string', description: 'Opcjonalne wyszukiwanie po SKU produktu (np. DdUsl247, A5D186ENB0).' },
33
57
  filter_ean: { type: 'string', description: 'Opcjonalne wyszukiwanie po kodzie EAN.' },
34
58
  filter_name: { type: 'string', description: 'Opcjonalne wyszukiwanie po fragmencie nazwy.' },
35
- page: { type: 'integer', default: 1, description: 'Numer strony wyników (po 1000 produktów).' }
36
- },
37
- required: ['inventory_id']
59
+ use_cache: { type: 'boolean', default: true, description: 'Domyślnie true (błyskawiczne przeszukiwanie lokalnego cache 2ms). Ustaw false, aby wymusić zapytanie live do API.' },
60
+ page: { type: 'integer', default: 1, description: 'Numer strony wyników.' }
61
+ }
38
62
  }
39
63
  },
40
64
  {
@@ -56,7 +80,9 @@ const CATALOG_MCP_TOOLS = [
56
80
  height: { type: 'number', description: 'Wysokość w cm.' },
57
81
  width: { type: 'number', description: 'Szerokość w cm.' },
58
82
  length: { type: 'number', description: 'Długość w cm.' },
59
- prices: { type: 'object', description: 'Ceny brutto wg grup cenowych, np. {"105": 149.99}.' },
83
+ prices: { type: 'object', description: 'Ceny brutto wg grup cenowych, np. {"49162": 149.99}.' },
84
+ average_cost: { type: 'number', description: 'Koszt własny / stawka robocizny wykonawcy netto.' },
85
+ average_landed_cost: { type: 'number', description: 'Średni koszt dodatkowy / koszt materiału budowlanego dodatkowego netto z zapasem.' },
60
86
  stock: { type: 'object', description: 'Stany magazynowe wg magazynów, np. {"bl_123": 25}.' },
61
87
  tags: { type: 'array', items: { type: 'string' }, description: 'Lista tagów przypisanych do produktu.' }
62
88
  },
@@ -278,14 +304,49 @@ async function handleCatalogToolCall(toolName, args, client = null) {
278
304
  return product;
279
305
  }
280
306
 
307
+ case 'bl_catalog_sync_cache': {
308
+ const { inventory_id = DEER_KOSZTORYS_CATALOG_ID, force = false } = args;
309
+ return await syncCatalogCache(inventory_id, force, bl);
310
+ }
311
+
281
312
  case 'bl_catalog_search_products': {
282
- const { inventory_id, filter_category_id, filter_manufacturer_id, filter_sku, filter_ean, filter_name, page = 1 } = args;
313
+ const {
314
+ inventory_id = DEER_KOSZTORYS_CATALOG_ID,
315
+ query,
316
+ branch = 'wszystko',
317
+ filter_category_id,
318
+ filter_manufacturer_id,
319
+ filter_sku,
320
+ filter_ean,
321
+ filter_name,
322
+ use_cache = true,
323
+ page = 1
324
+ } = args;
325
+
326
+ // 1. Spróbuj lokalnego błyskawicznego przeszukania cache (2ms)
327
+ if (use_cache !== false) {
328
+ const cachedRes = searchCatalogCache(inventory_id, {
329
+ query: query || filter_name,
330
+ filter_name,
331
+ filter_sku,
332
+ filter_ean,
333
+ filter_category_id,
334
+ branch,
335
+ page,
336
+ limit: 50
337
+ });
338
+ if (cachedRes) {
339
+ return cachedRes;
340
+ }
341
+ }
342
+
343
+ // 2. Fallback do live API BaseLinkera
283
344
  const params = { inventory_id, page };
284
345
  if (filter_category_id) params.filter_category_id = filter_category_id;
285
346
  if (filter_manufacturer_id) params.filter_manufacturer_id = filter_manufacturer_id;
286
347
  if (filter_sku) params.filter_sku = filter_sku;
287
348
  if (filter_ean) params.filter_ean = filter_ean;
288
- if (filter_name) params.filter_name = filter_name;
349
+ if (filter_name || query) params.filter_name = filter_name || query;
289
350
  return await bl.call('getInventoryProductsList', params);
290
351
  }
291
352
 
@@ -407,11 +468,20 @@ async function handleCatalogToolCall(toolName, args, client = null) {
407
468
  }
408
469
 
409
470
  case 'bl_catalog_update_prices_batch': {
410
- const { inventory_id, products } = args;
411
- return await bl.call('updateInventoryProductsPrices', {
471
+ const { inventory_id = DEER_KOSZTORYS_CATALOG_ID, products } = args;
472
+ const res = await bl.call('updateInventoryProductsPrices', {
412
473
  inventory_id,
413
474
  products
414
475
  });
476
+ // Write-through cache update
477
+ try {
478
+ for (const [prodId, groupPrices] of Object.entries(products || {})) {
479
+ for (const [groupId, priceVal] of Object.entries(groupPrices || {})) {
480
+ updateProductPriceInCache(inventory_id, prodId, groupId, priceVal);
481
+ }
482
+ }
483
+ } catch (_) {}
484
+ return res;
415
485
  }
416
486
 
417
487
  case 'bl_catalog_update_stock_batch': {
package/src/commands.js CHANGED
@@ -52,7 +52,8 @@ async function cmdLogin(pos, flags) {
52
52
  log: out
53
53
  });
54
54
  out(`✓ Zalogowano do BaseLinker. Token zapisany w credentials.json.`);
55
- out(` Serwer MCP: deertools mcp install | Test połączenia: deertools baselinker test`);
55
+ installMcpServers(null, true);
56
+ out(` Serwery MCP: automatycznie zaktualizowano konfigurację dla Cursor, Antigravity i Claude.`);
56
57
  return;
57
58
  } catch (e) {
58
59
  err(`✗ BaseLinker: ${e.message}`);
@@ -67,6 +68,7 @@ async function cmdLogin(pos, flags) {
67
68
  const token = pos[1] || flags.token;
68
69
  await loginSkillsDirect(token);
69
70
  out(`✓ Zalogowano do DeerSkills Hub. Klucz zapisany w credentials.json.`);
71
+ installMcpServers(null, true);
70
72
  out(` Przegląd umiejętności: deertools skill list | Synchronizacja: deertools skill sync`);
71
73
  return;
72
74
  } catch (e) {
@@ -105,23 +107,26 @@ async function cmdLogin(pos, flags) {
105
107
  : scope;
106
108
  const entry = await login(product, { scope: productScope, port: flags.port ? Number(flags.port) : 0, clientName: flags.name, openBrowser: flags['no-open'] ? null : undefined, log: out });
107
109
  out(`✓ Zalogowano${entry.email ? ` jako ${entry.email}` : ''}. Zakres: ${entry.scope}${entry.expires_at ? `, ważny do ${entry.expires_at.slice(0, 10)}` : ''}.`);
108
- out(` Konfiguracja MCP: deertools mcp install | Synchronizacja skilli: deertools skill sync`);
109
110
  } catch (e) {
110
111
  failures += 1;
111
112
  err(`✗ ${product.name}: ${e.message}`);
112
113
  }
113
114
  }
114
115
  if (failures) fail(`Nie udało się zalogować do ${failures} z ${products.length} produktów.`, 1);
116
+ out(`\n[Auto-Sync] Automatycznie zaktualizowano konfigurację serwerów MCP dla Cursor, Antigravity, Claude i .mcp.json.`);
117
+ installMcpServers(null, true);
115
118
  }
116
119
 
117
120
  async function cmdLogout(pos) {
118
121
  if (pos[0] === 'baselinker' || pos[0] === 'bl') {
119
122
  out(credentials.removeProduct('baselinker') ? `✓ Wylogowano z BaseLinker (token usunięty lokalnie).` : `– BaseLinker: nie byłeś zalogowany.`);
123
+ installMcpServers(null, true);
120
124
  return;
121
125
  }
122
126
  for (const product of resolveProducts(pos[0])) {
123
127
  out(credentials.removeProduct(product.id) ? `✓ Wylogowano z ${product.name} (klucz usunięty lokalnie).` : `– ${product.name}: nie byłeś zalogowany.`);
124
128
  }
129
+ installMcpServers(null, true);
125
130
  if (pos[0] === 'all') {
126
131
  if (credentials.removeProduct('baselinker')) {
127
132
  out('✓ Wylogowano z BaseLinker (token usunięty lokalnie).');
@@ -384,77 +389,158 @@ async function cmdMcp(pos, flags) {
384
389
  return;
385
390
  }
386
391
 
387
- if (sub === 'install') {
388
- const servers = buildMcpServersConfig(products, true);
389
- let touched = 0;
390
- // 1. Cursor global (~/.cursor/mcp.json)
391
- const cursorGlobal = path.join(os.homedir(), '.cursor', 'mcp.json');
392
+ function installMcpServers(customProducts = null, silent = false) {
393
+ const prods = customProducts || resolveProducts('all');
394
+ const servers = buildMcpServersConfig(prods, true);
395
+ let touched = 0;
396
+
397
+ // 1. Cursor global (~/.cursor/mcp.json)
398
+ const cursorGlobal = path.join(os.homedir(), '.cursor', 'mcp.json');
399
+ try {
400
+ let cur = { mcpServers: {} };
401
+ if (fs.existsSync(cursorGlobal)) {
402
+ try { cur = JSON.parse(fs.readFileSync(cursorGlobal, 'utf8')); } catch (_) {}
403
+ }
404
+ cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
405
+ fs.mkdirSync(path.dirname(cursorGlobal), { recursive: true });
406
+ fs.writeFileSync(cursorGlobal, JSON.stringify(cur, null, 2));
407
+ if (!silent) out(`✓ Cursor IDE: ${cursorGlobal}`);
408
+ touched++;
409
+ } catch (e) {
410
+ if (!silent) err(`✗ Cursor IDE (${cursorGlobal}): ${e.message}`);
411
+ }
412
+
413
+ // 2. Cursor workspace (.cursor/mcp.json)
414
+ const workspaceCursor = path.join(process.cwd(), '.cursor', 'mcp.json');
415
+ if (fs.existsSync(path.join(process.cwd(), '.cursor')) || fs.existsSync(path.join(process.cwd(), '.git'))) {
392
416
  try {
393
417
  let cur = { mcpServers: {} };
394
- if (fs.existsSync(cursorGlobal)) {
395
- try { cur = JSON.parse(fs.readFileSync(cursorGlobal, 'utf8')); } catch (_) {}
418
+ if (fs.existsSync(workspaceCursor)) {
419
+ try { cur = JSON.parse(fs.readFileSync(workspaceCursor, 'utf8')); } catch (_) {}
396
420
  }
397
421
  cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
398
- fs.mkdirSync(path.dirname(cursorGlobal), { recursive: true });
399
- fs.writeFileSync(cursorGlobal, JSON.stringify(cur, null, 2));
400
- out(`✓ Cursor IDE: ${cursorGlobal}`);
422
+ fs.mkdirSync(path.dirname(workspaceCursor), { recursive: true });
423
+ fs.writeFileSync(workspaceCursor, JSON.stringify(cur, null, 2));
424
+ if (!silent) out(`✓ Cursor Workspace: ${workspaceCursor}`);
401
425
  touched++;
402
426
  } catch (e) {
403
- err(`✗ Cursor IDE (${cursorGlobal}): ${e.message}`);
427
+ if (!silent) err(`✗ Cursor Workspace (${workspaceCursor}): ${e.message}`);
404
428
  }
429
+ }
405
430
 
406
- // 2. Cursor workspace (.cursor/mcp.json)
407
- const workspaceCursor = path.join(process.cwd(), '.cursor', 'mcp.json');
408
- if (fs.existsSync(path.join(process.cwd(), '.cursor')) || fs.existsSync(path.join(process.cwd(), '.git'))) {
409
- try {
410
- let cur = { mcpServers: {} };
411
- if (fs.existsSync(workspaceCursor)) {
412
- try { cur = JSON.parse(fs.readFileSync(workspaceCursor, 'utf8')); } catch (_) {}
413
- }
414
- cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
415
- fs.mkdirSync(path.dirname(workspaceCursor), { recursive: true });
416
- fs.writeFileSync(workspaceCursor, JSON.stringify(cur, null, 2));
417
- out(`✓ Cursor Workspace: ${workspaceCursor}`);
418
- touched++;
419
- } catch (e) {
420
- err(`✗ Cursor Workspace (${workspaceCursor}): ${e.message}`);
431
+ // 3. Antigravity / Gemini Workspace (.agents/mcp_config.json)
432
+ const agentsConfig = path.join(process.cwd(), '.agents', 'mcp_config.json');
433
+ if (fs.existsSync(path.dirname(agentsConfig))) {
434
+ try {
435
+ let cur = { mcpServers: {} };
436
+ if (fs.existsSync(agentsConfig)) {
437
+ try { cur = JSON.parse(fs.readFileSync(agentsConfig, 'utf8')); } catch (_) {}
421
438
  }
439
+ cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
440
+ fs.writeFileSync(agentsConfig, JSON.stringify(cur, null, 2));
441
+ if (!silent) out(`✓ Antigravity / Agent Workspace: ${agentsConfig}`);
442
+ touched++;
443
+ } catch (e) {
444
+ if (!silent) err(`✗ Antigravity (${agentsConfig}): ${e.message}`);
422
445
  }
446
+ }
423
447
 
424
- // 3. Antigravity / Gemini Workspace (.agents/mcp_config.json)
425
- const agentsConfig = path.join(process.cwd(), '.agents', 'mcp_config.json');
426
- if (fs.existsSync(path.dirname(agentsConfig))) {
427
- try {
428
- let cur = { mcpServers: {} };
429
- if (fs.existsSync(agentsConfig)) {
430
- try { cur = JSON.parse(fs.readFileSync(agentsConfig, 'utf8')); } catch (_) {}
448
+ // 4. Antigravity Global (~/.gemini/config/mcp_config.json)
449
+ const geminiGlobal = path.join(os.homedir(), '.gemini', 'config', 'mcp_config.json');
450
+ if (fs.existsSync(path.dirname(geminiGlobal))) {
451
+ try {
452
+ let cur = { mcpServers: {} };
453
+ if (fs.existsSync(geminiGlobal)) {
454
+ try { cur = JSON.parse(fs.readFileSync(geminiGlobal, 'utf8')); } catch (_) {}
455
+ }
456
+ cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
457
+ fs.writeFileSync(geminiGlobal, JSON.stringify(cur, null, 2));
458
+ if (!silent) out(`✓ Antigravity Global: ${geminiGlobal}`);
459
+ touched++;
460
+ } catch (e) {
461
+ if (!silent) err(`✗ Antigravity Global (${geminiGlobal}): ${e.message}`);
462
+ }
463
+ }
464
+
465
+ // 5. Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json)
466
+ const claudeDesktop = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
467
+ if (fs.existsSync(path.dirname(claudeDesktop))) {
468
+ try {
469
+ let cur = {};
470
+ if (fs.existsSync(claudeDesktop)) {
471
+ try { cur = JSON.parse(fs.readFileSync(claudeDesktop, 'utf8')); } catch (_) {}
472
+ }
473
+ const claudeServers = {};
474
+ for (const [k, v] of Object.entries(servers)) {
475
+ claudeServers[k] = { ...v };
476
+ if (v.command) {
477
+ claudeServers[k].env = {
478
+ PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:' + path.join(os.homedir(), '.local', 'bin')
479
+ };
431
480
  }
432
- cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
433
- fs.writeFileSync(agentsConfig, JSON.stringify(cur, null, 2));
434
- out(`✓ Antigravity / Agent Workspace: ${agentsConfig}`);
435
- touched++;
436
- } catch (e) {
437
- err(`✗ Antigravity (${agentsConfig}): ${e.message}`);
438
481
  }
482
+ cur.mcpServers = Object.assign({}, cur.mcpServers || {}, claudeServers);
483
+ fs.writeFileSync(claudeDesktop, JSON.stringify(cur, null, 2));
484
+ if (!silent) out(`✓ Claude Desktop: ${claudeDesktop}`);
485
+ touched++;
486
+ } catch (e) {
487
+ if (!silent) err(`✗ Claude Desktop (${claudeDesktop}): ${e.message}`);
439
488
  }
489
+ }
440
490
 
441
- // 4. Antigravity Global (~/.gemini/config/mcp_config.json)
442
- const geminiGlobal = path.join(os.homedir(), '.gemini', 'config', 'mcp_config.json');
443
- if (fs.existsSync(path.dirname(geminiGlobal))) {
444
- try {
445
- let cur = { mcpServers: {} };
446
- if (fs.existsSync(geminiGlobal)) {
447
- try { cur = JSON.parse(fs.readFileSync(geminiGlobal, 'utf8')); } catch (_) {}
491
+ // 6. Claude Code CLI (~/.claude.json)
492
+ const claudeCli = path.join(os.homedir(), '.claude.json');
493
+ if (fs.existsSync(claudeCli)) {
494
+ try {
495
+ let cur = {};
496
+ try { cur = JSON.parse(fs.readFileSync(claudeCli, 'utf8')); } catch (_) {}
497
+ const claudeServers = {};
498
+ for (const [k, v] of Object.entries(servers)) {
499
+ claudeServers[k] = { ...v };
500
+ if (v.command) {
501
+ claudeServers[k].env = {
502
+ PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:' + path.join(os.homedir(), '.local', 'bin')
503
+ };
448
504
  }
449
- cur.mcpServers = Object.assign({}, cur.mcpServers, servers);
450
- fs.writeFileSync(geminiGlobal, JSON.stringify(cur, null, 2));
451
- out(`✓ Antigravity Global: ${geminiGlobal}`);
452
- touched++;
453
- } catch (e) {
454
- err(`✗ Antigravity Global (${geminiGlobal}): ${e.message}`);
505
+ }
506
+ cur.mcpServers = Object.assign({}, cur.mcpServers || {}, claudeServers);
507
+ fs.writeFileSync(claudeCli, JSON.stringify(cur, null, 2));
508
+ if (!silent) out(`✓ Claude Code CLI: ${claudeCli}`);
509
+ touched++;
510
+ } catch (e) {
511
+ if (!silent) err(`✗ Claude Code CLI (${claudeCli}): ${e.message}`);
512
+ }
513
+ }
514
+
515
+ // 7. Workspace .mcp.json (Claude Code / Cowork / Standard MCP workspace)
516
+ const workspaceMcp = path.join(process.cwd(), '.mcp.json');
517
+ try {
518
+ let cur = {};
519
+ if (fs.existsSync(workspaceMcp)) {
520
+ try { cur = JSON.parse(fs.readFileSync(workspaceMcp, 'utf8')); } catch (_) {}
521
+ }
522
+ const claudeServers = {};
523
+ for (const [k, v] of Object.entries(servers)) {
524
+ claudeServers[k] = { ...v };
525
+ if (v.command) {
526
+ claudeServers[k].env = {
527
+ PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:' + path.join(os.homedir(), '.local', 'bin')
528
+ };
455
529
  }
456
530
  }
531
+ cur.mcpServers = Object.assign({}, cur.mcpServers || {}, claudeServers);
532
+ fs.writeFileSync(workspaceMcp, JSON.stringify(cur, null, 2));
533
+ if (!silent) out(`✓ Workspace MCP (.mcp.json): ${workspaceMcp}`);
534
+ touched++;
535
+ } catch (e) {
536
+ if (!silent) err(`✗ Workspace MCP (${workspaceMcp}): ${e.message}`);
537
+ }
538
+
539
+ return touched;
540
+ }
457
541
 
542
+ if (sub === 'install') {
543
+ const touched = installMcpServers(products, false);
458
544
  if (!touched) fail('Nie znaleziono środowiska agenta do instalacji MCP.', 4);
459
545
  out(`\nPomyślnie skonfigurowano wszystkie 7 serwerów MCP dla produktów DeerTools!`);
460
546
  return;
package/src/doctor.js CHANGED
@@ -254,7 +254,11 @@ async function cmdDoctor(pos, flags) {
254
254
  const mcpLocations = [
255
255
  { name: 'Cursor IDE (global)', path: path.join(os.homedir(), '.cursor', 'mcp.json') },
256
256
  { name: 'Cursor Workspace', path: path.join(process.cwd(), '.cursor', 'mcp.json') },
257
- { name: 'Antigravity / Gemini Workspace', path: path.join(process.cwd(), '.agents', 'mcp_config.json') },
257
+ { name: 'Antigravity Workspace', path: path.join(process.cwd(), '.agents', 'mcp_config.json') },
258
+ { name: 'Antigravity Global', path: path.join(os.homedir(), '.gemini', 'config', 'mcp_config.json') },
259
+ { name: 'Claude Desktop', path: path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
260
+ { name: 'Claude Code CLI', path: path.join(os.homedir(), '.claude.json') },
261
+ { name: 'Workspace MCP (.mcp.json)', path: path.join(process.cwd(), '.mcp.json') },
258
262
  ];
259
263
  for (const loc of mcpLocations) {
260
264
  if (fs.existsSync(loc.path)) {