@jackwener/opencli 0.5.2 → 0.6.1
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 +23 -7
- package/README.zh-CN.md +24 -8
- package/SKILL.md +6 -2
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/main.js +7 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/package.json +1 -1
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/main.ts +8 -0
- package/src/setup.ts +169 -0
- package/src/tui.ts +171 -0
package/dist/coupang.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
function itemKey(item) {
|
|
2
|
+
return item.url || item.product_id || `${item.title}:${item.price ?? ''}`;
|
|
3
|
+
}
|
|
4
|
+
const ROCKET_PATTERNS = ['판매자로켓', '로켓프레시', '로켓와우', '로켓배송', '로켓직구'];
|
|
5
|
+
const DELIVERY_TYPE_PATTERNS = ['무료배송', '일반배송'];
|
|
6
|
+
const DELIVERY_PROMISE_PATTERNS = ['오늘도착', '내일도착', '새벽도착', '오늘출발'];
|
|
7
|
+
const BADGE_ID_TO_ROCKET = {
|
|
8
|
+
ROCKET: '로켓배송',
|
|
9
|
+
ROCKET_MERCHANT: '판매자로켓',
|
|
10
|
+
ROCKET_WOW: '로켓와우',
|
|
11
|
+
WOW: '로켓와우',
|
|
12
|
+
ROCKET_FRESH: '로켓프레시',
|
|
13
|
+
FRESH: '로켓프레시',
|
|
14
|
+
SELLER_ROCKET: '판매자로켓',
|
|
15
|
+
ROCKET_JIKGU: '로켓직구',
|
|
16
|
+
JIKGU: '로켓직구',
|
|
17
|
+
COUPANG_GLOBAL: '로켓직구',
|
|
18
|
+
};
|
|
19
|
+
const BADGE_ID_TO_PROMISE = {
|
|
20
|
+
DAWN: '새벽도착',
|
|
21
|
+
EARLY_DAWN: '새벽도착',
|
|
22
|
+
TOMORROW: '내일도착',
|
|
23
|
+
TODAY: '오늘도착',
|
|
24
|
+
SAME_DAY: '오늘도착',
|
|
25
|
+
TODAY_SHIP: '오늘출발',
|
|
26
|
+
TODAY_DISPATCH: '오늘출발',
|
|
27
|
+
};
|
|
28
|
+
function asString(value) {
|
|
29
|
+
if (value == null)
|
|
30
|
+
return '';
|
|
31
|
+
return String(value).trim();
|
|
32
|
+
}
|
|
33
|
+
function toNumber(value) {
|
|
34
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
35
|
+
return value;
|
|
36
|
+
const text = asString(value).replace(/[^\d.]/g, '');
|
|
37
|
+
if (!text)
|
|
38
|
+
return null;
|
|
39
|
+
const num = Number(text);
|
|
40
|
+
return Number.isFinite(num) ? num : null;
|
|
41
|
+
}
|
|
42
|
+
function pickFirst(obj, paths) {
|
|
43
|
+
for (const path of paths) {
|
|
44
|
+
const parts = path.split('.');
|
|
45
|
+
let current = obj;
|
|
46
|
+
let ok = true;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (!current || typeof current !== 'object' || !(part in current)) {
|
|
49
|
+
ok = false;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
current = current[part];
|
|
53
|
+
}
|
|
54
|
+
if (ok && current != null && asString(current) !== '')
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
export function normalizeProductId(raw) {
|
|
60
|
+
const text = asString(raw);
|
|
61
|
+
if (!text)
|
|
62
|
+
return '';
|
|
63
|
+
const match = text.match(/\/vp\/products\/(\d+)/) || text.match(/\b(\d{6,})\b/);
|
|
64
|
+
return match?.[1] ?? text;
|
|
65
|
+
}
|
|
66
|
+
export function canonicalizeProductUrl(rawUrl, productId) {
|
|
67
|
+
const raw = asString(rawUrl);
|
|
68
|
+
if (raw) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(raw.startsWith('http') ? raw : `https://www.coupang.com${raw}`);
|
|
71
|
+
if (!url.hostname.includes('coupang.com'))
|
|
72
|
+
return '';
|
|
73
|
+
const id = normalizeProductId(url.pathname) || normalizeProductId(productId);
|
|
74
|
+
if (!id)
|
|
75
|
+
return url.toString();
|
|
76
|
+
return `https://www.coupang.com/vp/products/${id}`;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const id = normalizeProductId(productId);
|
|
83
|
+
return id ? `https://www.coupang.com/vp/products/${id}` : '';
|
|
84
|
+
}
|
|
85
|
+
function extractTokens(values) {
|
|
86
|
+
return values
|
|
87
|
+
.flatMap((value) => {
|
|
88
|
+
const text = asString(value);
|
|
89
|
+
if (!text)
|
|
90
|
+
return [];
|
|
91
|
+
return text.split(/[,\s|]+/);
|
|
92
|
+
})
|
|
93
|
+
.map((token) => token.trim().toUpperCase())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
function normalizeJoinedText(...values) {
|
|
97
|
+
return values
|
|
98
|
+
.map(asString)
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join(' ')
|
|
101
|
+
.replace(/schema\.org\/[A-Za-z]+/gi, ' ')
|
|
102
|
+
.replace(/\s+/g, ' ')
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
|
105
|
+
function normalizeRocket(...values) {
|
|
106
|
+
const tokens = extractTokens(values);
|
|
107
|
+
for (const token of tokens) {
|
|
108
|
+
if (BADGE_ID_TO_ROCKET[token])
|
|
109
|
+
return BADGE_ID_TO_ROCKET[token];
|
|
110
|
+
}
|
|
111
|
+
const text = normalizeJoinedText(...values);
|
|
112
|
+
if (!text)
|
|
113
|
+
return '';
|
|
114
|
+
if (/판매자\s*로켓/.test(text))
|
|
115
|
+
return '판매자로켓';
|
|
116
|
+
if (/로켓\s*프레시|새벽\s*도착\s*보장/.test(text))
|
|
117
|
+
return '로켓프레시';
|
|
118
|
+
if (/로켓\s*와우/.test(text))
|
|
119
|
+
return '로켓와우';
|
|
120
|
+
if (/로켓\s*직구|직구/.test(text))
|
|
121
|
+
return '로켓직구';
|
|
122
|
+
if (/로켓\s*배송/.test(text))
|
|
123
|
+
return '로켓배송';
|
|
124
|
+
return ROCKET_PATTERNS.find(pattern => text.includes(pattern)) ?? '';
|
|
125
|
+
}
|
|
126
|
+
function normalizeDeliveryType(...values) {
|
|
127
|
+
const text = normalizeJoinedText(...values);
|
|
128
|
+
if (!text)
|
|
129
|
+
return '';
|
|
130
|
+
if (/무료\s*배송/.test(text))
|
|
131
|
+
return '무료배송';
|
|
132
|
+
if (/일반\s*배송/.test(text))
|
|
133
|
+
return '일반배송';
|
|
134
|
+
return DELIVERY_TYPE_PATTERNS.find(pattern => text.includes(pattern)) ?? '';
|
|
135
|
+
}
|
|
136
|
+
function normalizeDeliveryPromise(...values) {
|
|
137
|
+
const tokens = extractTokens(values);
|
|
138
|
+
for (const token of tokens) {
|
|
139
|
+
if (BADGE_ID_TO_PROMISE[token])
|
|
140
|
+
return BADGE_ID_TO_PROMISE[token];
|
|
141
|
+
}
|
|
142
|
+
const text = normalizeJoinedText(...values);
|
|
143
|
+
if (!text)
|
|
144
|
+
return '';
|
|
145
|
+
if (/오늘\s*출발/.test(text))
|
|
146
|
+
return '오늘출발';
|
|
147
|
+
if (/오늘.*도착/.test(text))
|
|
148
|
+
return '오늘도착';
|
|
149
|
+
if (/새벽.*도착/.test(text))
|
|
150
|
+
return '새벽도착';
|
|
151
|
+
if (/내일.*도착/.test(text))
|
|
152
|
+
return '내일도착';
|
|
153
|
+
return DELIVERY_PROMISE_PATTERNS.find(pattern => text.includes(pattern)) ?? '';
|
|
154
|
+
}
|
|
155
|
+
function normalizeBadge(value) {
|
|
156
|
+
const normalizeOne = (entry) => {
|
|
157
|
+
const text = asString(entry);
|
|
158
|
+
if (!text)
|
|
159
|
+
return '';
|
|
160
|
+
if (/schema\.org\//i.test(text)) {
|
|
161
|
+
return text.split('/').pop() ?? '';
|
|
162
|
+
}
|
|
163
|
+
return text;
|
|
164
|
+
};
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
return value.map(normalizeOne).filter(Boolean).join(', ');
|
|
167
|
+
}
|
|
168
|
+
return normalizeOne(value);
|
|
169
|
+
}
|
|
170
|
+
export function normalizeSearchItem(raw, index) {
|
|
171
|
+
const productId = normalizeProductId(pickFirst(raw, ['productId', 'product_id', 'id', 'productNo', 'item.id', 'product.productId', 'url']));
|
|
172
|
+
const title = asString(pickFirst(raw, ['title', 'name', 'productName', 'productTitle', 'itemName', 'item.title']));
|
|
173
|
+
const price = toNumber(pickFirst(raw, ['price', 'salePrice', 'finalPrice', 'sellingPrice', 'discountPrice', 'item.price']));
|
|
174
|
+
const originalPrice = toNumber(pickFirst(raw, ['originalPrice', 'basePrice', 'listPrice', 'originPrice', 'strikePrice']));
|
|
175
|
+
const unitPrice = asString(pickFirst(raw, ['unitPrice', 'unit_price', 'unitPriceText']));
|
|
176
|
+
const rating = toNumber(pickFirst(raw, ['rating', 'star', 'reviewRating', 'review.rating', 'item.rating']));
|
|
177
|
+
const reviewCount = toNumber(pickFirst(raw, ['reviewCount', 'ratingCount', 'reviews', 'reviewCnt', 'item.reviewCount']));
|
|
178
|
+
const deliveryHintValues = [
|
|
179
|
+
pickFirst(raw, ['deliveryType', 'deliveryBadge', 'badgeLabel', 'shippingType', 'shippingBadge']),
|
|
180
|
+
pickFirst(raw, ['badge', 'badges', 'labels', 'benefitBadge', 'promotionBadge']),
|
|
181
|
+
pickFirst(raw, ['text', 'summary']),
|
|
182
|
+
pickFirst(raw, ['deliveryPromise', 'promise', 'arrivalText', 'arrivalBadge']),
|
|
183
|
+
pickFirst(raw, ['rocket', 'rocketType']),
|
|
184
|
+
];
|
|
185
|
+
const deliveryType = normalizeDeliveryType(...deliveryHintValues);
|
|
186
|
+
const deliveryPromise = normalizeDeliveryPromise(...deliveryHintValues);
|
|
187
|
+
const rocket = normalizeRocket(...deliveryHintValues);
|
|
188
|
+
const badge = normalizeBadge(pickFirst(raw, ['badge', 'badges', 'labels', 'benefitBadge', 'promotionBadge']));
|
|
189
|
+
const category = asString(pickFirst(raw, ['category', 'categoryName', 'categoryPath', 'item.category']));
|
|
190
|
+
const seller = asString(pickFirst(raw, ['seller', 'sellerName', 'vendorName', 'merchantName', 'item.seller']));
|
|
191
|
+
const url = canonicalizeProductUrl(pickFirst(raw, ['url', 'productUrl', 'link', 'item.url']), productId);
|
|
192
|
+
const discountRate = toNumber(pickFirst(raw, ['discountRate', 'discount', 'discountPercent', 'discount_rate']));
|
|
193
|
+
return {
|
|
194
|
+
rank: index + 1,
|
|
195
|
+
product_id: productId,
|
|
196
|
+
title,
|
|
197
|
+
price,
|
|
198
|
+
original_price: originalPrice,
|
|
199
|
+
unit_price: unitPrice,
|
|
200
|
+
discount_rate: discountRate,
|
|
201
|
+
rating,
|
|
202
|
+
review_count: reviewCount,
|
|
203
|
+
rocket,
|
|
204
|
+
delivery_type: deliveryType,
|
|
205
|
+
delivery_promise: deliveryPromise,
|
|
206
|
+
seller,
|
|
207
|
+
badge,
|
|
208
|
+
category,
|
|
209
|
+
url,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export function dedupeSearchItems(items) {
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
const key = itemKey(item);
|
|
217
|
+
if (!key || seen.has(key))
|
|
218
|
+
continue;
|
|
219
|
+
seen.add(key);
|
|
220
|
+
out.push({ ...item, rank: out.length + 1 });
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
export function sanitizeSearchItems(items, limit) {
|
|
225
|
+
return dedupeSearchItems(items.filter(item => Boolean(item.title && (item.product_id || item.url)))).slice(0, limit);
|
|
226
|
+
}
|
|
227
|
+
export function mergeSearchItems(base, extra, limit) {
|
|
228
|
+
const extraMap = new Map();
|
|
229
|
+
for (const item of extra) {
|
|
230
|
+
const key = itemKey(item);
|
|
231
|
+
if (key)
|
|
232
|
+
extraMap.set(key, item);
|
|
233
|
+
}
|
|
234
|
+
const merged = base.map((item) => {
|
|
235
|
+
const key = itemKey(item);
|
|
236
|
+
const patch = key ? extraMap.get(key) : null;
|
|
237
|
+
if (!patch)
|
|
238
|
+
return item;
|
|
239
|
+
return {
|
|
240
|
+
...item,
|
|
241
|
+
price: patch.price ?? item.price,
|
|
242
|
+
original_price: patch.original_price ?? item.original_price,
|
|
243
|
+
unit_price: patch.unit_price || item.unit_price,
|
|
244
|
+
discount_rate: patch.discount_rate ?? item.discount_rate,
|
|
245
|
+
rating: patch.rating ?? item.rating,
|
|
246
|
+
review_count: patch.review_count ?? item.review_count,
|
|
247
|
+
rocket: patch.rocket || item.rocket,
|
|
248
|
+
delivery_type: patch.delivery_type || item.delivery_type,
|
|
249
|
+
delivery_promise: patch.delivery_promise || item.delivery_promise,
|
|
250
|
+
seller: patch.seller || item.seller,
|
|
251
|
+
badge: patch.badge || item.badge,
|
|
252
|
+
category: patch.category || item.category,
|
|
253
|
+
url: patch.url || item.url,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
const mergedKeys = new Set(merged.map(item => itemKey(item)).filter(Boolean));
|
|
257
|
+
const appended = extra.filter(item => {
|
|
258
|
+
const key = itemKey(item);
|
|
259
|
+
return key && !mergedKeys.has(key);
|
|
260
|
+
});
|
|
261
|
+
return sanitizeSearchItems([...merged, ...appended], limit);
|
|
262
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { canonicalizeProductUrl, dedupeSearchItems, normalizeProductId, normalizeSearchItem, sanitizeSearchItems, } from './coupang.js';
|
|
3
|
+
describe('normalizeProductId', () => {
|
|
4
|
+
it('extracts product id from canonical path', () => {
|
|
5
|
+
expect(normalizeProductId('https://www.coupang.com/vp/products/123456789')).toBe('123456789');
|
|
6
|
+
});
|
|
7
|
+
it('preserves numeric ids', () => {
|
|
8
|
+
expect(normalizeProductId('987654321')).toBe('987654321');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe('canonicalizeProductUrl', () => {
|
|
12
|
+
it('normalizes relative Coupang paths', () => {
|
|
13
|
+
expect(canonicalizeProductUrl('/vp/products/123456789?itemId=1', '')).toBe('https://www.coupang.com/vp/products/123456789');
|
|
14
|
+
});
|
|
15
|
+
it('builds url from product id', () => {
|
|
16
|
+
expect(canonicalizeProductUrl('', '123456789')).toBe('https://www.coupang.com/vp/products/123456789');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('normalizeSearchItem', () => {
|
|
20
|
+
it('maps raw fields into compare-ready shape', () => {
|
|
21
|
+
const item = normalizeSearchItem({
|
|
22
|
+
productId: '123456789',
|
|
23
|
+
productName: '무선 마우스',
|
|
24
|
+
salePrice: '29,900원',
|
|
25
|
+
originalPrice: '39,900원',
|
|
26
|
+
rating: '4.8',
|
|
27
|
+
reviewCount: '1,234',
|
|
28
|
+
sellerName: '쿠팡',
|
|
29
|
+
badge: ['ROCKET', 'TOMORROW', '무료배송'],
|
|
30
|
+
categoryName: 'PC',
|
|
31
|
+
url: '/vp/products/123456789?itemId=1',
|
|
32
|
+
}, 0);
|
|
33
|
+
expect(item).toMatchObject({
|
|
34
|
+
rank: 1,
|
|
35
|
+
product_id: '123456789',
|
|
36
|
+
title: '무선 마우스',
|
|
37
|
+
price: 29900,
|
|
38
|
+
original_price: 39900,
|
|
39
|
+
rating: 4.8,
|
|
40
|
+
review_count: 1234,
|
|
41
|
+
rocket: '로켓배송',
|
|
42
|
+
delivery_type: '무료배송',
|
|
43
|
+
delivery_promise: '내일도착',
|
|
44
|
+
seller: '쿠팡',
|
|
45
|
+
category: 'PC',
|
|
46
|
+
url: 'https://www.coupang.com/vp/products/123456789',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('sanitizeSearchItems', () => {
|
|
51
|
+
it('drops duplicates and invalid rows', () => {
|
|
52
|
+
const rows = [
|
|
53
|
+
normalizeSearchItem({ productId: '1', productName: 'A', price: '1000', url: '/vp/products/1' }, 0),
|
|
54
|
+
normalizeSearchItem({ productId: '1', productName: 'A', price: '1000', url: '/vp/products/1' }, 1),
|
|
55
|
+
normalizeSearchItem({ productId: '', productName: '', price: '1000' }, 2),
|
|
56
|
+
normalizeSearchItem({ productId: '2', productName: 'B', price: '2000', url: '/vp/products/2' }, 3),
|
|
57
|
+
];
|
|
58
|
+
expect(dedupeSearchItems(rows)).toHaveLength(3);
|
|
59
|
+
expect(sanitizeSearchItems(rows, 10)).toHaveLength(2);
|
|
60
|
+
expect(sanitizeSearchItems(rows, 10).map(item => item.rank)).toEqual([1, 2]);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const PLAYWRIGHT_TOKEN_ENV = "PLAYWRIGHT_MCP_EXTENSION_TOKEN";
|
|
1
2
|
export type DoctorOptions = {
|
|
2
3
|
fix?: boolean;
|
|
3
4
|
yes?: boolean;
|
|
@@ -26,6 +27,8 @@ export type DoctorReport = {
|
|
|
26
27
|
cliVersion?: string;
|
|
27
28
|
envToken: string | null;
|
|
28
29
|
envFingerprint: string | null;
|
|
30
|
+
extensionToken: string | null;
|
|
31
|
+
extensionFingerprint: string | null;
|
|
29
32
|
shellFiles: ShellFileStatus[];
|
|
30
33
|
configs: McpConfigStatus[];
|
|
31
34
|
recommendedToken: string | null;
|
|
@@ -33,6 +36,8 @@ export type DoctorReport = {
|
|
|
33
36
|
warnings: string[];
|
|
34
37
|
issues: string[];
|
|
35
38
|
};
|
|
39
|
+
export declare function shortenPath(p: string): string;
|
|
40
|
+
export declare function toolName(p: string): string;
|
|
36
41
|
export declare function getDefaultShellRcPath(): string;
|
|
37
42
|
export declare function getDefaultMcpConfigPaths(cwd?: string): string[];
|
|
38
43
|
export declare function readTokenFromShellContent(content: string): string | null;
|
|
@@ -40,6 +45,16 @@ export declare function upsertShellToken(content: string, token: string): string
|
|
|
40
45
|
export declare function upsertJsonConfigToken(content: string, token: string): string;
|
|
41
46
|
export declare function readTomlConfigToken(content: string): string | null;
|
|
42
47
|
export declare function upsertTomlConfigToken(content: string, token: string): string;
|
|
48
|
+
export declare function fileExists(filePath: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Discover the auth token stored by the Playwright MCP Bridge extension
|
|
51
|
+
* by scanning Chrome's LevelDB localStorage files directly.
|
|
52
|
+
*
|
|
53
|
+
* Uses `strings` + `grep` for fast binary scanning on macOS/Linux,
|
|
54
|
+
* with a pure-Node fallback on Windows.
|
|
55
|
+
*/
|
|
56
|
+
export declare function discoverExtensionToken(): string | null;
|
|
43
57
|
export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
|
|
44
58
|
export declare function renderBrowserDoctorReport(report: DoctorReport): string;
|
|
59
|
+
export declare function writeFileWithMkdir(filePath: string, content: string): void;
|
|
45
60
|
export declare function applyBrowserDoctorFix(report: DoctorReport, opts?: DoctorOptions): Promise<string[]>;
|