hiresquire-cli 1.1.0 → 1.2.2

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