hiresquire-cli 1.1.0 → 1.2.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 +57 -0
- package/dist/api.d.ts +250 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +689 -0
- package/dist/api.js.map +1 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +195 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1370 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +353 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +49 -0
- package/dist/types.js.map +1 -0
- package/package.json +73 -62
- package/jest.config.js +0 -30
- package/tsconfig.tsbuildinfo +0 -1
package/dist/api.js
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* HireSquire CLI - API Client
|
|
4
|
+
*
|
|
5
|
+
* HTTP client for interacting with the HireSquire API
|
|
6
|
+
* Handles authentication, request/response typing, and error handling
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.ApiClient = void 0;
|
|
46
|
+
exports.createApiClient = createApiClient;
|
|
47
|
+
exports.createApiClientFromToken = createApiClientFromToken;
|
|
48
|
+
exports.readResumeFromFile = readResumeFromFile;
|
|
49
|
+
exports.readResumesFromDirectory = readResumesFromDirectory;
|
|
50
|
+
exports.readResumesFromPaths = readResumesFromPaths;
|
|
51
|
+
const axios_1 = __importDefault(require("axios"));
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const pdf_parse_1 = __importDefault(require("pdf-parse"));
|
|
55
|
+
const officeparser_1 = __importDefault(require("officeparser"));
|
|
56
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
57
|
+
const types_1 = require("./types");
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Constants
|
|
60
|
+
// ============================================================================
|
|
61
|
+
const DEFAULT_BASE_URL = 'https://api.hiresquireai.com/api/v1';
|
|
62
|
+
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
63
|
+
const MAX_RETRIES = 3;
|
|
64
|
+
const RETRY_DELAY = 1000; // 1 second base delay
|
|
65
|
+
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB max file size
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// API Client Class
|
|
68
|
+
// ============================================================================
|
|
69
|
+
class ApiClient {
|
|
70
|
+
constructor(config) {
|
|
71
|
+
this.config = {
|
|
72
|
+
...config,
|
|
73
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
74
|
+
};
|
|
75
|
+
this.client = axios_1.default.create({
|
|
76
|
+
baseURL: this.config.baseUrl,
|
|
77
|
+
timeout: DEFAULT_TIMEOUT,
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${this.config.apiToken}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Accept': 'application/json',
|
|
82
|
+
'User-Agent': 'hiresquire-cli/1.2.1',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Add response interceptor for error handling with retry
|
|
86
|
+
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
87
|
+
const originalRequest = error.config;
|
|
88
|
+
if (!originalRequest) {
|
|
89
|
+
throw this.handleError(error);
|
|
90
|
+
}
|
|
91
|
+
const shouldRetry = this.shouldRetryRequest(error);
|
|
92
|
+
const retryCount = originalRequest._retryCount || 0;
|
|
93
|
+
if (shouldRetry && retryCount < MAX_RETRIES) {
|
|
94
|
+
originalRequest._retryCount = retryCount + 1;
|
|
95
|
+
const delay = RETRY_DELAY * Math.pow(2, retryCount);
|
|
96
|
+
await this.sleep(delay);
|
|
97
|
+
return this.client(originalRequest);
|
|
98
|
+
}
|
|
99
|
+
throw this.handleError(error);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
shouldRetryRequest(error) {
|
|
103
|
+
const statusCode = error.response?.status;
|
|
104
|
+
const code = error.code;
|
|
105
|
+
// Retry on: rate limit, server errors, timeouts, and network errors
|
|
106
|
+
const retryableCodes = ['ECONNABORTED', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'];
|
|
107
|
+
return statusCode === 429 || statusCode === 503 || statusCode === 500 || retryableCodes.includes(code || '');
|
|
108
|
+
}
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Error Handling
|
|
111
|
+
// ============================================================================
|
|
112
|
+
handleError(error) {
|
|
113
|
+
const statusCode = error.response?.status;
|
|
114
|
+
const responseData = error.response?.data;
|
|
115
|
+
const message = responseData?.message || responseData?.error || error.message;
|
|
116
|
+
switch (statusCode) {
|
|
117
|
+
case 401:
|
|
118
|
+
case 403:
|
|
119
|
+
return new types_1.AuthenticationError(message);
|
|
120
|
+
case 400:
|
|
121
|
+
return new types_1.ValidationError(message);
|
|
122
|
+
case 404:
|
|
123
|
+
return new types_1.NotFoundError(message);
|
|
124
|
+
case 429:
|
|
125
|
+
const retryAfter = error.response?.headers['retry-after'];
|
|
126
|
+
return new types_1.RateLimitError(message + (retryAfter ? ` Retry after ${retryAfter} seconds.` : ''));
|
|
127
|
+
case 422:
|
|
128
|
+
return new types_1.ValidationError(message);
|
|
129
|
+
default:
|
|
130
|
+
return new types_1.HireSquireError(message, 'API_ERROR', statusCode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Job Operations
|
|
135
|
+
// ============================================================================
|
|
136
|
+
/**
|
|
137
|
+
* Generic GET request
|
|
138
|
+
*/
|
|
139
|
+
async get(url, config) {
|
|
140
|
+
const response = await this.client.get(url, config);
|
|
141
|
+
return { data: response.data };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generic POST request
|
|
145
|
+
*/
|
|
146
|
+
async post(url, data, config) {
|
|
147
|
+
const response = await this.client.post(url, data, config);
|
|
148
|
+
return { data: response.data };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Generic PUT request
|
|
152
|
+
*/
|
|
153
|
+
async put(url, data, config) {
|
|
154
|
+
const response = await this.client.put(url, data, config);
|
|
155
|
+
return { data: response.data };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Generic DELETE request
|
|
159
|
+
*/
|
|
160
|
+
async delete(url, config) {
|
|
161
|
+
const response = await this.client.delete(url, config);
|
|
162
|
+
return { data: response.data };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Create a new screening job
|
|
166
|
+
*/
|
|
167
|
+
async createJob(params) {
|
|
168
|
+
const response = await this.client.post('/jobs', params);
|
|
169
|
+
return response.data;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Upload a ZIP file containing resumes to create a new screening job
|
|
173
|
+
*/
|
|
174
|
+
async uploadZip(params) {
|
|
175
|
+
const formData = new form_data_1.default();
|
|
176
|
+
formData.append('title', params.title);
|
|
177
|
+
formData.append('description', params.description);
|
|
178
|
+
if (params.leniency_level !== undefined) {
|
|
179
|
+
formData.append('leniency_level', params.leniency_level.toString());
|
|
180
|
+
}
|
|
181
|
+
if (params.custom_instructions) {
|
|
182
|
+
formData.append('custom_instructions', params.custom_instructions);
|
|
183
|
+
}
|
|
184
|
+
if (params.webhook_url) {
|
|
185
|
+
formData.append('webhook_url', params.webhook_url);
|
|
186
|
+
}
|
|
187
|
+
// Append the zip file
|
|
188
|
+
formData.append('zip_file', fs.createReadStream(params.zipPath));
|
|
189
|
+
const response = await this.client.post('/jobs/upload-zip', formData, {
|
|
190
|
+
headers: {
|
|
191
|
+
...formData.getHeaders()
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return response.data;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get job status
|
|
198
|
+
*/
|
|
199
|
+
async getJobStatus(jobId) {
|
|
200
|
+
const response = await this.client.get(`/jobs/${jobId}`);
|
|
201
|
+
return response.data;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get job results
|
|
205
|
+
*/
|
|
206
|
+
async getResults(jobId, options = {}) {
|
|
207
|
+
const params = new URLSearchParams();
|
|
208
|
+
if (options.min_score)
|
|
209
|
+
params.append('min_score', options.min_score.toString());
|
|
210
|
+
if (options.only_top_n)
|
|
211
|
+
params.append('only_top_n', options.only_top_n.toString());
|
|
212
|
+
const queryString = params.toString();
|
|
213
|
+
const url = queryString
|
|
214
|
+
? `/jobs/${jobId}/results?${queryString}`
|
|
215
|
+
: `/jobs/${jobId}/results`;
|
|
216
|
+
const response = await this.client.get(url);
|
|
217
|
+
return response.data;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* List all jobs
|
|
221
|
+
*/
|
|
222
|
+
async listJobs(options = {}) {
|
|
223
|
+
const params = new URLSearchParams();
|
|
224
|
+
if (options.status)
|
|
225
|
+
params.append('status', options.status);
|
|
226
|
+
if (options.page)
|
|
227
|
+
params.append('page', options.page.toString());
|
|
228
|
+
if (options.per_page)
|
|
229
|
+
params.append('per_page', options.per_page.toString());
|
|
230
|
+
const queryString = params.toString();
|
|
231
|
+
const url = queryString ? `/jobs?${queryString}` : '/jobs';
|
|
232
|
+
const response = await this.client.get(url);
|
|
233
|
+
return response.data;
|
|
234
|
+
}
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Email Operations
|
|
237
|
+
// ============================================================================
|
|
238
|
+
/**
|
|
239
|
+
* Generate an email for a candidate
|
|
240
|
+
*/
|
|
241
|
+
async generateEmail(params) {
|
|
242
|
+
const response = await this.client.post(`/jobs/${params.job_id}/generate-email`, {
|
|
243
|
+
candidate_id: params.candidate_id,
|
|
244
|
+
type: params.type,
|
|
245
|
+
custom_message: params.custom_message,
|
|
246
|
+
});
|
|
247
|
+
return response.data;
|
|
248
|
+
}
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Utility Methods
|
|
251
|
+
// ============================================================================
|
|
252
|
+
/**
|
|
253
|
+
* Poll for job completion
|
|
254
|
+
*/
|
|
255
|
+
async pollForCompletion(jobId, options = {}) {
|
|
256
|
+
const interval = options.interval || 3000; // 3 seconds
|
|
257
|
+
const timeout = options.timeout || 300000; // 5 minutes
|
|
258
|
+
const startTime = Date.now();
|
|
259
|
+
while (Date.now() - startTime < timeout) {
|
|
260
|
+
const status = await this.getJobStatus(jobId);
|
|
261
|
+
if (options.onProgress) {
|
|
262
|
+
options.onProgress(status);
|
|
263
|
+
}
|
|
264
|
+
if (status.status === 'completed' || status.status === 'failed') {
|
|
265
|
+
return status;
|
|
266
|
+
}
|
|
267
|
+
await this.sleep(interval);
|
|
268
|
+
}
|
|
269
|
+
throw new types_1.HireSquireError(`Job polling timed out after ${timeout / 1000} seconds`, 'TIMEOUT');
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Sleep for specified milliseconds
|
|
273
|
+
*/
|
|
274
|
+
sleep(ms) {
|
|
275
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Test API connection
|
|
279
|
+
*/
|
|
280
|
+
async testConnection() {
|
|
281
|
+
try {
|
|
282
|
+
await this.listJobs({ per_page: 1 });
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Get current config
|
|
291
|
+
*/
|
|
292
|
+
getConfig() {
|
|
293
|
+
return { ...this.config };
|
|
294
|
+
}
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Job Control
|
|
297
|
+
// ============================================================================
|
|
298
|
+
/**
|
|
299
|
+
* Cancel a running job
|
|
300
|
+
*/
|
|
301
|
+
async cancelJob(jobId) {
|
|
302
|
+
const response = await this.client.post(`/jobs/${jobId}/cancel`);
|
|
303
|
+
return response.data;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Compare multiple candidates side-by-side
|
|
307
|
+
*/
|
|
308
|
+
async compareCandidates(jobId, candidateIds) {
|
|
309
|
+
const response = await this.client.get(`/jobs/${jobId}/compare?ids=${candidateIds.join(',')}`);
|
|
310
|
+
return response.data;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Report hiring outcome to improve AI accuracy
|
|
314
|
+
*/
|
|
315
|
+
async reportOutcome(jobId, candidateId, outcome) {
|
|
316
|
+
const response = await this.client.post(`/jobs/${jobId}/outcome`, { candidate_id: candidateId, outcome });
|
|
317
|
+
return response.data;
|
|
318
|
+
}
|
|
319
|
+
// ============================================================================
|
|
320
|
+
// Webhook
|
|
321
|
+
// ============================================================================
|
|
322
|
+
/**
|
|
323
|
+
* Test a webhook endpoint
|
|
324
|
+
*/
|
|
325
|
+
async testWebhook(webhookUrl) {
|
|
326
|
+
const response = await this.client.post('/webhooks/test', { url: webhookUrl });
|
|
327
|
+
return response.data;
|
|
328
|
+
}
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Rate Limit
|
|
331
|
+
// ============================================================================
|
|
332
|
+
/**
|
|
333
|
+
* Get current rate limit status
|
|
334
|
+
*/
|
|
335
|
+
async getRateLimit() {
|
|
336
|
+
const response = await this.client.get('/rate-limit');
|
|
337
|
+
return response.data;
|
|
338
|
+
}
|
|
339
|
+
// ============================================================================
|
|
340
|
+
// Candidate Operations
|
|
341
|
+
// ============================================================================
|
|
342
|
+
/**
|
|
343
|
+
* Get a specific candidate
|
|
344
|
+
*/
|
|
345
|
+
async getCandidate(candidateId) {
|
|
346
|
+
const response = await this.client.get(`/candidates/${candidateId}`);
|
|
347
|
+
return response.data;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Update candidate status
|
|
351
|
+
*/
|
|
352
|
+
async updateCandidateStatus(candidateId, status) {
|
|
353
|
+
const response = await this.client.patch(`/candidates/${candidateId}/status`, { status });
|
|
354
|
+
return response.data;
|
|
355
|
+
}
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// Schema Discovery
|
|
358
|
+
// ============================================================================
|
|
359
|
+
/**
|
|
360
|
+
* Get API schema for discovery
|
|
361
|
+
*/
|
|
362
|
+
async getSchema() {
|
|
363
|
+
const response = await this.client.get('/schema');
|
|
364
|
+
return response.data;
|
|
365
|
+
}
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
// Calendar Operations
|
|
368
|
+
// ===========================================================================
|
|
369
|
+
/**
|
|
370
|
+
* List calendar connections
|
|
371
|
+
*/
|
|
372
|
+
async listCalendarConnections() {
|
|
373
|
+
const response = await this.client.get('/calendar/connections');
|
|
374
|
+
return response.data;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create a calendar connection
|
|
378
|
+
*/
|
|
379
|
+
async createCalendarConnection(params) {
|
|
380
|
+
const response = await this.client.post('/calendar/connections', params);
|
|
381
|
+
return response.data;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Delete a calendar connection
|
|
385
|
+
*/
|
|
386
|
+
async deleteCalendarConnection(id) {
|
|
387
|
+
const response = await this.client.delete(`/calendar/connections/${id}`);
|
|
388
|
+
return response.data;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get available slots from calendar
|
|
392
|
+
*/
|
|
393
|
+
async getAvailableSlots(params) {
|
|
394
|
+
const response = await this.client.get('/calendar/slots', { params });
|
|
395
|
+
return response.data;
|
|
396
|
+
}
|
|
397
|
+
// ===========================================================================
|
|
398
|
+
// Interview Operations
|
|
399
|
+
// ===========================================================================
|
|
400
|
+
/**
|
|
401
|
+
* List interviews
|
|
402
|
+
*/
|
|
403
|
+
async listInterviews(jobId) {
|
|
404
|
+
const params = jobId ? { job_id: jobId } : {};
|
|
405
|
+
const response = await this.client.get('/interviews', { params });
|
|
406
|
+
return response.data;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Create an interview
|
|
410
|
+
*/
|
|
411
|
+
async createInterview(params) {
|
|
412
|
+
const response = await this.client.post('/interviews', params);
|
|
413
|
+
return response.data;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Show a specific interview
|
|
417
|
+
*/
|
|
418
|
+
async showInterview(id) {
|
|
419
|
+
const response = await this.client.get(`/interviews/${id}`);
|
|
420
|
+
return response.data;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Update an interview
|
|
424
|
+
*/
|
|
425
|
+
async updateInterview(id, params) {
|
|
426
|
+
const response = await this.client.put(`/interviews/${id}`, params);
|
|
427
|
+
return response.data;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Delete/cancel an interview
|
|
431
|
+
*/
|
|
432
|
+
async deleteInterview(id) {
|
|
433
|
+
const response = await this.client.delete(`/interviews/${id}`);
|
|
434
|
+
return response.data;
|
|
435
|
+
}
|
|
436
|
+
// ===========================================================================
|
|
437
|
+
// Meeting Operations
|
|
438
|
+
// ===========================================================================
|
|
439
|
+
/**
|
|
440
|
+
* Generate a meeting link
|
|
441
|
+
*/
|
|
442
|
+
async generateMeetingLink(params) {
|
|
443
|
+
const response = await this.client.post('/meetings/links', params);
|
|
444
|
+
return response.data;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
exports.ApiClient = ApiClient;
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
// Factory Functions
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// Factory Functions
|
|
452
|
+
// ============================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Create API client from config
|
|
455
|
+
*/
|
|
456
|
+
function createApiClient(config) {
|
|
457
|
+
return new ApiClient(config);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Create API client from token
|
|
461
|
+
*/
|
|
462
|
+
function createApiClientFromToken(token, baseUrl) {
|
|
463
|
+
return new ApiClient({
|
|
464
|
+
apiToken: token,
|
|
465
|
+
baseUrl: baseUrl || DEFAULT_BASE_URL,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// Resume Parsing Utilities
|
|
470
|
+
// ============================================================================
|
|
471
|
+
/**
|
|
472
|
+
* Read resume from file path (async - supports PDF, DOCX, DOC, TXT, MD)
|
|
473
|
+
*/
|
|
474
|
+
async function readResumeFromFile(filePath) {
|
|
475
|
+
const absolutePath = path.resolve(filePath);
|
|
476
|
+
if (!fs.existsSync(absolutePath)) {
|
|
477
|
+
throw new Error(`File not found: ${filePath}`);
|
|
478
|
+
}
|
|
479
|
+
const stats = fs.statSync(absolutePath);
|
|
480
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
481
|
+
throw new Error(`File too large: ${filePath} (max ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
|
|
482
|
+
}
|
|
483
|
+
const filename = path.basename(filePath);
|
|
484
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
485
|
+
let content;
|
|
486
|
+
try {
|
|
487
|
+
if (ext === '.pdf') {
|
|
488
|
+
content = await parsePdf(absolutePath);
|
|
489
|
+
}
|
|
490
|
+
else if (ext === '.docx') {
|
|
491
|
+
content = await parseDocx(absolutePath);
|
|
492
|
+
}
|
|
493
|
+
else if (ext === '.doc') {
|
|
494
|
+
content = await parseDoc(absolutePath);
|
|
495
|
+
}
|
|
496
|
+
else if (['.txt', '.md'].includes(ext)) {
|
|
497
|
+
content = fs.readFileSync(absolutePath, 'utf-8');
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
throw new Error(`Unsupported file format: ${ext}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
throw new Error(`Failed to parse ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
505
|
+
}
|
|
506
|
+
// Validate extracted content
|
|
507
|
+
if (!content || content.trim().length < 50) {
|
|
508
|
+
throw new Error(`Could not extract sufficient text from: ${filePath}`);
|
|
509
|
+
}
|
|
510
|
+
return { filename, content };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Parse PDF file and extract text
|
|
514
|
+
*/
|
|
515
|
+
async function parsePdf(filePath) {
|
|
516
|
+
try {
|
|
517
|
+
const dataBuffer = fs.readFileSync(filePath);
|
|
518
|
+
const data = await (0, pdf_parse_1.default)(dataBuffer);
|
|
519
|
+
return data.text || '';
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
throw new Error(`PDF parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Parse DOCX file and extract text
|
|
527
|
+
*/
|
|
528
|
+
async function parseDocx(filePath) {
|
|
529
|
+
try {
|
|
530
|
+
// officeparser returns AST, convert to plain text
|
|
531
|
+
const result = await new Promise((resolve, reject) => {
|
|
532
|
+
officeparser_1.default.parseOffice(filePath, (data) => {
|
|
533
|
+
const text = officeparser_1.default.toText(data);
|
|
534
|
+
resolve({ content: text || '' });
|
|
535
|
+
}, (error) => {
|
|
536
|
+
reject(error);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
return result.content || '';
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
throw new Error(`DOCX parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Parse DOC file and extract text (uses same parser as DOCX)
|
|
547
|
+
*/
|
|
548
|
+
async function parseDoc(filePath) {
|
|
549
|
+
// officeparser supports DOC files as well
|
|
550
|
+
return parseDocx(filePath);
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Read resumes from directory (async)
|
|
554
|
+
*/
|
|
555
|
+
async function readResumesFromDirectory(dirPath, currentCount = 0) {
|
|
556
|
+
const absolutePath = path.resolve(dirPath);
|
|
557
|
+
if (!fs.existsSync(absolutePath)) {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
const files = fs.readdirSync(absolutePath);
|
|
561
|
+
const resumeExtensions = ['.txt', '.pdf', '.doc', '.docx', '.md'];
|
|
562
|
+
const resumes = [];
|
|
563
|
+
for (const file of files) {
|
|
564
|
+
if (currentCount + resumes.length >= 100)
|
|
565
|
+
break;
|
|
566
|
+
const ext = path.extname(file).toLowerCase();
|
|
567
|
+
if (resumeExtensions.includes(ext)) {
|
|
568
|
+
const filePath = path.join(absolutePath, file);
|
|
569
|
+
try {
|
|
570
|
+
resumes.push(await readResumeFromFile(filePath));
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
console.warn(`Warning: Could not read ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return resumes;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Read multiple resume files (async)
|
|
581
|
+
*/
|
|
582
|
+
async function readResumesFromPaths(paths) {
|
|
583
|
+
const resumes = [];
|
|
584
|
+
for (const p of paths) {
|
|
585
|
+
if (resumes.length >= 100) {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
const resolvedPath = path.resolve(p);
|
|
589
|
+
// Check if it's a glob pattern
|
|
590
|
+
if (p.includes('*')) {
|
|
591
|
+
const patternMatches = await matchGlobPattern(resolvedPath, resumes.length);
|
|
592
|
+
resumes.push(...patternMatches);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const stat = fs.statSync(resolvedPath);
|
|
597
|
+
if (stat.isDirectory()) {
|
|
598
|
+
const dirResumes = await readResumesFromDirectory(resolvedPath, resumes.length);
|
|
599
|
+
resumes.push(...dirResumes);
|
|
600
|
+
}
|
|
601
|
+
else if (stat.isFile()) {
|
|
602
|
+
resumes.push(await readResumeFromFile(resolvedPath));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
console.warn(`Warning: Could not read ${p}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (resumes.length >= 100) {
|
|
610
|
+
console.warn('⚠️ Maximum limit of 100 resumes reached. Additional files in this batch were ignored.');
|
|
611
|
+
}
|
|
612
|
+
return resumes.slice(0, 100);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Match glob patterns for resume files (async)
|
|
616
|
+
*/
|
|
617
|
+
async function matchGlobPattern(pattern, currentCount = 0) {
|
|
618
|
+
const resumes = [];
|
|
619
|
+
const resumeExtensions = ['.txt', '.pdf', '.doc', '.docx', '.md'];
|
|
620
|
+
// Handle **/*.ext patterns (recursive)
|
|
621
|
+
if (pattern.includes('**')) {
|
|
622
|
+
const parts = pattern.split('**');
|
|
623
|
+
const baseDir = path.resolve(parts[0] || '.');
|
|
624
|
+
const suffix = parts[1] || '';
|
|
625
|
+
async function walk(dir) {
|
|
626
|
+
if (!fs.existsSync(dir))
|
|
627
|
+
return;
|
|
628
|
+
if (currentCount + resumes.length >= 100)
|
|
629
|
+
return;
|
|
630
|
+
const files = fs.readdirSync(dir);
|
|
631
|
+
for (const file of files) {
|
|
632
|
+
if (currentCount + resumes.length >= 100)
|
|
633
|
+
break;
|
|
634
|
+
const fullPath = path.join(dir, file);
|
|
635
|
+
const stat = fs.statSync(fullPath);
|
|
636
|
+
if (stat.isDirectory()) {
|
|
637
|
+
await walk(fullPath);
|
|
638
|
+
}
|
|
639
|
+
else if (stat.isFile()) {
|
|
640
|
+
const ext = path.extname(file).toLowerCase();
|
|
641
|
+
if (resumeExtensions.includes(ext)) {
|
|
642
|
+
if (!suffix || fullPath.endsWith(suffix.replace('/', path.sep))) {
|
|
643
|
+
try {
|
|
644
|
+
resumes.push(await readResumeFromFile(fullPath));
|
|
645
|
+
}
|
|
646
|
+
catch (e) {
|
|
647
|
+
console.warn(`Warning: Could not read ${file}: ${e.message}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
await walk(baseDir);
|
|
655
|
+
return resumes;
|
|
656
|
+
}
|
|
657
|
+
// Standard glob implementation (non-recursive)
|
|
658
|
+
const lastSlash = pattern.lastIndexOf(path.sep);
|
|
659
|
+
let searchDir = '.';
|
|
660
|
+
let filePattern = pattern;
|
|
661
|
+
if (lastSlash >= 0) {
|
|
662
|
+
searchDir = pattern.slice(0, lastSlash);
|
|
663
|
+
filePattern = pattern.slice(lastSlash + 1);
|
|
664
|
+
}
|
|
665
|
+
const searchPath = path.resolve(searchDir);
|
|
666
|
+
if (!fs.existsSync(searchPath)) {
|
|
667
|
+
return resumes;
|
|
668
|
+
}
|
|
669
|
+
const regexPattern = new RegExp('^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i');
|
|
670
|
+
const files = fs.readdirSync(searchPath);
|
|
671
|
+
for (const file of files) {
|
|
672
|
+
if (currentCount + resumes.length >= 100)
|
|
673
|
+
break;
|
|
674
|
+
if (regexPattern.test(file)) {
|
|
675
|
+
const ext = path.extname(file).toLowerCase();
|
|
676
|
+
if (resumeExtensions.includes(ext)) {
|
|
677
|
+
const filePath = path.join(searchPath, file);
|
|
678
|
+
try {
|
|
679
|
+
resumes.push(await readResumeFromFile(filePath));
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
console.warn(`Warning: Could not read ${file}: ${e.message}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return resumes;
|
|
688
|
+
}
|
|
689
|
+
//# sourceMappingURL=api.js.map
|