@jlcpcb/core 0.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +474 -0
  3. package/package.json +48 -0
  4. package/src/api/easyeda-community.ts +259 -0
  5. package/src/api/easyeda.ts +153 -0
  6. package/src/api/index.ts +7 -0
  7. package/src/api/jlc.ts +185 -0
  8. package/src/constants/design-rules.ts +119 -0
  9. package/src/constants/footprints.ts +68 -0
  10. package/src/constants/index.ts +7 -0
  11. package/src/constants/kicad.ts +147 -0
  12. package/src/converter/category-router.ts +638 -0
  13. package/src/converter/footprint-mapper.ts +236 -0
  14. package/src/converter/footprint.ts +949 -0
  15. package/src/converter/global-lib-table.ts +394 -0
  16. package/src/converter/index.ts +46 -0
  17. package/src/converter/lib-table.ts +181 -0
  18. package/src/converter/svg-arc.ts +179 -0
  19. package/src/converter/symbol-templates.ts +214 -0
  20. package/src/converter/symbol.ts +1682 -0
  21. package/src/converter/value-normalizer.ts +262 -0
  22. package/src/index.ts +25 -0
  23. package/src/parsers/easyeda-shapes.ts +628 -0
  24. package/src/parsers/http-client.ts +96 -0
  25. package/src/parsers/index.ts +38 -0
  26. package/src/parsers/utils.ts +29 -0
  27. package/src/services/component-service.ts +100 -0
  28. package/src/services/fix-service.ts +50 -0
  29. package/src/services/index.ts +9 -0
  30. package/src/services/library-service.ts +696 -0
  31. package/src/types/component.ts +61 -0
  32. package/src/types/easyeda-community.ts +78 -0
  33. package/src/types/easyeda.ts +356 -0
  34. package/src/types/index.ts +12 -0
  35. package/src/types/jlc.ts +84 -0
  36. package/src/types/kicad.ts +136 -0
  37. package/src/types/mcp.ts +77 -0
  38. package/src/types/project.ts +60 -0
  39. package/src/utils/conversion.ts +104 -0
  40. package/src/utils/file-system.ts +143 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +96 -0
  43. package/src/utils/validation.ts +110 -0
  44. package/tsconfig.json +9 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * EasyEDA Community Library API client
3
+ * For searching and fetching user-contributed components
4
+ *
5
+ * Uses shared parsers from common/parsers for all shape types.
6
+ */
7
+
8
+ import type {
9
+ EasyEDACommunitySearchParams,
10
+ EasyEDACommunitySearchResult,
11
+ EasyEDACommunityComponent,
12
+ } from '../types/index.js'
13
+ import { createLogger } from '../utils/index.js'
14
+ import {
15
+ fetchWithCurlFallback,
16
+ parseSymbolShapes,
17
+ parseFootprintShapes,
18
+ } from '../parsers/index.js'
19
+
20
+ const logger = createLogger('easyeda-community-api')
21
+
22
+ const API_SEARCH_ENDPOINT = 'https://easyeda.com/api/components/search'
23
+ const API_COMPONENT_ENDPOINT = 'https://easyeda.com/api/components'
24
+ const API_VERSION = '6.5.51'
25
+
26
+ // Reuse 3D model endpoints from existing easyeda client
27
+ const ENDPOINT_3D_MODEL_STEP = 'https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{uuid}'
28
+ const ENDPOINT_3D_MODEL_OBJ = 'https://modules.easyeda.com/3dmodel/{uuid}'
29
+
30
+ export class EasyEDACommunityClient {
31
+ /**
32
+ * Search the EasyEDA community library
33
+ */
34
+ async search(
35
+ params: EasyEDACommunitySearchParams
36
+ ): Promise<EasyEDACommunitySearchResult[]> {
37
+ const formData = new URLSearchParams()
38
+ formData.append('type', '3') // Component type
39
+ formData.append('doctype[]', '2') // Symbol+footprint
40
+ formData.append('uid', params.source || 'user')
41
+ formData.append('returnListStyle', 'classifyarr')
42
+ formData.append('wd', params.query)
43
+ formData.append('version', API_VERSION)
44
+
45
+ logger.debug(`Searching EasyEDA community: ${params.query}`)
46
+
47
+ try {
48
+ const responseText = (await fetchWithCurlFallback(API_SEARCH_ENDPOINT, {
49
+ method: 'POST',
50
+ body: formData.toString(),
51
+ contentType: 'application/x-www-form-urlencoded',
52
+ })) as string
53
+
54
+ const data = JSON.parse(responseText)
55
+
56
+ if (!data.success || !data.result) {
57
+ logger.warn('EasyEDA search returned no results')
58
+ return []
59
+ }
60
+
61
+ return this.parseSearchResults(data.result.lists, params.limit)
62
+ } catch (error) {
63
+ logger.error('EasyEDA search failed:', error)
64
+ throw error
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get full component details by UUID
70
+ */
71
+ async getComponent(uuid: string): Promise<EasyEDACommunityComponent | null> {
72
+ const url = `${API_COMPONENT_ENDPOINT}/${uuid}?version=${API_VERSION}&uuid=${uuid}`
73
+
74
+ logger.debug(`Fetching component: ${uuid}`)
75
+
76
+ try {
77
+ const responseText = (await fetchWithCurlFallback(url)) as string
78
+ const data = JSON.parse(responseText)
79
+
80
+ if (!data.success || !data.result) {
81
+ return null
82
+ }
83
+
84
+ return this.parseComponent(data.result)
85
+ } catch (error) {
86
+ logger.error(`Failed to fetch component ${uuid}:`, error)
87
+ throw error
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Download 3D model for a component
93
+ */
94
+ async get3DModel(
95
+ uuid: string,
96
+ format: 'step' | 'obj' = 'step'
97
+ ): Promise<Buffer | null> {
98
+ const url =
99
+ format === 'step'
100
+ ? ENDPOINT_3D_MODEL_STEP.replace('{uuid}', uuid)
101
+ : ENDPOINT_3D_MODEL_OBJ.replace('{uuid}', uuid)
102
+
103
+ try {
104
+ const result = await fetchWithCurlFallback(url, { binary: true })
105
+ return result as Buffer
106
+ } catch {
107
+ return null
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Parse search results from the API response
113
+ */
114
+ private parseSearchResults(
115
+ lists: Record<string, unknown[]>,
116
+ limit?: number
117
+ ): EasyEDACommunitySearchResult[] {
118
+ const results: EasyEDACommunitySearchResult[] = []
119
+
120
+ // Process all source lists (user, lcsc, easyeda, etc.)
121
+ for (const [_source, items] of Object.entries(lists)) {
122
+ if (!Array.isArray(items)) continue
123
+
124
+ for (const item of items) {
125
+ const result = this.parseSearchItem(item)
126
+ if (result) {
127
+ results.push(result)
128
+ }
129
+
130
+ if (limit && results.length >= limit) {
131
+ return results
132
+ }
133
+ }
134
+ }
135
+
136
+ return results
137
+ }
138
+
139
+ /**
140
+ * Parse a single search result item
141
+ */
142
+ private parseSearchItem(item: any): EasyEDACommunitySearchResult | null {
143
+ try {
144
+ const cPara = item.dataStr?.head?.c_para || {}
145
+ const puuid = item.dataStr?.head?.puuid // Package/footprint UUID
146
+
147
+ return {
148
+ uuid: item.uuid || '',
149
+ title: item.title || '',
150
+ thumb: item.thumb || '',
151
+ description: item.description || '',
152
+ tags: item.tags || [],
153
+ package: cPara.package || '',
154
+ packageUuid: puuid || undefined,
155
+ manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer || undefined,
156
+ owner: {
157
+ uuid: item.owner?.uuid || '',
158
+ username: item.owner?.username || '',
159
+ nickname: item.owner?.nickname || '',
160
+ avatar: item.owner?.avatar,
161
+ team: item.owner?.team,
162
+ },
163
+ contributor: cPara.Contributor,
164
+ has3DModel: false, // Will be determined when fetching full component
165
+ docType: item.docType || 2,
166
+ updateTime: item.dataStr?.head?.utime,
167
+ }
168
+ } catch {
169
+ return null
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Parse full component data from the API response
175
+ * Uses shared parsers for consistent shape parsing with LCSC client.
176
+ */
177
+ private parseComponent(result: any): EasyEDACommunityComponent {
178
+ const dataStr = result.dataStr || {}
179
+ const packageDetail = result.packageDetail || {}
180
+ const cPara = dataStr.head?.c_para || {}
181
+
182
+ // Parse symbol shapes using shared parser
183
+ const symbolData = parseSymbolShapes(dataStr.shape || [])
184
+
185
+ // Parse footprint shapes using shared parser
186
+ const fpDataStr = packageDetail.dataStr || {}
187
+ const fpCPara = fpDataStr.head?.c_para || {}
188
+ const footprintData = parseFootprintShapes(fpDataStr.shape || [])
189
+
190
+ // Get origins for coordinate normalization
191
+ const symbolOrigin = {
192
+ x: parseFloat(dataStr.head?.x) || 0,
193
+ y: parseFloat(dataStr.head?.y) || 0,
194
+ }
195
+ const footprintOrigin = {
196
+ x: parseFloat(fpDataStr.head?.x) || 0,
197
+ y: parseFloat(fpDataStr.head?.y) || 0,
198
+ }
199
+
200
+ return {
201
+ uuid: result.uuid || '',
202
+ title: result.title || cPara.name || '',
203
+ description: result.description || '',
204
+ tags: result.tags || [],
205
+ owner: {
206
+ uuid: result.owner?.uuid || '',
207
+ username: result.owner?.username || '',
208
+ nickname: result.owner?.nickname || '',
209
+ avatar: result.owner?.avatar,
210
+ team: result.owner?.team,
211
+ },
212
+ creator: result.creator
213
+ ? {
214
+ uuid: result.creator.uuid || '',
215
+ username: result.creator.username || '',
216
+ nickname: result.creator.nickname || '',
217
+ avatar: result.creator.avatar,
218
+ team: result.creator.team,
219
+ }
220
+ : undefined,
221
+ updateTime: result.updateTime || fpDataStr.head?.utime || 0,
222
+ docType: result.docType || 2,
223
+ verify: result.verify || false,
224
+ symbol: {
225
+ pins: symbolData.pins,
226
+ rectangles: symbolData.rectangles,
227
+ circles: symbolData.circles,
228
+ ellipses: symbolData.ellipses,
229
+ arcs: symbolData.arcs,
230
+ polylines: symbolData.polylines,
231
+ polygons: symbolData.polygons,
232
+ paths: symbolData.paths,
233
+ origin: symbolOrigin,
234
+ head: dataStr.head || {},
235
+ },
236
+ footprint: {
237
+ uuid: packageDetail.uuid || '',
238
+ name: fpCPara.package || packageDetail.title || 'Unknown',
239
+ type: footprintData.type,
240
+ pads: footprintData.pads,
241
+ tracks: footprintData.tracks,
242
+ holes: footprintData.holes,
243
+ circles: footprintData.circles,
244
+ arcs: footprintData.arcs,
245
+ rects: footprintData.rects,
246
+ texts: footprintData.texts,
247
+ vias: footprintData.vias,
248
+ solidRegions: footprintData.solidRegions,
249
+ model3d: footprintData.model3d,
250
+ origin: footprintOrigin,
251
+ head: fpDataStr.head || {},
252
+ },
253
+ model3d: footprintData.model3d,
254
+ rawData: result,
255
+ }
256
+ }
257
+ }
258
+
259
+ export const easyedaCommunityClient = new EasyEDACommunityClient()
@@ -0,0 +1,153 @@
1
+ /**
2
+ * EasyEDA API client for component library fetching
3
+ *
4
+ * Uses shared parsers from common/parsers for all shape types.
5
+ */
6
+
7
+ import type { EasyEDAComponentData } from '../types/index.js';
8
+ import { createLogger } from '../utils/index.js';
9
+ import {
10
+ fetchWithCurlFallback,
11
+ parseSymbolShapes,
12
+ parseFootprintShapes,
13
+ } from '../parsers/index.js';
14
+
15
+ const logger = createLogger('easyeda-api');
16
+
17
+ const API_ENDPOINT = 'https://easyeda.com/api/products/{lcsc_id}/components?version=6.4.19.5';
18
+ const ENDPOINT_3D_MODEL_STEP = 'https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/{uuid}';
19
+ const ENDPOINT_3D_MODEL_OBJ = 'https://modules.easyeda.com/3dmodel/{uuid}';
20
+
21
+ export class EasyEDAClient {
22
+ private userAgent = 'ai-eda-lcsc-mcp/1.0.0';
23
+
24
+ /**
25
+ * Fetch component data from EasyEDA API
26
+ */
27
+ async getComponentData(lcscPartNumber: string): Promise<EasyEDAComponentData | null> {
28
+ const url = API_ENDPOINT.replace('{lcsc_id}', lcscPartNumber);
29
+
30
+ logger.debug(`Fetching component data for: ${lcscPartNumber}`);
31
+
32
+ try {
33
+ const responseText = await fetchWithCurlFallback(url) as string;
34
+ const data = JSON.parse(responseText);
35
+
36
+ if (!data.result) {
37
+ return null;
38
+ }
39
+
40
+ return this.parseComponentData(data.result, lcscPartNumber);
41
+ } catch (error) {
42
+ logger.error(`Failed to fetch component ${lcscPartNumber}:`, error);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Download 3D model for a component
49
+ */
50
+ async get3DModel(uuid: string, format: 'step' | 'obj' = 'step'): Promise<Buffer | null> {
51
+ const url = format === 'step'
52
+ ? ENDPOINT_3D_MODEL_STEP.replace('{uuid}', uuid)
53
+ : ENDPOINT_3D_MODEL_OBJ.replace('{uuid}', uuid);
54
+
55
+ try {
56
+ const result = await fetchWithCurlFallback(url, { binary: true });
57
+ return result as Buffer;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Parse raw EasyEDA API response into structured data
65
+ */
66
+ private parseComponentData(result: any, lcscId: string): EasyEDAComponentData {
67
+ const dataStr = result.dataStr;
68
+ const packageDetail = result.packageDetail;
69
+ const lcscInfo = result.lcsc || {};
70
+ const cPara = dataStr?.head?.c_para || {};
71
+
72
+ // Parse symbol shapes using shared parser
73
+ const symbolData = parseSymbolShapes(dataStr?.shape || []);
74
+
75
+ // Parse footprint shapes using shared parser
76
+ const fpDataStr = packageDetail?.dataStr;
77
+ const fpCPara = fpDataStr?.head?.c_para || {};
78
+ const footprintData = parseFootprintShapes(fpDataStr?.shape || []);
79
+
80
+ // Get origins for coordinate normalization
81
+ const symbolOrigin = {
82
+ x: parseFloat(dataStr?.head?.x) || 0,
83
+ y: parseFloat(dataStr?.head?.y) || 0,
84
+ };
85
+ const footprintOrigin = {
86
+ x: parseFloat(fpDataStr?.head?.x) || 0,
87
+ y: parseFloat(fpDataStr?.head?.y) || 0,
88
+ };
89
+
90
+ // Extract attributes from c_para (BOM_ prefixed fields)
91
+ const attributes: Record<string, string> = {};
92
+ for (const [key, value] of Object.entries(cPara)) {
93
+ if (key.startsWith('BOM_') && value && typeof value === 'string') {
94
+ // Clean up key name: "BOM_Resistance" -> "Resistance"
95
+ const cleanKey = key.replace(/^BOM_/, '');
96
+ if (cleanKey !== 'Manufacturer' && cleanKey !== 'JLCPCB Part Class') {
97
+ attributes[cleanKey] = value;
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ info: {
104
+ name: cPara.name || lcscId,
105
+ prefix: cPara.pre || 'U',
106
+ package: cPara.package || fpCPara?.package,
107
+ manufacturer: cPara.BOM_Manufacturer || cPara.Manufacturer,
108
+ datasheet: lcscInfo.url,
109
+ lcscId: lcscInfo.number || lcscId,
110
+ jlcId: cPara['BOM_JLCPCB Part Class'],
111
+ description: result.title || cPara.name,
112
+ category: result.category || undefined,
113
+ attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
114
+ // CDFER parity fields
115
+ stock: lcscInfo.stock,
116
+ price: lcscInfo.price,
117
+ minOrderQty: lcscInfo.min,
118
+ process: result.SMT ? 'SMT' : 'THT',
119
+ partClass: cPara['JLCPCB Part Class'],
120
+ partNumber: cPara['Manufacturer Part'],
121
+ },
122
+ symbol: {
123
+ pins: symbolData.pins,
124
+ rectangles: symbolData.rectangles,
125
+ circles: symbolData.circles,
126
+ ellipses: symbolData.ellipses,
127
+ arcs: symbolData.arcs,
128
+ polylines: symbolData.polylines,
129
+ polygons: symbolData.polygons,
130
+ paths: symbolData.paths,
131
+ origin: symbolOrigin,
132
+ },
133
+ footprint: {
134
+ name: fpCPara?.package || 'Unknown',
135
+ type: result.SMT && !packageDetail?.title?.includes('-TH_') ? 'smd' : 'tht',
136
+ pads: footprintData.pads,
137
+ tracks: footprintData.tracks,
138
+ holes: footprintData.holes,
139
+ circles: footprintData.circles,
140
+ arcs: footprintData.arcs,
141
+ rects: footprintData.rects,
142
+ texts: footprintData.texts,
143
+ vias: footprintData.vias,
144
+ solidRegions: footprintData.solidRegions,
145
+ origin: footprintOrigin,
146
+ },
147
+ model3d: footprintData.model3d,
148
+ rawData: result,
149
+ };
150
+ }
151
+ }
152
+
153
+ export const easyedaClient = new EasyEDAClient();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * API clients for JLC and EasyEDA
3
+ */
4
+
5
+ export { JLCClient, jlcClient } from './jlc.js';
6
+ export { EasyEDAClient, easyedaClient } from './easyeda.js';
7
+ export { EasyEDACommunityClient, easyedaCommunityClient } from './easyeda-community.js';
package/src/api/jlc.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * JLC API client for component search and details
3
+ * Uses JLCPCB's parts library API which provides LCSC component data
4
+ */
5
+
6
+ import type { LCSCSearchOptions, ComponentSearchResult } from '../types/index.js';
7
+ import { createLogger } from '../utils/index.js';
8
+
9
+ const logger = createLogger('jlc-api');
10
+
11
+ // JLCPCB parts API - provides LCSC component data with better reliability
12
+ const JLCPCB_SEARCH_API =
13
+ 'https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList/v2';
14
+
15
+ /**
16
+ * JLCPCB component structure from API response
17
+ */
18
+ interface JLCPCBComponent {
19
+ componentCode: string; // LCSC part number (e.g., "C82899")
20
+ componentModelEn: string; // Part model (e.g., "ESP32-WROOM-32-N4")
21
+ componentBrandEn: string; // Manufacturer
22
+ componentSpecificationEn: string; // Package type
23
+ stockCount: number;
24
+ componentPrices: Array<{
25
+ startNumber: number;
26
+ endNumber: number;
27
+ productPrice: number;
28
+ }>;
29
+ describe: string;
30
+ dataManualUrl?: string;
31
+ lcscGoodsUrl?: string;
32
+ componentTypeEn?: string;
33
+ componentLibraryType?: string; // "base" = basic part (no setup fee), "expand" = extended (setup fee required)
34
+ attributes?: Array<{
35
+ attribute_name_en: string;
36
+ attribute_value_name: string;
37
+ }>;
38
+ }
39
+
40
+ interface JLCPCBSearchResponse {
41
+ code: number;
42
+ data: {
43
+ componentPageInfo: {
44
+ total: number;
45
+ list: JLCPCBComponent[] | null;
46
+ };
47
+ };
48
+ message: string | null;
49
+ }
50
+
51
+ export interface LCSCProduct {
52
+ productCode: string;
53
+ productModel: string;
54
+ brandNameEn: string;
55
+ encapStandard: string;
56
+ productPriceList: Array<{
57
+ ladder: number;
58
+ productPrice: number;
59
+ currencySymbol: string;
60
+ }>;
61
+ stockNumber: number;
62
+ productIntroEn: string;
63
+ }
64
+
65
+ export class JLCClient {
66
+ /**
67
+ * Search for components via JLCPCB API
68
+ */
69
+ async search(
70
+ query: string,
71
+ options: LCSCSearchOptions = {}
72
+ ): Promise<ComponentSearchResult[]> {
73
+ const { limit = 10, page = 1, inStock = false, basicOnly = false } = options;
74
+
75
+ logger.debug(`Searching LCSC (via JLCPCB) for: ${query} (inStock=${inStock}, basicOnly=${basicOnly})`);
76
+
77
+ const requestBody: Record<string, unknown> = {
78
+ currentPage: page,
79
+ pageSize: Math.min(limit, 50),
80
+ keyword: query,
81
+ searchType: 2, // Better search relevance matching
82
+ };
83
+
84
+ // Add in-stock filter
85
+ if (inStock) {
86
+ requestBody.presaleType = 'stock';
87
+ }
88
+
89
+ // Add basic/preferred library filter (JLCPCB basic parts = lower assembly cost)
90
+ if (basicOnly) {
91
+ requestBody.componentLibTypes = ['base'];
92
+ requestBody.preferredComponentFlag = true;
93
+ }
94
+
95
+ const body = JSON.stringify(requestBody);
96
+
97
+ try {
98
+ const response = await fetch(JLCPCB_SEARCH_API, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ Accept: 'application/json',
103
+ },
104
+ body,
105
+ });
106
+
107
+ if (!response.ok) {
108
+ throw new Error(`JLCPCB API returned ${response.status}`);
109
+ }
110
+
111
+ const data: JLCPCBSearchResponse = await response.json();
112
+
113
+ if (data.code !== 200) {
114
+ throw new Error(`JLCPCB API error: ${data.message || 'Unknown error'}`);
115
+ }
116
+
117
+ const components = data.data?.componentPageInfo?.list || [];
118
+
119
+ logger.debug(`Found ${components.length} components`);
120
+
121
+ return components.map((c) => ({
122
+ lcscId: c.componentCode,
123
+ name: c.componentModelEn,
124
+ manufacturer: c.componentBrandEn,
125
+ package: c.componentSpecificationEn || '',
126
+ price: c.componentPrices?.[0]?.productPrice,
127
+ stock: c.stockCount,
128
+ description: c.describe,
129
+ productUrl: c.lcscGoodsUrl, // LCSC product page
130
+ datasheetPdf: c.dataManualUrl, // Actual PDF datasheet
131
+ category: c.componentTypeEn,
132
+ // JLCPCB assembly part type: "basic" = no setup fee, "extended" = setup fee required
133
+ libraryType: c.componentLibraryType === 'base' ? 'basic' : 'extended',
134
+ // Component specifications as key-value pairs
135
+ attributes: c.attributes?.reduce(
136
+ (acc, attr) => {
137
+ if (attr.attribute_value_name && attr.attribute_value_name !== '-') {
138
+ acc[attr.attribute_name_en] = attr.attribute_value_name;
139
+ }
140
+ return acc;
141
+ },
142
+ {} as Record<string, string>
143
+ ),
144
+ }));
145
+ } catch (error) {
146
+ logger.error(`Search failed: ${error}`);
147
+ throw error;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Get stock and pricing information for a component
153
+ */
154
+ async getStock(lcscPartNumber: string): Promise<{
155
+ stock: number;
156
+ priceBreaks: Array<{ quantity: number; price: number }>;
157
+ }> {
158
+ // Re-search to get current stock info
159
+ const results = await this.search(lcscPartNumber, { limit: 1 });
160
+
161
+ if (results.length === 0) {
162
+ throw new Error(`Component ${lcscPartNumber} not found`);
163
+ }
164
+
165
+ const product = results[0];
166
+ return {
167
+ stock: product.stock,
168
+ priceBreaks: [{ quantity: 1, price: product.price || 0 }],
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Get detailed component information including attributes
174
+ * Used to enrich EasyEDA data with JLC-specific attributes
175
+ */
176
+ async getComponentDetails(lcscPartNumber: string): Promise<ComponentSearchResult | null> {
177
+ const results = await this.search(lcscPartNumber, { limit: 1 });
178
+ if (results.length === 0) {
179
+ return null;
180
+ }
181
+ return results[0];
182
+ }
183
+ }
184
+
185
+ export const jlcClient = new JLCClient();