@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,164 @@
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-deliveries-mcp', version: '1.3.0' };
8
+
9
+ const FALLBACK_DELIVERIES_TOOLS = [
10
+ {
11
+ name: 'list_deliveries',
12
+ description: 'Pobiera listę dostaw z systemu DeerTools Dostawy (ship.deergroup.pl).',
13
+ inputSchema: { type: 'object', properties: { status: { type: 'string' }, limit: { type: 'number' } } }
14
+ },
15
+ {
16
+ name: 'get_delivery',
17
+ description: 'Pobiera szczegóły pojedynczej dostawy.',
18
+ inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }
19
+ },
20
+ {
21
+ name: 'search_deliveries',
22
+ description: 'Wyszukuje dostawy po frazie, projekcie lub dostawcy.',
23
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } } }
24
+ }
25
+ ];
26
+
27
+ function getDeliveriesKey() {
28
+ const p = getProduct('deliveries');
29
+ const cred = credentials.getKey(p);
30
+ if (cred && cred.key) return cred.key;
31
+ if (process.env.DELIVERIES_API_KEY) return process.env.DELIVERIES_API_KEY;
32
+ if (process.env.DEERTOOLS_DELIVERIES_KEY) return process.env.DEERTOOLS_DELIVERIES_KEY;
33
+ return null;
34
+ }
35
+
36
+ function ensureDeliveriesAuth() {
37
+ const key = getDeliveriesKey();
38
+ if (!key) {
39
+ throw new Error('Brak klucza API do Dostaw. Zaloguj się poleceniem: deertools login deliveries');
40
+ }
41
+ return key;
42
+ }
43
+
44
+ async function forwardToDeliveriesMcp(req) {
45
+ const apiKey = ensureDeliveriesAuth();
46
+ const p = getProduct('deliveries');
47
+ const endpoint = p.mcpEndpoint || 'https://ddyfbvogkumvscckotvb.supabase.co/functions/v1/mcp-server';
48
+
49
+ const res = await fetch(endpoint, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'x-api-key': apiKey,
54
+ 'Authorization': `Bearer ${apiKey}`
55
+ },
56
+ body: JSON.stringify(req)
57
+ });
58
+
59
+ const text = await res.text();
60
+ let json = null;
61
+ try { json = JSON.parse(text); } catch (_) {}
62
+
63
+ if (!res.ok) {
64
+ const errorMsg = (json && (json.message || json.error)) || text;
65
+ return {
66
+ jsonrpc: '2.0',
67
+ id: req.id || null,
68
+ error: { code: -32000, message: `Dostawy MCP (${res.status}): ${errorMsg}` }
69
+ };
70
+ }
71
+
72
+ return json !== null ? json : { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text }] } };
73
+ }
74
+
75
+ async function handleMcpMessage(msg) {
76
+ if (!msg || typeof msg !== 'object') return null;
77
+ const { id, method } = msg;
78
+
79
+ if (method === 'initialize') {
80
+ return {
81
+ jsonrpc: '2.0',
82
+ id,
83
+ result: {
84
+ protocolVersion: PROTOCOL_VERSION,
85
+ capabilities: { tools: {} },
86
+ serverInfo: SERVER_INFO
87
+ }
88
+ };
89
+ }
90
+
91
+ if (method === 'notifications/initialized') {
92
+ return null;
93
+ }
94
+
95
+ if (method === 'tools/list') {
96
+ if (!getDeliveriesKey()) {
97
+ return { jsonrpc: '2.0', id, result: { tools: FALLBACK_DELIVERIES_TOOLS } };
98
+ }
99
+ try {
100
+ const upstream = await forwardToDeliveriesMcp(msg);
101
+ if (upstream && upstream.result && Array.isArray(upstream.result.tools)) {
102
+ return upstream;
103
+ }
104
+ } catch (_) {}
105
+ return { jsonrpc: '2.0', id, result: { tools: FALLBACK_DELIVERIES_TOOLS } };
106
+ }
107
+
108
+ // Forward tools/call to upstream
109
+ return await forwardToDeliveriesMcp(msg);
110
+ }
111
+
112
+ function runDeliveriesMcpServer() {
113
+ return new Promise((resolve) => {
114
+ let pendingCount = 0;
115
+ let isClosed = false;
116
+ let closeResolve = null;
117
+
118
+ const rl = readline.createInterface({
119
+ input: process.stdin,
120
+ output: process.stdout,
121
+ terminal: false
122
+ });
123
+
124
+ rl.on('line', async (line) => {
125
+ const trimmed = line.trim();
126
+ if (!trimmed) return;
127
+ pendingCount++;
128
+ try {
129
+ const msg = JSON.parse(trimmed);
130
+ const resp = await handleMcpMessage(msg);
131
+ if (resp) {
132
+ process.stdout.write(JSON.stringify(resp) + '\n');
133
+ }
134
+ } catch (e) {
135
+ process.stdout.write(JSON.stringify({
136
+ jsonrpc: '2.0',
137
+ id: null,
138
+ error: { code: -32700, message: `Parse error: ${e.message}` }
139
+ }) + '\n');
140
+ } finally {
141
+ pendingCount--;
142
+ if (isClosed && pendingCount === 0 && closeResolve) {
143
+ closeResolve();
144
+ }
145
+ }
146
+ });
147
+
148
+ rl.on('close', () => {
149
+ isClosed = true;
150
+ if (pendingCount === 0) {
151
+ resolve();
152
+ } else {
153
+ closeResolve = resolve;
154
+ }
155
+ });
156
+ process.on('SIGINT', () => { rl.close(); resolve(); });
157
+ process.on('SIGTERM', () => { rl.close(); resolve(); });
158
+ });
159
+ }
160
+
161
+ module.exports = {
162
+ handleMcpMessage,
163
+ runDeliveriesMcpServer
164
+ };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+
7
+ const CACHE_FILE = path.join(os.homedir(), '.deertools', 'connect_msal_token.json');
8
+ const STATE_PATH = '/Users/mariusz.szostak/.gemini/antigravity/brain/1bdbb6ae-0d30-48b7-8737-ff63d4d310d3/scratch/kosztorys_storage_state.json';
9
+ const BASE_URL = 'https://connect.deerdesign.pl/kosztorys';
10
+
11
+ /**
12
+ * Zwraca aktywny Bearer token do dev.deergroup.pl/api.
13
+ * 1. Sprawdza cache ~/.deertools/connect_msal_token.json
14
+ * 2. Jeśli brak lub wygasa za < 60 sek, odświeża w tle przez Playwright storage_state.
15
+ */
16
+ function getDevToken(forceRefresh = false) {
17
+ const now = Math.floor(Date.now() / 1000);
18
+
19
+ if (!forceRefresh && fs.existsSync(CACHE_FILE)) {
20
+ try {
21
+ const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
22
+ if (data && data.token && data.expires_at > now + 60) {
23
+ return data.token;
24
+ }
25
+ } catch (_) {
26
+ // Ignorujemy błędy odczytu i przechodzimy do odświeżenia
27
+ }
28
+ }
29
+
30
+ // Odświeżanie tokena przy użyciu Playwright z zapisanego stanu sesji
31
+ try {
32
+ const pythonScript = `
33
+ import os
34
+ import sys
35
+ import json
36
+ import time
37
+ import base64
38
+ from playwright.sync_api import sync_playwright
39
+
40
+ state_path = '${STATE_PATH}'
41
+ base_url = '${BASE_URL}'
42
+ cache_file = '${CACHE_FILE}'
43
+
44
+ if not os.path.exists(state_path):
45
+ sys.exit(1)
46
+
47
+ with sync_playwright() as p:
48
+ browser = p.chromium.launch(headless=True)
49
+ context = browser.new_context(storage_state=state_path)
50
+ page = context.new_page()
51
+ token = [None]
52
+ def on_req(req):
53
+ auth = req.headers.get("authorization")
54
+ if auth and auth.startswith("Bearer eyJ") and not token[0]:
55
+ token[0] = auth.replace("Bearer ", "")
56
+ page.on("request", on_req)
57
+ page.goto(f"{base_url}/projects", wait_until="networkidle")
58
+ browser.close()
59
+ t = token[0]
60
+ if t:
61
+ try:
62
+ payload_b64 = t.split('.')[1] + '=='
63
+ payload = json.loads(base64.urlsafe_b64decode(payload_b64))
64
+ exp = payload.get('exp', int(time.time() + 3600))
65
+ except Exception:
66
+ exp = int(time.time() + 3600)
67
+ os.makedirs(os.path.dirname(cache_file), exist_ok=True)
68
+ with open(cache_file, 'w') as f:
69
+ json.dump({'token': t, 'expires_at': exp, 'updated_at': int(time.time())}, f, indent=2)
70
+ print(t)
71
+ sys.exit(0)
72
+ sys.exit(2)
73
+ `;
74
+
75
+ const token = execFileSync('python3', ['-c', pythonScript], {
76
+ encoding: 'utf8',
77
+ timeout: 25000,
78
+ stdio: ['pipe', 'pipe', 'ignore']
79
+ }).trim();
80
+
81
+ if (token && token.startsWith('eyJ')) {
82
+ return token;
83
+ }
84
+ } catch (err) {
85
+ // Jeśli nie udało się odświeżyć, spróbuj użyć ostatniego zapisanego tokena z cache (nawet jeśli wygasł, backend może mieć tolerancję)
86
+ if (fs.existsSync(CACHE_FILE)) {
87
+ try {
88
+ const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
89
+ if (data && data.token) return data.token;
90
+ } catch (_) {}
91
+ }
92
+ throw new Error(`Błąd autoryzacji do dev.deergroup.pl: ${err.message}`);
93
+ }
94
+
95
+ throw new Error('Nie udało się uzyskać tokena autoryzacyjnego do dev.deergroup.pl.');
96
+ }
97
+
98
+ function ensureDevToken() {
99
+ const token = getDevToken();
100
+ if (!token) {
101
+ throw new Error('Brak aktywnej autoryzacji do dev.deergroup.pl. Uruchom odświeżenie sesji.');
102
+ }
103
+ return token;
104
+ }
105
+
106
+ module.exports = {
107
+ getDevToken,
108
+ ensureDevToken,
109
+ CACHE_FILE
110
+ };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ const client = require('./client');
3
+ const { runEstimateMcpServer } = require('./mcp-server');
4
+
5
+ async function cmdEstimate(pos, flags) {
6
+ const sub = pos[0] || 'help';
7
+
8
+ if (sub === 'mcp' || sub === 'serve') {
9
+ return await runEstimateMcpServer();
10
+ }
11
+
12
+ if (sub === 'projects' || sub === 'list-projects') {
13
+ const data = await client.listProjects({
14
+ search: flags.search || pos[1],
15
+ page: Number(flags.page) || 1,
16
+ pageSize: Number(flags.pageSize || flags.limit) || 20
17
+ });
18
+ if (flags.json) {
19
+ console.log(JSON.stringify(data, null, 2));
20
+ } else {
21
+ console.log(`Znaleziono projektów: ${data.total} (strona ${data.page}/${Math.ceil(data.total / data.pageSize)})`);
22
+ console.log('─'.repeat(70));
23
+ for (const p of data.items || []) {
24
+ console.log(`[ID ${p.id.padEnd(4)}] ${p.number ? p.number.padEnd(10) : ''} ${p.name}`);
25
+ }
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ if (sub === 'project' || sub === 'get-project') {
31
+ const id = pos[1] || flags.id;
32
+ if (!id) {
33
+ console.error('Błąd: Podaj ID projektu, np. deertools estimate project 707');
34
+ return 1;
35
+ }
36
+ const data = await client.getProject(id);
37
+ if (flags.json) {
38
+ console.log(JSON.stringify(data, null, 2));
39
+ } else {
40
+ console.log(`Projekt: ${data.name} (Nr: ${data.number || '-'})`);
41
+ console.log(`Status: ${data.propertyStatus || '-'} | Typ: ${data.projectType || '-'}`);
42
+ console.log(`Powiązane dokumenty (${data.documents.length}):`);
43
+ for (const d of data.documents) {
44
+ console.log(` • [ID ${d.id}] ${d.name} (${d.type})`);
45
+ }
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ if (sub === 'document' || sub === 'get-document') {
51
+ const id = pos[1] || flags.id;
52
+ if (!id) {
53
+ console.error('Błąd: Podaj ID dokumentu, np. deertools estimate document 1401');
54
+ return 1;
55
+ }
56
+ const data = await client.getDocument(id, { summaryOnly: Boolean(flags.summary) });
57
+ if (flags.json) {
58
+ console.log(JSON.stringify(data, null, 2));
59
+ } else if (flags.summary) {
60
+ console.log(JSON.stringify(data, null, 2));
61
+ } else {
62
+ console.log(`Dokument: ${data.name} (Typ: ${data.type}, Nr: ${data.documentNumber || '-'})`);
63
+ const items = data.dataJson?.estimatePanelData?.estimateItems || [];
64
+ const active = items.filter(x => !x.cancelled);
65
+ const reduced = items.filter(x => x.cancelled);
66
+ const totals = data.dataJson?.estimatePanelData?.totalPrices || {};
67
+ console.log(`Pozycji w kosztorysie: ${items.length} (aktywne: ${active.length}, redukcje: ${reduced.length})`);
68
+ console.log(`Pakiet: ${data.dataJson?.finishStandard?.selectedPackageName || '-'} | Netto: ${totals.totalNetPrice ?? 0} zł | Brutto: ${totals.totalGrossPrice ?? 0} zł`);
69
+ const rooms = new Set(items.map(it => it.group).filter(Boolean));
70
+ console.log(`Grupy pomieszczeń: ${Array.from(rooms).join(', ')}`);
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ if (sub === 'catalog' || sub === 'search-catalog') {
76
+ const data = await client.searchEstimateCatalog({
77
+ search: flags.search || pos[1],
78
+ type: flags.type,
79
+ categoryPathPrefix: flags.category,
80
+ packageFilter: flags.package,
81
+ priceMin: flags.priceMin != null ? Number(flags.priceMin) : undefined,
82
+ priceMax: flags.priceMax != null ? Number(flags.priceMax) : undefined,
83
+ unit: flags.unit,
84
+ documentId: flags.doc,
85
+ slotRowId: flags.slot,
86
+ unlockFilters: Boolean(flags.unlock),
87
+ page: Number(flags.page) || 1,
88
+ pageSize: Number(flags.limit) || 20
89
+ });
90
+ if (flags.json) {
91
+ console.log(JSON.stringify(data, null, 2));
92
+ } else {
93
+ console.log(`Wyników w Katalogu 5: ${data.total} (filtry: ${JSON.stringify(data.appliedFilters)})`);
94
+ for (const it of data.items || []) {
95
+ const badge = it.inPackageScope ? '[W PAKIECIE]' : '[DOPŁATA/ZAMIENNIK]';
96
+ console.log(` • [${it.id}] SKU:${it.sku} | ${it.name} | zak:${it.purchasePricePerPricingUnit ?? it.purchasePrice} zł | sprz.net:${it.computedUnitPriceNet} zł/${it.pricingUnit || it.unit} | pkg:${it.package || '-'} ${badge}`);
97
+ }
98
+ }
99
+ return 0;
100
+ }
101
+
102
+ if (sub === 'rooms') {
103
+ const id = pos[1] || flags.id || flags.doc;
104
+ if (!id) {
105
+ console.error('Błąd: Podaj ID dokumentu, np. deertools estimate rooms 1401');
106
+ return 1;
107
+ }
108
+ const data = await client.getRoomsAndMeasurements(id);
109
+ console.log(JSON.stringify(data, null, 2));
110
+ return 0;
111
+ }
112
+
113
+ if (sub === 'package') {
114
+ const id = pos[1] || flags.id || flags.doc;
115
+ if (!id) {
116
+ console.error('Błąd: Podaj ID dokumentu, np. deertools estimate package 1401');
117
+ return 1;
118
+ }
119
+ if (flags.set) {
120
+ const res = await client.setPackageAndVariants(id, {
121
+ package: flags.set,
122
+ dryRun: Boolean(flags['dry-run'])
123
+ });
124
+ console.log(JSON.stringify(res, null, 2));
125
+ } else {
126
+ const res = await client.getPackageAndVariants(id);
127
+ console.log(JSON.stringify(res, null, 2));
128
+ }
129
+ return 0;
130
+ }
131
+
132
+ if (sub === 'autofill') {
133
+ const id = pos[1] || flags.id || flags.doc;
134
+ if (!id) {
135
+ console.error('Błąd: Podaj ID dokumentu, np. deertools estimate autofill 1402');
136
+ return 1;
137
+ }
138
+ const res = await client.autoFillTemplateSlots(id, {
139
+ documentName: flags.name,
140
+ dryRun: Boolean(flags['dry-run'])
141
+ });
142
+ console.log(JSON.stringify(res, null, 2));
143
+ return 0;
144
+ }
145
+
146
+ console.log(`Użycie deertools estimate:
147
+ deertools estimate mcp - Uruchamia lokalny serwer MCP Kosztorysu (stdio)
148
+ deertools estimate projects [--search <fraza>] - Wyświetla listę projektów
149
+ deertools estimate project <id> - Wyświetla szczegóły projektu i jego dokumenty
150
+ deertools estimate document <id> [--summary] - Pobiera dane dokumentu kosztorysu
151
+ deertools estimate catalog [<fraza>] [--package smart] - Wyszukuje produkty/usługi w Katalogu 5 Kosztorysu
152
+ deertools estimate rooms <docId> - Wyświetla obmiary pomieszczeń i mapowania pakietowe
153
+ deertools estimate package <docId> [--set SMART|MODERN|HIGH] - Pobiera lub zmienia pakiet i warianty kosztorysu
154
+ deertools estimate autofill <docId> [--name <nazwa>] - Automatycznie uzupełnia puste sloty produktami pakietowymi
155
+ `);
156
+ return 0;
157
+ }
158
+
159
+ module.exports = {
160
+ cmdEstimate
161
+ };