hiresquire-cli 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1370 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * HireSquire CLI - Main Entry Point
5
+ *
6
+ * Command-line interface for HireSquire AI-powered candidate screening
7
+ *
8
+ * Supported agents:
9
+ * - Claude Code / Claude Desktop
10
+ * - OpenCode
11
+ * - OpenClaw
12
+ * - Codex
13
+ * - And any other CLI-capable agent
14
+ *
15
+ * @packageDocumentation
16
+ * @module HireSquire CLI
17
+ */
18
+ var __importDefault = (this && this.__importDefault) || function (mod) {
19
+ return (mod && mod.__esModule) ? mod : { "default": mod };
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.ValidationError = exports.HireSquireError = exports.saveConfig = exports.getConfigManager = exports.readResumesFromPaths = exports.createApiClient = exports.ApiClient = void 0;
23
+ exports.createCli = createCli;
24
+ const commander_1 = require("commander");
25
+ const chalk_1 = __importDefault(require("chalk"));
26
+ const ora_1 = __importDefault(require("ora"));
27
+ const inquirer_1 = __importDefault(require("inquirer"));
28
+ const api_1 = require("./api");
29
+ Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return api_1.ApiClient; } });
30
+ Object.defineProperty(exports, "createApiClient", { enumerable: true, get: function () { return api_1.createApiClient; } });
31
+ Object.defineProperty(exports, "readResumesFromPaths", { enumerable: true, get: function () { return api_1.readResumesFromPaths; } });
32
+ const config_1 = require("./config");
33
+ Object.defineProperty(exports, "getConfigManager", { enumerable: true, get: function () { return config_1.getConfigManager; } });
34
+ Object.defineProperty(exports, "saveConfig", { enumerable: true, get: function () { return config_1.saveConfig; } });
35
+ const types_1 = require("./types");
36
+ var types_2 = require("./types");
37
+ Object.defineProperty(exports, "HireSquireError", { enumerable: true, get: function () { return types_2.HireSquireError; } });
38
+ Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return types_2.ValidationError; } });
39
+ // For testing purposes
40
+ function createCli() {
41
+ return new commander_1.Command();
42
+ }
43
+ // ============================================================================
44
+ // Setup
45
+ // ============================================================================
46
+ const program = new commander_1.Command();
47
+ let apiClient = null;
48
+ let isJsonOutput = false;
49
+ // ============================================================================
50
+ // Utility Functions
51
+ // ============================================================================
52
+ /**
53
+ * Initialize API client
54
+ */
55
+ function initApi() {
56
+ const config = (0, config_1.getConfigManager)().load();
57
+ if (!config.apiToken) {
58
+ console.error(chalk_1.default.red('Error: API token not configured.'));
59
+ console.log(chalk_1.default.blue('Run ') + chalk_1.default.cyan('hiresquire init') + chalk_1.default.blue(' to configure your API token.'));
60
+ process.exit(1);
61
+ }
62
+ return (0, api_1.createApiClient)(config);
63
+ }
64
+ /**
65
+ * Output JSON and exit
66
+ * @param data The data to output
67
+ * @param shouldExit Whether to exit the process (default: true)
68
+ */
69
+ function outputJson(data, shouldExit = true) {
70
+ console.log(JSON.stringify(data, null, 2));
71
+ if (shouldExit) {
72
+ process.exit(0);
73
+ }
74
+ }
75
+ /**
76
+ * Handle errors
77
+ */
78
+ function handleError(error, context = 'Operation failed') {
79
+ if (isJsonOutput) {
80
+ outputJson({
81
+ success: false,
82
+ error: error instanceof Error ? error.message : context,
83
+ context,
84
+ });
85
+ }
86
+ if (error instanceof types_1.HireSquireError) {
87
+ console.error(chalk_1.default.red(`Error: ${error.message}`));
88
+ }
89
+ else if (error instanceof types_1.ValidationError) {
90
+ console.error(chalk_1.default.red(`Validation Error: ${error.message}`));
91
+ }
92
+ else if (error instanceof Error) {
93
+ console.error(chalk_1.default.red(`${context}: ${error.message}`));
94
+ }
95
+ else {
96
+ console.error(chalk_1.default.red(context));
97
+ }
98
+ process.exit(1);
99
+ }
100
+ /**
101
+ * Confirm action
102
+ */
103
+ async function confirm(message) {
104
+ const answers = await inquirer_1.default.prompt([
105
+ {
106
+ type: 'confirm',
107
+ name: 'confirm',
108
+ message,
109
+ default: false,
110
+ },
111
+ ]);
112
+ return answers.confirm;
113
+ }
114
+ // ============================================================================
115
+ // Commands
116
+ // ============================================================================
117
+ /**
118
+ * Init command - Configure API token
119
+ */
120
+ program
121
+ .command('init')
122
+ .description('Initialize configuration with API token')
123
+ .requiredOption('-t, --token <token>', 'API token from HireSquire dashboard')
124
+ .option('-u, --base-url <url>', 'API base URL', 'https://api.hiresquireai.com/api/v1')
125
+ .option('-w, --webhook <url>', 'Default webhook URL')
126
+ .option('-y, --yes', 'Skip confirmation (auto-enabled for non-interactive/JSON mode)')
127
+ .action(async (options) => {
128
+ try {
129
+ // Auto-skip confirmation for agents using JSON output or CI mode
130
+ const autoYes = isJsonOutput || process.env.CI === 'true';
131
+ if (!options.yes && !autoYes) {
132
+ const confirmed = await confirm(`Save API token to ${(0, config_1.getConfigManager)().getConfigPath()}?`);
133
+ if (!confirmed) {
134
+ if (isJsonOutput) {
135
+ outputJson({ success: false, message: 'Configuration cancelled' });
136
+ }
137
+ console.log(chalk_1.default.yellow('Configuration cancelled.'));
138
+ return;
139
+ }
140
+ }
141
+ (0, config_1.saveConfig)({
142
+ apiToken: options.token,
143
+ baseUrl: options.baseUrl,
144
+ webhookUrl: options.webhook,
145
+ });
146
+ if (isJsonOutput) {
147
+ outputJson({
148
+ success: true,
149
+ message: 'Configuration saved successfully',
150
+ configPath: (0, config_1.getConfigManager)().getConfigPath(),
151
+ });
152
+ return;
153
+ }
154
+ console.log(chalk_1.default.green('✓ Configuration saved successfully'));
155
+ console.log(chalk_1.default.gray(` Config: ${(0, config_1.getConfigManager)().getConfigPath()}`));
156
+ }
157
+ catch (error) {
158
+ handleError(error, 'Failed to save configuration');
159
+ }
160
+ });
161
+ /**
162
+ * Screen command - Submit screening job
163
+ */
164
+ program
165
+ .command('screen')
166
+ .description('Submit a candidate screening job')
167
+ .requiredOption('-t, --title <title>', 'Job posting title')
168
+ .requiredOption('-d, --description <description>', 'Job description (string or @file)')
169
+ .option('-r, --resumes <paths>', 'Resume files or directory (comma-separated or @file)')
170
+ .option('-z, --zip <path>', 'Path to a ZIP file containing resumes (overrides --resumes)')
171
+ .option('-l, --leniency <1-10>', 'Screening leniency level (1=strict, 10=loose)', '5')
172
+ .option('-w, --webhook <url>', 'Webhook URL for notifications')
173
+ .option('--watch', 'Poll for completion and show results')
174
+ .option('--min-score <number>', 'Minimum score threshold for webhook notifications (0-100)')
175
+ .option('--only-top-n <number>', 'Only send top N candidates to webhook')
176
+ .action(async (options) => {
177
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Submitting screening job...').start() : null;
178
+ try {
179
+ // Validate leniency level
180
+ const leniency = parseInt(options.leniency);
181
+ if (isNaN(leniency) || leniency < 1 || leniency > 10) {
182
+ throw new types_1.ValidationError('Leniency level must be an integer between 1 and 10');
183
+ }
184
+ const config = (0, config_1.getConfigManager)().load();
185
+ const api = initApi();
186
+ // Parse job description
187
+ let jobDescription = options.description;
188
+ if (options.description.startsWith('@')) {
189
+ const fs = require('fs');
190
+ jobDescription = fs.readFileSync(options.description.slice(1), 'utf-8');
191
+ }
192
+ let result;
193
+ // Prepare webhook conditions
194
+ const webhookConditions = {};
195
+ if (options.minScore)
196
+ webhookConditions.min_score = parseInt(options.minScore);
197
+ if (options.onlyTopN)
198
+ webhookConditions.only_top_n = parseInt(options.onlyTopN);
199
+ if (options.zip) {
200
+ const params = {
201
+ title: options.title,
202
+ description: jobDescription,
203
+ zipPath: options.zip,
204
+ leniency_level: parseInt(options.leniency),
205
+ webhook_url: options.webhook || config.webhookUrl,
206
+ webhook_conditions: Object.keys(webhookConditions).length > 0
207
+ ? webhookConditions
208
+ : undefined,
209
+ };
210
+ result = await api.uploadZip(params);
211
+ if (spinner)
212
+ spinner.succeed(`Job #${result.job_id} created from ZIP archive`);
213
+ }
214
+ else {
215
+ if (!options.resumes) {
216
+ throw new types_1.ValidationError('You must provide either --resumes or --zip');
217
+ }
218
+ // Parse resumes
219
+ let resumes = [];
220
+ if (options.resumes.startsWith('@')) {
221
+ // Read from file (one filename per line)
222
+ const fs = require('fs');
223
+ const files = fs.readFileSync(options.resumes.slice(1), 'utf-8')
224
+ .split('\n')
225
+ .filter((line) => line.trim());
226
+ resumes = await (0, api_1.readResumesFromPaths)(files);
227
+ }
228
+ else {
229
+ // Comma-separated paths
230
+ const paths = options.resumes.split(',').map((p) => p.trim());
231
+ resumes = await (0, api_1.readResumesFromPaths)(paths);
232
+ }
233
+ if (resumes.length === 0) {
234
+ throw new types_1.ValidationError('No resume files found');
235
+ }
236
+ // Create job params
237
+ const params = {
238
+ title: options.title,
239
+ description: jobDescription,
240
+ resumes,
241
+ leniency_level: parseInt(options.leniency),
242
+ webhook_url: options.webhook || config.webhookUrl,
243
+ webhook_conditions: Object.keys(webhookConditions).length > 0
244
+ ? webhookConditions
245
+ : undefined,
246
+ };
247
+ // Submit job
248
+ result = await api.createJob(params);
249
+ if (spinner)
250
+ spinner.succeed(`Job #${result.job_id} created`);
251
+ }
252
+ if (isJsonOutput) {
253
+ if (options.watch) {
254
+ // Output intermediate result for watch mode
255
+ console.log(JSON.stringify({
256
+ type: 'job_created',
257
+ job_id: result.job_id,
258
+ status: result.status,
259
+ }));
260
+ }
261
+ else {
262
+ outputJson({
263
+ success: true,
264
+ job_id: result.job_id,
265
+ status: result.status,
266
+ status_url: result.status_url,
267
+ });
268
+ return;
269
+ }
270
+ }
271
+ console.log(chalk_1.default.blue(` Status: ${chalk_1.default.bold(result.status)}`));
272
+ console.log(chalk_1.default.gray(` Poll: ${result.status_url}`));
273
+ // Watch mode
274
+ if (options.watch) {
275
+ if (!isJsonOutput) {
276
+ console.log(chalk_1.default.blue('\n⏳ Waiting for completion...\n'));
277
+ }
278
+ const finalStatus = await api.pollForCompletion(result.job_id, {
279
+ onProgress: (status) => {
280
+ const progress = status.progress || 0;
281
+ const bar = '█'.repeat(Math.floor(progress / 10)) + '░'.repeat(10 - Math.floor(progress / 10));
282
+ process.stdout.write(`\r Progress: [${bar}] ${progress}% ${status.message || ''}`);
283
+ },
284
+ });
285
+ if (!isJsonOutput) {
286
+ console.log(chalk_1.default.green(`\n✓ Screening complete!`));
287
+ console.log(chalk_1.default.blue(` Final status: ${chalk_1.default.bold(finalStatus.status)}`));
288
+ }
289
+ if (finalStatus.status === 'completed') {
290
+ const results = await api.getResults(result.job_id);
291
+ if (isJsonOutput) {
292
+ outputJson({
293
+ type: 'job_completed',
294
+ success: true,
295
+ job_id: result.job_id,
296
+ results,
297
+ });
298
+ return;
299
+ }
300
+ console.log(chalk_1.default.blue(`\n📊 Results (${results.candidates.length} candidates):\n`));
301
+ // Sort by score
302
+ const sorted = [...results.candidates].sort((a, b) => b.score - a.score);
303
+ sorted.forEach((candidate, index) => {
304
+ const scoreColor = candidate.score >= 80 ? chalk_1.default.green :
305
+ candidate.score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
306
+ console.log(`${chalk_1.default.bold(index + 1)}. ${candidate.name} ${scoreColor(`(${candidate.score}/100)`)}`);
307
+ console.log(chalk_1.default.gray(` ${candidate.summary?.substring(0, 80) || 'No summary'}...`));
308
+ console.log();
309
+ });
310
+ }
311
+ }
312
+ }
313
+ catch (error) {
314
+ if (spinner)
315
+ spinner.fail('Failed to submit job');
316
+ handleError(error, 'Screening failed');
317
+ }
318
+ });
319
+ /**
320
+ * Jobs command - List all jobs
321
+ */
322
+ program
323
+ .command('jobs')
324
+ .description('List all screening jobs')
325
+ .option('-s, --status <status>', 'Filter by status (pending, processing, completed, failed)')
326
+ .option('-p, --page <number>', 'Page number', '1')
327
+ .option('-l, --limit <number>', 'Results per page', '10')
328
+ .action(async (options) => {
329
+ try {
330
+ const api = initApi();
331
+ const spinner = (0, ora_1.default)('Fetching jobs...').start();
332
+ const result = await api.listJobs({
333
+ status: options.status,
334
+ page: parseInt(options.page),
335
+ per_page: parseInt(options.limit),
336
+ });
337
+ spinner.stop();
338
+ if (isJsonOutput) {
339
+ outputJson({
340
+ success: true,
341
+ jobs: result.jobs,
342
+ total: result.total,
343
+ page: result.page,
344
+ });
345
+ return;
346
+ }
347
+ if (result.jobs.length === 0) {
348
+ console.log(chalk_1.default.yellow('No jobs found.'));
349
+ return;
350
+ }
351
+ console.log(chalk_1.default.blue(`📋 Jobs (${result.total} total):\n`));
352
+ result.jobs.forEach((job) => {
353
+ const statusColor = job.status === 'completed' ? chalk_1.default.green :
354
+ job.status === 'failed' ? chalk_1.default.red :
355
+ job.status === 'processing' ? chalk_1.default.yellow : chalk_1.default.gray;
356
+ console.log(` ${chalk_1.default.bold(`#${job.id}`)} ${job.title}`);
357
+ console.log(` Status: ${statusColor(job.status)} | Candidates: ${job.total_candidates}`);
358
+ console.log(` Created: ${chalk_1.default.gray(new Date(job.created_at).toLocaleString())}`);
359
+ console.log();
360
+ });
361
+ }
362
+ catch (error) {
363
+ handleError(error, 'Failed to fetch jobs');
364
+ }
365
+ });
366
+ /**
367
+ * Results command - Get job results
368
+ */
369
+ program
370
+ .command('results')
371
+ .description('Get results for a screening job')
372
+ .requiredOption('-j, --job <id>', 'Job ID')
373
+ .option('--min-score <number>', 'Filter by minimum score')
374
+ .option('--only-top-n <number>', 'Only return top N candidates')
375
+ .action(async (options) => {
376
+ try {
377
+ const api = initApi();
378
+ const spinner = (0, ora_1.default)('Fetching results...').start();
379
+ const jobId = parseInt(options.job);
380
+ if (isNaN(jobId)) {
381
+ throw new types_1.ValidationError('Invalid job ID');
382
+ }
383
+ const result = await api.getResults(jobId, {
384
+ min_score: options.minScore ? parseInt(options.minScore) : undefined,
385
+ only_top_n: options.onlyTopN ? parseInt(options.onlyTopN) : undefined,
386
+ });
387
+ spinner.stop();
388
+ if (isJsonOutput) {
389
+ outputJson({
390
+ success: true,
391
+ job_id: result.job_id,
392
+ job_title: result.job_title,
393
+ status: result.status,
394
+ results: result.candidates,
395
+ });
396
+ return;
397
+ }
398
+ console.log(chalk_1.default.blue(`📊 Results for: ${result.job_title}\n`));
399
+ console.log(chalk_1.default.gray(` Status: ${result.status}`));
400
+ console.log(chalk_1.default.gray(` Screened: ${new Date(result.screened_at).toLocaleString()}`));
401
+ console.log(chalk_1.default.gray(` Total: ${result.total_candidates} candidates\n`));
402
+ // Sort by score
403
+ const sorted = [...result.candidates].sort((a, b) => b.score - a.score);
404
+ sorted.forEach((candidate, index) => {
405
+ const scoreColor = candidate.score >= 80 ? chalk_1.default.green :
406
+ candidate.score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
407
+ console.log(`${chalk_1.default.bold(index + 1)}. ${candidate.name} ${scoreColor(`(${candidate.score}/100)`)}`);
408
+ console.log(chalk_1.default.gray(` ${candidate.summary?.substring(0, 100) || 'No summary'}...`));
409
+ if (candidate.interview_questions?.length > 0) {
410
+ console.log(chalk_1.default.blue(` Top Interview Question:`));
411
+ console.log(chalk_1.default.gray(` "${candidate.interview_questions[0]}"`));
412
+ }
413
+ console.log();
414
+ });
415
+ }
416
+ catch (error) {
417
+ handleError(error, 'Failed to fetch results');
418
+ }
419
+ });
420
+ /**
421
+ * Status command - Check job status
422
+ */
423
+ program
424
+ .command('status')
425
+ .description('Check status of a screening job')
426
+ .requiredOption('-j, --job <id>', 'Job ID')
427
+ .option('-w, --watch', 'Watch for status changes')
428
+ .action(async (options) => {
429
+ try {
430
+ const api = initApi();
431
+ const jobId = parseInt(options.job);
432
+ if (isNaN(jobId)) {
433
+ throw new types_1.ValidationError('Invalid job ID');
434
+ }
435
+ if (options.watch) {
436
+ console.log(chalk_1.default.blue('⏳ Watching for status changes...\n'));
437
+ const finalStatus = await api.pollForCompletion(jobId, {
438
+ onProgress: (status) => {
439
+ const progress = status.progress || 0;
440
+ const bar = '█'.repeat(Math.floor(progress / 10)) + '░'.repeat(10 - Math.floor(progress / 10));
441
+ console.clear();
442
+ console.log(chalk_1.default.blue(`📋 Job #${jobId}\n`));
443
+ console.log(chalk_1.default.gray(` Status: ${chalk_1.default.bold(status.status)}`));
444
+ console.log(chalk_1.default.gray(` Progress: [${bar}] ${progress}%`));
445
+ if (status.message) {
446
+ console.log(chalk_1.default.gray(` Message: ${status.message}`));
447
+ }
448
+ },
449
+ });
450
+ console.log(chalk_1.default.green(`\n✓ Job ${finalStatus.status}!`));
451
+ }
452
+ else {
453
+ const spinner = (0, ora_1.default)('Checking status...').start();
454
+ const status = await api.getJobStatus(jobId);
455
+ spinner.stop();
456
+ if (isJsonOutput) {
457
+ outputJson({
458
+ success: true,
459
+ job_id: status.job_id,
460
+ status: status.status,
461
+ progress: status.progress,
462
+ });
463
+ return;
464
+ }
465
+ const statusColor = status.status === 'completed' ? chalk_1.default.green :
466
+ status.status === 'failed' ? chalk_1.default.red :
467
+ status.status === 'processing' ? chalk_1.default.yellow : chalk_1.default.gray;
468
+ console.log(chalk_1.default.blue(`📋 Job #${jobId}\n`));
469
+ console.log(chalk_1.default.gray(` Status: ${statusColor(status.status)}`));
470
+ if (status.progress !== undefined) {
471
+ console.log(chalk_1.default.gray(` Progress: ${status.progress}%`));
472
+ }
473
+ if (status.message) {
474
+ console.log(chalk_1.default.gray(` Message: ${status.message}`));
475
+ }
476
+ }
477
+ }
478
+ catch (error) {
479
+ handleError(error, 'Failed to check status');
480
+ }
481
+ });
482
+ /**
483
+ * Email command - Generate email for candidate
484
+ */
485
+ program
486
+ .command('email')
487
+ .description('Generate an email for a candidate')
488
+ .requiredOption('-j, --job <id>', 'Job ID')
489
+ .requiredOption('-c, --candidate <id>', 'Candidate ID')
490
+ .requiredOption('-t, --type <type>', 'Email type (invite, rejection, followup)')
491
+ .option('-m, --message <text>', 'Custom message to include')
492
+ .action(async (options) => {
493
+ try {
494
+ const api = initApi();
495
+ const spinner = (0, ora_1.default)('Generating email...').start();
496
+ const jobId = parseInt(options.job);
497
+ const candidateId = parseInt(options.candidate);
498
+ if (isNaN(jobId) || isNaN(candidateId)) {
499
+ throw new types_1.ValidationError('Invalid job or candidate ID');
500
+ }
501
+ const validTypes = ['invite', 'rejection', 'followup'];
502
+ if (!validTypes.includes(options.type)) {
503
+ throw new types_1.ValidationError(`Invalid email type. Must be: ${validTypes.join(', ')}`);
504
+ }
505
+ const result = await api.generateEmail({
506
+ job_id: jobId,
507
+ candidate_id: candidateId,
508
+ type: options.type,
509
+ custom_message: options.message,
510
+ });
511
+ spinner.stop();
512
+ if (isJsonOutput) {
513
+ outputJson({
514
+ success: true,
515
+ candidate_id: result.candidate_id,
516
+ email_type: result.email_type,
517
+ subject: result.subject,
518
+ body: result.body,
519
+ });
520
+ return;
521
+ }
522
+ console.log(chalk_1.default.blue(`📧 Email Generated\n`));
523
+ console.log(chalk_1.default.gray(` Type: ${result.email_type}`));
524
+ console.log(chalk_1.default.gray(` Subject: ${result.subject}\n`));
525
+ console.log(result.body);
526
+ }
527
+ catch (error) {
528
+ handleError(error, 'Failed to generate email');
529
+ }
530
+ });
531
+ /**
532
+ * Configure command - Update configuration
533
+ */
534
+ program
535
+ .command('configure')
536
+ .description('Update configuration settings')
537
+ .option('-t, --token <token>', 'API token')
538
+ .option('-u, --base-url <url>', 'API base URL')
539
+ .option('-w, --webhook <url>', 'Default webhook URL')
540
+ .option('-l, --leniency <number>', 'Default leniency level (1-10)')
541
+ .option('-y, --yes', 'Skip confirmation (auto-enabled for non-interactive/JSON mode)')
542
+ .option('--clear', 'Clear configuration')
543
+ .action(async (options) => {
544
+ try {
545
+ if (options.clear) {
546
+ // Auto-skip confirmation for agents using JSON output or CI mode
547
+ const autoYes = isJsonOutput || process.env.CI === 'true';
548
+ if (!options.yes && !autoYes) {
549
+ const confirmed = await confirm('Clear all configuration?');
550
+ if (!confirmed) {
551
+ console.log(chalk_1.default.yellow('Configuration cancelled.'));
552
+ return;
553
+ }
554
+ }
555
+ (0, config_1.getConfigManager)().clear();
556
+ if (isJsonOutput) {
557
+ outputJson({ success: true, message: 'Configuration cleared' });
558
+ return;
559
+ }
560
+ console.log(chalk_1.default.green('✓ Configuration cleared'));
561
+ return;
562
+ }
563
+ const updates = {};
564
+ if (options.token)
565
+ updates.apiToken = options.token;
566
+ if (options.baseUrl)
567
+ updates.baseUrl = options.baseUrl;
568
+ if (options.webhook)
569
+ updates.webhookUrl = options.webhook;
570
+ if (options.leniency)
571
+ updates.defaultLeniency = parseInt(options.leniency);
572
+ if (Object.keys(updates).length === 0) {
573
+ // Show current config
574
+ }
575
+ else {
576
+ (0, config_1.saveConfig)(updates);
577
+ }
578
+ const config = (0, config_1.getConfigManager)().load();
579
+ if (isJsonOutput) {
580
+ outputJson({
581
+ success: true,
582
+ config: {
583
+ baseUrl: config.baseUrl,
584
+ webhookUrl: config.webhookUrl,
585
+ defaultLeniency: config.defaultLeniency,
586
+ },
587
+ });
588
+ return;
589
+ }
590
+ console.log(chalk_1.default.green('✓ Configuration updated'));
591
+ console.log(chalk_1.default.gray(`\n API URL: ${config.baseUrl}`));
592
+ console.log(chalk_1.default.gray(` Webhook: ${config.webhookUrl || '(not set)'}`));
593
+ console.log(chalk_1.default.gray(` Default Leniency: ${config.defaultLeniency}`));
594
+ }
595
+ catch (error) {
596
+ handleError(error, 'Failed to update configuration');
597
+ }
598
+ });
599
+ /**
600
+ * Cancel command - Cancel a running job
601
+ */
602
+ program
603
+ .command('cancel')
604
+ .description('Cancel a running screening job')
605
+ .requiredOption('-j, --job <id>', 'Job ID to cancel')
606
+ .action(async (options) => {
607
+ try {
608
+ const api = initApi();
609
+ const jobId = parseInt(options.job);
610
+ if (isNaN(jobId)) {
611
+ throw new types_1.ValidationError('Invalid job ID');
612
+ }
613
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Cancelling job...').start() : null;
614
+ const result = await api.cancelJob(jobId);
615
+ if (spinner)
616
+ spinner.stop();
617
+ if (isJsonOutput) {
618
+ outputJson(result);
619
+ return;
620
+ }
621
+ console.log(chalk_1.default.green(`✓ Job #${jobId} cancelled`));
622
+ console.log(chalk_1.default.gray(` Status: ${result.status}`));
623
+ console.log(chalk_1.default.gray(` Message: ${result.message}`));
624
+ }
625
+ catch (error) {
626
+ handleError(error, 'Failed to cancel job');
627
+ }
628
+ });
629
+ /**
630
+ * Compare command - Compare candidates side-by-side
631
+ */
632
+ program
633
+ .command('compare')
634
+ .description('Compare multiple candidates side-by-side')
635
+ .requiredOption('-j, --job <id>', 'Job ID')
636
+ .requiredOption('-c, --candidates <ids>', 'Comma-separated candidate IDs')
637
+ .action(async (options) => {
638
+ try {
639
+ const api = initApi();
640
+ const jobId = parseInt(options.job);
641
+ const candidateIds = options.candidates.split(',').map(id => parseInt(id.trim()));
642
+ if (isNaN(jobId)) {
643
+ throw new types_1.ValidationError('Invalid job ID');
644
+ }
645
+ if (candidateIds.some(id => isNaN(id))) {
646
+ throw new types_1.ValidationError('Invalid candidate ID');
647
+ }
648
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Comparing candidates...').start() : null;
649
+ const result = await api.compareCandidates(jobId, candidateIds);
650
+ if (spinner)
651
+ spinner.stop();
652
+ if (isJsonOutput) {
653
+ outputJson(result);
654
+ return;
655
+ }
656
+ console.log(chalk_1.default.blue(`📊 Comparison (${result.candidates.length} candidates)\n`));
657
+ console.log(chalk_1.default.green(` Top Candidate: ${result.comparison.top_candidate}`));
658
+ console.log(chalk_1.default.gray(` Score Difference: ${result.comparison.score_diff}\n`));
659
+ result.candidates.forEach((cand, idx) => {
660
+ const scoreColor = cand.score >= 80 ? chalk_1.default.green : cand.score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
661
+ console.log(`${idx + 1}. ${cand.name} ${scoreColor(`(${cand.score})`)}`);
662
+ console.log(chalk_1.default.gray(` ${cand.summary?.substring(0, 80) || 'No summary'}...`));
663
+ console.log();
664
+ });
665
+ }
666
+ catch (error) {
667
+ handleError(error, 'Failed to compare candidates');
668
+ }
669
+ });
670
+ /**
671
+ * Outcome command - Report hiring outcome
672
+ */
673
+ program
674
+ .command('outcome')
675
+ .description('Report hiring outcome to improve AI accuracy')
676
+ .requiredOption('-j, --job <id>', 'Job ID')
677
+ .requiredOption('-c, --candidate <id>', 'Candidate ID')
678
+ .requiredOption('-o, --outcome <outcome>', 'Outcome (hired, rejected, withdrawn)')
679
+ .action(async (options) => {
680
+ try {
681
+ const api = initApi();
682
+ const jobId = parseInt(options.job);
683
+ const candidateId = parseInt(options.candidate);
684
+ const validOutcomes = ['hired', 'rejected', 'withdrawn'];
685
+ if (isNaN(jobId) || isNaN(candidateId)) {
686
+ throw new types_1.ValidationError('Invalid job or candidate ID');
687
+ }
688
+ if (!validOutcomes.includes(options.outcome)) {
689
+ throw new types_1.ValidationError(`Invalid outcome. Must be: ${validOutcomes.join(', ')}`);
690
+ }
691
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Reporting outcome...').start() : null;
692
+ const result = await api.reportOutcome(jobId, candidateId, options.outcome);
693
+ if (spinner)
694
+ spinner.stop();
695
+ if (isJsonOutput) {
696
+ outputJson(result);
697
+ return;
698
+ }
699
+ console.log(chalk_1.default.green('✓ Outcome reported'));
700
+ console.log(chalk_1.default.gray(` Message: ${result.message}`));
701
+ }
702
+ catch (error) {
703
+ handleError(error, 'Failed to report outcome');
704
+ }
705
+ });
706
+ /**
707
+ * Webhook-test command - Test a webhook endpoint
708
+ */
709
+ program
710
+ .command('webhook-test')
711
+ .description('Test a webhook endpoint')
712
+ .requiredOption('-u, --url <url>', 'Webhook URL to test')
713
+ .action(async (options) => {
714
+ try {
715
+ const api = initApi();
716
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Testing webhook...').start() : null;
717
+ const result = await api.testWebhook(options.url);
718
+ if (spinner)
719
+ spinner.stop();
720
+ if (isJsonOutput) {
721
+ outputJson(result);
722
+ return;
723
+ }
724
+ if (result.success) {
725
+ console.log(chalk_1.default.green('✓ Webhook test successful'));
726
+ }
727
+ else {
728
+ console.log(chalk_1.default.red('✗ Webhook test failed'));
729
+ }
730
+ console.log(chalk_1.default.gray(` Message: ${result.message}`));
731
+ if (result.response_code) {
732
+ console.log(chalk_1.default.gray(` Response Code: ${result.response_code}`));
733
+ }
734
+ }
735
+ catch (error) {
736
+ handleError(error, 'Failed to test webhook');
737
+ }
738
+ });
739
+ /**
740
+ * Rate-limit command - Check rate limit status
741
+ */
742
+ program
743
+ .command('rate-limit')
744
+ .description('Check current API rate limit status')
745
+ .action(async (options) => {
746
+ try {
747
+ const api = initApi();
748
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Checking rate limits...').start() : null;
749
+ const result = await api.getRateLimit();
750
+ if (spinner)
751
+ spinner.stop();
752
+ if (isJsonOutput) {
753
+ outputJson(result);
754
+ return;
755
+ }
756
+ console.log(chalk_1.default.blue('📊 Rate Limit Status\n'));
757
+ console.log(chalk_1.default.gray(` Limit: ${result.limit} requests/minute`));
758
+ console.log(chalk_1.default.gray(` Remaining: ${result.remaining}`));
759
+ console.log(chalk_1.default.gray(` Resets: ${result.reset_at}`));
760
+ console.log(chalk_1.default.gray(` Reset in: ${result.reset_in_seconds} seconds`));
761
+ }
762
+ catch (error) {
763
+ handleError(error, 'Failed to check rate limit');
764
+ }
765
+ });
766
+ /**
767
+ * Candidate command - Get candidate details
768
+ */
769
+ program
770
+ .command('candidate')
771
+ .description('Get details of a specific candidate')
772
+ .requiredOption('-i, --id <id>', 'Candidate ID')
773
+ .action(async (options) => {
774
+ try {
775
+ const api = initApi();
776
+ const candidateId = parseInt(options.id);
777
+ if (isNaN(candidateId)) {
778
+ throw new types_1.ValidationError('Invalid candidate ID');
779
+ }
780
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Fetching candidate...').start() : null;
781
+ const result = await api.getCandidate(candidateId);
782
+ if (spinner)
783
+ spinner.stop();
784
+ if (isJsonOutput) {
785
+ outputJson(result);
786
+ return;
787
+ }
788
+ const scoreColor = result.score >= 80 ? chalk_1.default.green : result.score >= 60 ? chalk_1.default.yellow : chalk_1.default.red;
789
+ console.log(chalk_1.default.blue(`👤 ${result.name}\n`));
790
+ console.log(chalk_1.default.gray(` Score: ${scoreColor(result.score)}`));
791
+ console.log(chalk_1.default.gray(` Email: ${result.email || 'N/A'}`));
792
+ console.log(chalk_1.default.gray(` Status: ${result.status || 'N/A'}`));
793
+ console.log(chalk_1.default.gray(`\n Summary: ${result.summary?.substring(0, 150) || 'No summary'}...`));
794
+ }
795
+ catch (error) {
796
+ handleError(error, 'Failed to fetch candidate');
797
+ }
798
+ });
799
+ /**
800
+ * Set-status command - Update candidate status
801
+ */
802
+ program
803
+ .command('set-status')
804
+ .description('Update a candidate\'s status')
805
+ .requiredOption('-i, --id <id>', 'Candidate ID')
806
+ .requiredOption('-s, --status <status>', 'New status (pending, shortlisted, rejected, interviewed, offered, hired)')
807
+ .action(async (options) => {
808
+ try {
809
+ const api = initApi();
810
+ const candidateId = parseInt(options.id);
811
+ const validStatuses = ['pending', 'shortlisted', 'rejected', 'interviewed', 'offered', 'hired'];
812
+ if (isNaN(candidateId)) {
813
+ throw new types_1.ValidationError('Invalid candidate ID');
814
+ }
815
+ if (!validStatuses.includes(options.status)) {
816
+ throw new types_1.ValidationError(`Invalid status. Must be: ${validStatuses.join(', ')}`);
817
+ }
818
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Updating status...').start() : null;
819
+ const result = await api.updateCandidateStatus(candidateId, options.status);
820
+ if (spinner)
821
+ spinner.stop();
822
+ if (isJsonOutput) {
823
+ outputJson(result);
824
+ return;
825
+ }
826
+ console.log(chalk_1.default.green(`✓ Status updated to ${options.status}`));
827
+ }
828
+ catch (error) {
829
+ handleError(error, 'Failed to update status');
830
+ }
831
+ });
832
+ /**
833
+ * Schema command - Get API schema
834
+ */
835
+ program
836
+ .command('schema')
837
+ .description('Get API schema for discovery')
838
+ .action(async (options) => {
839
+ try {
840
+ const api = initApi();
841
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Fetching schema...').start() : null;
842
+ const result = await api.getSchema();
843
+ if (spinner)
844
+ spinner.stop();
845
+ if (isJsonOutput) {
846
+ outputJson(result);
847
+ return;
848
+ }
849
+ console.log(chalk_1.default.blue(`📖 API Schema v${result.version}\n`));
850
+ result.endpoints.forEach((ep) => {
851
+ console.log(` ${chalk_1.default.bold(ep.path)}`);
852
+ console.log(chalk_1.default.gray(` Methods: ${ep.methods.join(', ')}`));
853
+ console.log(chalk_1.default.gray(` ${ep.description}`));
854
+ console.log();
855
+ });
856
+ }
857
+ catch (error) {
858
+ handleError(error, 'Failed to fetch schema');
859
+ }
860
+ });
861
+ /**
862
+ * WhoAmI command - Verify token and get profile info
863
+ */
864
+ program
865
+ .command('whoami')
866
+ .alias('profile')
867
+ .description('Verify API token and get profile info')
868
+ .action(async () => {
869
+ try {
870
+ const api = initApi();
871
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Verifying token...').start() : null;
872
+ const result = await api.get('/schema/validate');
873
+ if (spinner)
874
+ spinner.stop();
875
+ if (isJsonOutput) {
876
+ outputJson(result.data);
877
+ return;
878
+ }
879
+ console.log(chalk_1.default.blue('👤 Agent Profile\n'));
880
+ console.log(` Name: ${chalk_1.default.bold(result.data.user.name)}`);
881
+ console.log(` Email: ${result.data.user.email}`);
882
+ console.log(` Balance: ${chalk_1.default.green(result.data.credits.formatted_balance)}`);
883
+ if (result.data.credits.is_low) {
884
+ console.log(chalk_1.default.yellow(' ⚠️ Warning: Your credit balance is low.'));
885
+ }
886
+ }
887
+ catch (error) {
888
+ handleError(error, 'Token verification failed');
889
+ }
890
+ });
891
+ /**
892
+ * Agent Keys command - Manage agent API keys
893
+ */
894
+ program
895
+ .command('agent-keys')
896
+ .description('Manage agent API keys')
897
+ .option('-a, --action <action>', 'Action: list, create, show, revoke', 'list')
898
+ .option('-n, --name <name>', 'Key name (for create)')
899
+ .option('-m, --monthly-limit <amount>', 'Monthly spend limit in dollars')
900
+ .option('-d, --daily-limit <amount>', 'Daily spend limit in dollars')
901
+ .option('-l, --lifetime-limit <amount>', 'Lifetime spend limit in dollars')
902
+ .option('-i, --id <id>', 'Key ID (for show/revoke)')
903
+ .option('-p, --permissions <perms>', 'Comma-separated permissions (read,jobs,emails,credits,agent-keys)')
904
+ .action(async (options) => {
905
+ try {
906
+ const config = (0, config_1.getConfigManager)().load();
907
+ if (!config.apiToken) {
908
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
909
+ }
910
+ const api = initApi();
911
+ if (options.action === 'list') {
912
+ const response = await api.get('/agent-keys');
913
+ if (isJsonOutput) {
914
+ outputJson(response.data);
915
+ return;
916
+ }
917
+ console.log(chalk_1.default.blue('📋 Agent API Keys\n'));
918
+ response.data.keys.forEach((key) => {
919
+ console.log(` ${chalk_1.default.bold(key.name)} (${key.key_prefix}...)`);
920
+ console.log(` Status: ${key.is_active ? chalk_1.default.green('Active') : chalk_1.default.red('Inactive')}`);
921
+ console.log(` Monthly Spent: $${key.month_spent} / $${key.monthly_spend_limit || '∞'}`);
922
+ console.log(` Daily Spent: $${key.day_spent} / $${key.daily_spend_limit || '∞'}`);
923
+ console.log(` Total Spent: $${key.total_spent}`);
924
+ if (key.permissions && key.permissions.length > 0) {
925
+ console.log(` Permissions: ${key.permissions.join(', ')}`);
926
+ }
927
+ console.log();
928
+ });
929
+ }
930
+ else if (options.action === 'create') {
931
+ if (!options.name) {
932
+ throw new Error('Key name required for create. Use: --name "My Agent Key"');
933
+ }
934
+ const perms = options.permissions
935
+ ? options.permissions.split(',').map(p => p.trim().toLowerCase())
936
+ : undefined;
937
+ const response = await api.post('/agent-keys', {
938
+ name: options.name,
939
+ monthly_spend_limit: options.monthlyLimit ? parseFloat(options.monthlyLimit) : null,
940
+ daily_spend_limit: options.dailyLimit ? parseFloat(options.dailyLimit) : null,
941
+ lifetime_spend_limit: options.lifetimeLimit ? parseFloat(options.lifetimeLimit) : null,
942
+ permissions: perms,
943
+ });
944
+ if (isJsonOutput) {
945
+ outputJson(response.data);
946
+ return;
947
+ }
948
+ console.log(chalk_1.default.green('✓ Agent API key created!'));
949
+ console.log(chalk_1.default.yellow('⚠️ Save this key - it will not be shown again:'));
950
+ console.log(chalk_1.default.bold(response.data.key.key));
951
+ console.log(chalk_1.default.gray(`Key prefix: ${response.data.key.key_prefix}`));
952
+ if (perms) {
953
+ console.log(chalk_1.default.gray(`Permissions: ${perms.join(', ')}`));
954
+ }
955
+ }
956
+ else if (options.action === 'show') {
957
+ if (!options.id) {
958
+ throw new Error('Key ID required. Use: --id <key_id>');
959
+ }
960
+ const response = await api.get(`/agent-keys/${options.id}`);
961
+ if (isJsonOutput) {
962
+ outputJson(response.data);
963
+ return;
964
+ }
965
+ console.log(chalk_1.default.blue(`📋 Key: ${response.data.key.name}\n`));
966
+ console.log(` Prefix: ${response.data.key.key_prefix}`);
967
+ console.log(` Status: ${response.data.key.is_active ? 'Active' : 'Inactive'}`);
968
+ console.log(` Total Spent: $${response.data.key.total_spent}`);
969
+ console.log(` Month Spent: $${response.data.key.month_spent}`);
970
+ console.log(` Daily Spent: $${response.data.key.day_spent}`);
971
+ }
972
+ else if (options.action === 'revoke') {
973
+ if (!options.id) {
974
+ throw new Error('Key ID required. Use: --id <key_id>');
975
+ }
976
+ const response = await api.delete(`/agent-keys/${options.id}`);
977
+ if (isJsonOutput) {
978
+ outputJson(response.data);
979
+ return;
980
+ }
981
+ console.log(chalk_1.default.green('✓ Agent API key revoked'));
982
+ }
983
+ }
984
+ catch (error) {
985
+ handleError(error, 'Failed to manage agent keys');
986
+ }
987
+ });
988
+ /**
989
+ * Credits command - Manage prepaid credits
990
+ */
991
+ program
992
+ .command('credits')
993
+ .description('Manage prepaid credits')
994
+ .option('-a, --action <action>', 'Action: balance, purchase, transactions, checkout, list-packs, auto-reload-enable, auto-reload-disable', 'balance')
995
+ .option('-m, --amount <amount>', 'Amount to purchase in dollars (min $5)')
996
+ .option('-p, --pack <pack>', 'Credit pack to purchase: pouch, satchel, chest')
997
+ .option('-l, --limit <number>', 'Number of transactions to show')
998
+ .option('--threshold <number>', 'Auto-reload threshold in dollars')
999
+ .option('--payment-method-id <id>', 'Stripe payment method ID')
1000
+ .action(async (options) => {
1001
+ try {
1002
+ const config = (0, config_1.getConfigManager)().load();
1003
+ if (!config.apiToken) {
1004
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1005
+ }
1006
+ const api = initApi();
1007
+ if (options.action === 'balance') {
1008
+ const response = await api.get('/credits/balance');
1009
+ if (isJsonOutput) {
1010
+ outputJson(response.data);
1011
+ return;
1012
+ }
1013
+ console.log(chalk_1.default.blue('💳 Credit Balance\n'));
1014
+ console.log(` Balance: ${chalk_1.default.bold(response.data.formatted_balance)}`);
1015
+ console.log(` Total Purchased: $${response.data.total_purchased}`);
1016
+ console.log(` Total Spent: $${response.data.total_spent}`);
1017
+ console.log(` Auto-reload: ${response.data.auto_reload_enabled ? chalk_1.default.green('Enabled') : chalk_1.default.gray('Disabled')}`);
1018
+ }
1019
+ else if (options.action === 'list-packs') {
1020
+ const response = await api.get('/credits/packs');
1021
+ if (isJsonOutput) {
1022
+ outputJson(response.data);
1023
+ return;
1024
+ }
1025
+ console.log(chalk_1.default.blue('📦 Credit Packs\n'));
1026
+ response.data.packs.forEach((pack) => {
1027
+ console.log(` ${chalk_1.default.bold(pack.id)}: $${pack.price} for ${pack.credits} credits${pack.bonus > 0 ? ` + ${pack.bonus} bonus` : ''}`);
1028
+ });
1029
+ }
1030
+ else if (options.action === 'checkout') {
1031
+ const pack = options.pack || 'pouch';
1032
+ if (!['pouch', 'satchel', 'chest'].includes(pack)) {
1033
+ throw new Error('Invalid pack. Use: pouch, satchel, or chest');
1034
+ }
1035
+ const response = await api.post('/credits/checkout-session', {
1036
+ pack: pack,
1037
+ success_url: 'https://hiresquireai.com/credits/success',
1038
+ cancel_url: 'https://hiresquireai.com/credits/cancel',
1039
+ });
1040
+ if (isJsonOutput) {
1041
+ outputJson(response.data);
1042
+ return;
1043
+ }
1044
+ console.log(chalk_1.default.blue('🔗 Checkout Session Created\n'));
1045
+ console.log(` Pack: ${pack}`);
1046
+ console.log(chalk_1.default.cyan(` Checkout URL: ${response.data.checkout_url}`));
1047
+ console.log(chalk_1.default.gray(' Open this URL in your browser to complete payment.'));
1048
+ }
1049
+ else if (options.action === 'transactions') {
1050
+ const response = await api.get('/credits/transactions', {
1051
+ params: { limit: options.limit || 20 }
1052
+ });
1053
+ if (isJsonOutput) {
1054
+ outputJson(response.data);
1055
+ return;
1056
+ }
1057
+ console.log(chalk_1.default.blue('📜 Credit Transactions\n'));
1058
+ response.data.transactions.forEach((tx) => {
1059
+ const sign = tx.amount >= 0 ? '+' : '';
1060
+ console.log(` ${sign}$${tx.amount} - ${tx.description || tx.type}`);
1061
+ console.log(chalk_1.default.gray(` Balance: $${tx.balance_after} | ${tx.created_at}`));
1062
+ });
1063
+ }
1064
+ else if (options.action === 'auto-reload-enable') {
1065
+ if (!options.threshold || !options.amount || !options.paymentMethodId) {
1066
+ throw new Error('Auto-reload requires --threshold, --amount, and --payment-method-id');
1067
+ }
1068
+ const response = await api.post('/credits/auto-reload/enable', {
1069
+ threshold: parseFloat(options.threshold),
1070
+ amount: parseFloat(options.amount),
1071
+ payment_method_id: options.paymentMethodId
1072
+ });
1073
+ if (isJsonOutput) {
1074
+ outputJson(response.data);
1075
+ return;
1076
+ }
1077
+ console.log(chalk_1.default.green('✓ Auto-reload enabled'));
1078
+ console.log(chalk_1.default.gray(` Threshold: $${response.data.auto_reload.threshold}`));
1079
+ console.log(chalk_1.default.gray(` Amount: $${response.data.auto_reload.amount} per reload`));
1080
+ }
1081
+ else if (options.action === 'auto-reload-disable') {
1082
+ const response = await api.post('/credits/auto-reload/disable');
1083
+ if (isJsonOutput) {
1084
+ outputJson(response.data);
1085
+ return;
1086
+ }
1087
+ console.log(chalk_1.default.green('✓ Auto-reload disabled'));
1088
+ }
1089
+ else if (options.action === 'purchase') {
1090
+ if (!options.paymentMethodId) {
1091
+ throw new Error('Direct purchase requires --payment-method-id');
1092
+ }
1093
+ const requestData = {
1094
+ payment_method_id: options.paymentMethodId
1095
+ };
1096
+ if (options.pack) {
1097
+ requestData.pack = options.pack;
1098
+ }
1099
+ else if (options.amount) {
1100
+ requestData.amount = parseFloat(options.amount);
1101
+ }
1102
+ else {
1103
+ throw new Error('Purchase requires either --pack or --amount');
1104
+ }
1105
+ const response = await api.post('/credits/purchase', requestData);
1106
+ if (isJsonOutput) {
1107
+ outputJson(response.data);
1108
+ return;
1109
+ }
1110
+ console.log(chalk_1.default.green('✓ Purchase successful'));
1111
+ console.log(chalk_1.default.gray(` New balance: ${response.data.balance}`));
1112
+ }
1113
+ }
1114
+ catch (error) {
1115
+ handleError(error, 'Failed to manage credits');
1116
+ }
1117
+ });
1118
+ // ============================================================================
1119
+ // Calendar Commands
1120
+ // ============================================================================
1121
+ /**
1122
+ * Calendar:connect command - Connect a calendar provider
1123
+ */
1124
+ program
1125
+ .command('calendar:connect <provider>')
1126
+ .description('Connect a calendar provider (calendly, calcom)')
1127
+ .option('-t, --token <token>', 'API token/key for calendly/calcom')
1128
+ .option('-c, --calendar-id <id>', 'Calendar ID (optional)')
1129
+ .action(async (provider, options) => {
1130
+ try {
1131
+ const config = (0, config_1.getConfigManager)().load();
1132
+ if (!config.apiToken) {
1133
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1134
+ }
1135
+ const api = initApi();
1136
+ const params = { provider };
1137
+ if (options.token)
1138
+ params.api_key = options.token;
1139
+ if (options.calendarId)
1140
+ params.calendar_id = options.calendarId;
1141
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Connecting calendar...').start() : null;
1142
+ const response = await api.createCalendarConnection(params);
1143
+ if (spinner)
1144
+ spinner.stop();
1145
+ if (isJsonOutput) {
1146
+ outputJson(response);
1147
+ return;
1148
+ }
1149
+ console.log(chalk_1.default.green('✓ Calendar connection created'));
1150
+ console.log(` Provider: ${chalk_1.default.bold(response.data.provider)}`);
1151
+ }
1152
+ catch (error) {
1153
+ handleError(error, 'Failed to connect calendar');
1154
+ }
1155
+ });
1156
+ /**
1157
+ * Calendar:list command - List calendar connections
1158
+ */
1159
+ program
1160
+ .command('calendar:list')
1161
+ .description('List calendar connections')
1162
+ .action(async () => {
1163
+ try {
1164
+ const config = (0, config_1.getConfigManager)().load();
1165
+ if (!config.apiToken) {
1166
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1167
+ }
1168
+ const api = initApi();
1169
+ const response = await api.listCalendarConnections();
1170
+ if (isJsonOutput) {
1171
+ outputJson(response);
1172
+ return;
1173
+ }
1174
+ console.log(chalk_1.default.blue('📅 Calendar Connections\n'));
1175
+ response.data.forEach((conn) => {
1176
+ console.log(` ${chalk_1.default.bold(conn.provider)} - ${conn.status}`);
1177
+ });
1178
+ }
1179
+ catch (error) {
1180
+ handleError(error, 'Failed to list calendar connections');
1181
+ }
1182
+ });
1183
+ // ============================================================================
1184
+ // Interview Commands
1185
+ // ============================================================================
1186
+ /**
1187
+ * Interviews:schedule command - Schedule an interview
1188
+ */
1189
+ program
1190
+ .command('interviews:schedule')
1191
+ .description('Schedule an interview')
1192
+ .requiredOption('-j, --job <id>', 'Job ID')
1193
+ .requiredOption('-c, --candidate <id>', 'Candidate ID')
1194
+ .requiredOption('-t, --time <datetime>', 'Scheduled time (ISO 8601)')
1195
+ .option('-d, --duration <minutes>', 'Duration in minutes', '60')
1196
+ .option('-p, --provider <provider>', 'Calendar provider (calendly, calcom)')
1197
+ .action(async (options) => {
1198
+ try {
1199
+ const config = (0, config_1.getConfigManager)().load();
1200
+ if (!config.apiToken) {
1201
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1202
+ }
1203
+ const api = initApi();
1204
+ const params = {
1205
+ job_id: parseInt(options.job),
1206
+ candidate_id: parseInt(options.candidate),
1207
+ scheduled_at: options.time,
1208
+ duration_minutes: parseInt(options.duration),
1209
+ };
1210
+ if (options.provider)
1211
+ params.provider = options.provider;
1212
+ const response = await api.createInterview(params);
1213
+ if (isJsonOutput) {
1214
+ outputJson(response);
1215
+ return;
1216
+ }
1217
+ console.log(chalk_1.default.green('✓ Interview scheduled'));
1218
+ console.log(` Interview ID: ${chalk_1.default.bold(response.data.id)}`);
1219
+ if (response.data.meeting_link) {
1220
+ console.log(` Meeting Link: ${chalk_1.default.cyan(response.data.meeting_link)}`);
1221
+ }
1222
+ }
1223
+ catch (error) {
1224
+ handleError(error, 'Failed to schedule interview');
1225
+ }
1226
+ });
1227
+ /**
1228
+ * Interviews:list command - List interviews
1229
+ */
1230
+ program
1231
+ .command('interviews:list')
1232
+ .description('List interviews')
1233
+ .option('-j, --job <id>', 'Filter by job ID')
1234
+ .action(async (options) => {
1235
+ try {
1236
+ const config = (0, config_1.getConfigManager)().load();
1237
+ if (!config.apiToken) {
1238
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1239
+ }
1240
+ const api = initApi();
1241
+ const params = {};
1242
+ if (options.job)
1243
+ params.job_id = parseInt(options.job);
1244
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Fetching interviews...').start() : null;
1245
+ const response = await api.listInterviews(params.job_id);
1246
+ if (spinner)
1247
+ spinner.stop();
1248
+ if (isJsonOutput) {
1249
+ outputJson(response);
1250
+ return;
1251
+ }
1252
+ console.log(chalk_1.default.blue('📅 Interviews\n'));
1253
+ response.data.forEach((interview) => {
1254
+ console.log(` ID: ${interview.id} | Job: ${interview.job_posting_id} | Candidate: ${interview.candidate_id}`);
1255
+ console.log(` Scheduled: ${interview.scheduled_at} | Status: ${interview.status}`);
1256
+ if (interview.meeting_link) {
1257
+ console.log(` Link: ${interview.meeting_link}`);
1258
+ }
1259
+ });
1260
+ }
1261
+ catch (error) {
1262
+ handleError(error, 'Failed to list interviews');
1263
+ }
1264
+ });
1265
+ // ============================================================================
1266
+ // Meeting Commands
1267
+ // ============================================================================
1268
+ /**
1269
+ * Meetings:link command - Generate a meeting link
1270
+ */
1271
+ program
1272
+ .command('meetings:link')
1273
+ .description('Generate a meeting link')
1274
+ .requiredOption('-p, --provider <provider>', 'Calendar provider (calendly, calcom)')
1275
+ .requiredOption('-t, --topic <topic>', 'Meeting topic')
1276
+ .option('-d, --duration <minutes>', 'Duration in minutes', '60')
1277
+ .action(async (options) => {
1278
+ try {
1279
+ const config = (0, config_1.getConfigManager)().load();
1280
+ if (!config.apiToken) {
1281
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1282
+ }
1283
+ const api = initApi();
1284
+ const params = {
1285
+ provider: options.provider,
1286
+ topic: options.topic,
1287
+ duration: parseInt(options.duration),
1288
+ };
1289
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Generating meeting link...').start() : null;
1290
+ const response = await api.generateMeetingLink(params);
1291
+ if (spinner)
1292
+ spinner.stop();
1293
+ if (isJsonOutput) {
1294
+ outputJson(response);
1295
+ return;
1296
+ }
1297
+ console.log(chalk_1.default.green('✓ Meeting link generated'));
1298
+ if (response.data) {
1299
+ console.log(` Link: ${chalk_1.default.cyan(response.data.link)}`);
1300
+ console.log(` Provider: ${response.data.provider}`);
1301
+ }
1302
+ }
1303
+ catch (error) {
1304
+ handleError(error, 'Failed to generate meeting link');
1305
+ }
1306
+ });
1307
+ /**
1308
+ * Estimate command - Estimate screening cost
1309
+ */
1310
+ program
1311
+ .command('estimate')
1312
+ .description('Estimate screening cost')
1313
+ .option('-c, --candidates <number>', 'Number of candidates', '10')
1314
+ .action(async (options) => {
1315
+ try {
1316
+ const config = (0, config_1.getConfigManager)().load();
1317
+ if (!config.apiToken) {
1318
+ throw new Error('API token not configured. Run: hiresquire init -t <token>');
1319
+ }
1320
+ const api = initApi();
1321
+ const spinner = !isJsonOutput ? (0, ora_1.default)('Estimating cost...').start() : null;
1322
+ const response = await api.get('/credits/estimate', {
1323
+ params: { candidate_count: parseInt(options.candidates) }
1324
+ });
1325
+ if (spinner)
1326
+ spinner.stop();
1327
+ if (isJsonOutput) {
1328
+ outputJson(response.data);
1329
+ return;
1330
+ }
1331
+ console.log(chalk_1.default.blue('💰 Cost Estimate\n'));
1332
+ console.log(` Candidates: ${response.data.candidate_count}`);
1333
+ console.log(` Cost per candidate: $${response.data.cost_per_candidate}`);
1334
+ console.log(chalk_1.default.bold(` Total: $${response.data.total_cost}`));
1335
+ }
1336
+ catch (error) {
1337
+ handleError(error, 'Failed to estimate cost');
1338
+ }
1339
+ });
1340
+ // ============================================================================
1341
+ // Global Options
1342
+ // ============================================================================
1343
+ program
1344
+ .option('--json', 'Output as JSON')
1345
+ .option('-v, --verbose', 'Enable verbose logging')
1346
+ .version('1.2.1');
1347
+ // ============================================================================
1348
+ // Parse & Execute
1349
+ // ============================================================================
1350
+ // Check for JSON flag in argv (before command runs)
1351
+ if (process.argv.includes('--json')) {
1352
+ isJsonOutput = true;
1353
+ }
1354
+ // Show help if no command provided
1355
+ const commandArgs = process.argv.slice(2).filter(arg => !arg.startsWith('-'));
1356
+ if (commandArgs.length === 0) {
1357
+ if (isJsonOutput) {
1358
+ outputJson({
1359
+ success: false,
1360
+ error: "No command provided",
1361
+ commands: program.commands.map(cmd => cmd.name())
1362
+ });
1363
+ process.exit(1);
1364
+ }
1365
+ else {
1366
+ program.help();
1367
+ }
1368
+ }
1369
+ program.parse(process.argv);
1370
+ //# sourceMappingURL=index.js.map