huntr-cli 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.env.example +7 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  5. package/.github/labels.json +92 -0
  6. package/.github/pull_request_template.md +64 -0
  7. package/.github/workflows/ci.yml +87 -0
  8. package/.github/workflows/labels.yml +27 -0
  9. package/.github/workflows/manual-publish.yml +105 -0
  10. package/.github/workflows/publish.yml +57 -0
  11. package/.github/workflows/release.yml +124 -0
  12. package/.github/workflows/security-audit.yml +44 -0
  13. package/.husky/pre-commit +12 -0
  14. package/.husky/pre-push +27 -0
  15. package/.lintstagedrc.json +3 -0
  16. package/AGENTS.md +449 -0
  17. package/CHANGELOG.md +38 -0
  18. package/CHANGES.md +259 -0
  19. package/LICENSE +15 -0
  20. package/PUBLISHING.md +191 -0
  21. package/README.md +385 -0
  22. package/ROADMAP.md +158 -0
  23. package/SETUP-COMPLETE.md +446 -0
  24. package/WORKFLOW-SUMMARY.md +368 -0
  25. package/completions/_huntr +168 -0
  26. package/completions/huntr.1 +266 -0
  27. package/completions/huntr.bash +91 -0
  28. package/dist/api/client.d.ts +14 -0
  29. package/dist/api/client.d.ts.map +1 -0
  30. package/dist/api/client.js +74 -0
  31. package/dist/api/client.js.map +1 -0
  32. package/dist/api/personal/activities.d.ts +20 -0
  33. package/dist/api/personal/activities.d.ts.map +1 -0
  34. package/dist/api/personal/activities.js +50 -0
  35. package/dist/api/personal/activities.js.map +1 -0
  36. package/dist/api/personal/boards.d.ts +9 -0
  37. package/dist/api/personal/boards.d.ts.map +1 -0
  38. package/dist/api/personal/boards.js +16 -0
  39. package/dist/api/personal/boards.js.map +1 -0
  40. package/dist/api/personal/index.d.ts +17 -0
  41. package/dist/api/personal/index.d.ts.map +1 -0
  42. package/dist/api/personal/index.js +37 -0
  43. package/dist/api/personal/index.js.map +1 -0
  44. package/dist/api/personal/jobs.d.ts +13 -0
  45. package/dist/api/personal/jobs.d.ts.map +1 -0
  46. package/dist/api/personal/jobs.js +31 -0
  47. package/dist/api/personal/jobs.js.map +1 -0
  48. package/dist/api/personal/user.d.ts +8 -0
  49. package/dist/api/personal/user.d.ts.map +1 -0
  50. package/dist/api/personal/user.js +13 -0
  51. package/dist/api/personal/user.js.map +1 -0
  52. package/dist/cli.d.ts +3 -0
  53. package/dist/cli.d.ts.map +1 -0
  54. package/dist/cli.js +501 -0
  55. package/dist/cli.js.map +1 -0
  56. package/dist/commands/capture-session.d.ts +10 -0
  57. package/dist/commands/capture-session.d.ts.map +1 -0
  58. package/dist/commands/capture-session.js +478 -0
  59. package/dist/commands/capture-session.js.map +1 -0
  60. package/dist/config/clerk-session-manager.d.ts +44 -0
  61. package/dist/config/clerk-session-manager.d.ts.map +1 -0
  62. package/dist/config/clerk-session-manager.js +232 -0
  63. package/dist/config/clerk-session-manager.js.map +1 -0
  64. package/dist/config/config-manager.d.ts +15 -0
  65. package/dist/config/config-manager.d.ts.map +1 -0
  66. package/dist/config/config-manager.js +51 -0
  67. package/dist/config/config-manager.js.map +1 -0
  68. package/dist/config/keychain-manager.d.ts +6 -0
  69. package/dist/config/keychain-manager.d.ts.map +1 -0
  70. package/dist/config/keychain-manager.js +37 -0
  71. package/dist/config/keychain-manager.js.map +1 -0
  72. package/dist/config/token-capture.d.ts +11 -0
  73. package/dist/config/token-capture.d.ts.map +1 -0
  74. package/dist/config/token-capture.js +252 -0
  75. package/dist/config/token-capture.js.map +1 -0
  76. package/dist/config/token-manager.d.ts +38 -0
  77. package/dist/config/token-manager.d.ts.map +1 -0
  78. package/dist/config/token-manager.js +153 -0
  79. package/dist/config/token-manager.js.map +1 -0
  80. package/dist/lib/list-options.d.ts +69 -0
  81. package/dist/lib/list-options.d.ts.map +1 -0
  82. package/dist/lib/list-options.js +299 -0
  83. package/dist/lib/list-options.js.map +1 -0
  84. package/dist/types/personal.d.ts +113 -0
  85. package/dist/types/personal.d.ts.map +1 -0
  86. package/dist/types/personal.js +4 -0
  87. package/dist/types/personal.js.map +1 -0
  88. package/docs/AUTOMATIC-PUBLISHING.md +520 -0
  89. package/docs/CHANGELOG-AUTOMATION.md +418 -0
  90. package/docs/CI-CD-SETUP.md +582 -0
  91. package/docs/DEV-SETUP.md +512 -0
  92. package/docs/ENHANCEMENT-PLAN.md +204 -0
  93. package/docs/ENTITY-TYPES.md +462 -0
  94. package/docs/GITHUB-ACTIONS-GUIDE.md +367 -0
  95. package/docs/NPM-PUBLISHING.md +324 -0
  96. package/docs/OUTPUT-EXAMPLES.md +414 -0
  97. package/docs/OUTPUT-FORMATS.md +299 -0
  98. package/docs/TESTING.md +216 -0
  99. package/eslint.config.js +68 -0
  100. package/package.json +64 -0
  101. package/src/api/client.ts +88 -0
  102. package/src/api/personal/activities.ts +66 -0
  103. package/src/api/personal/boards.ts +14 -0
  104. package/src/api/personal/index.ts +25 -0
  105. package/src/api/personal/jobs.ts +33 -0
  106. package/src/api/personal/user.ts +10 -0
  107. package/src/cli.ts +487 -0
  108. package/src/commands/capture-session.ts +582 -0
  109. package/src/config/clerk-session-manager.ts +263 -0
  110. package/src/config/config-manager.ts +56 -0
  111. package/src/config/keychain-manager.ts +30 -0
  112. package/src/config/token-capture.ts +233 -0
  113. package/src/config/token-manager.ts +139 -0
  114. package/src/lib/list-options.ts +370 -0
  115. package/src/types/personal.ts +114 -0
  116. package/tests/example.test.ts +130 -0
  117. package/tsconfig.json +19 -0
package/src/cli.ts ADDED
@@ -0,0 +1,487 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { HuntrPersonalApi } from './api/personal';
5
+ import { TokenManager } from './config/token-manager';
6
+ import { ClerkSessionManager } from './config/clerk-session-manager';
7
+ import { captureSession, checkCdpSession } from './commands/capture-session';
8
+ import {
9
+ parseListOptions,
10
+ validateFields,
11
+ formatTableWithFields,
12
+ formatCsvWithFields,
13
+ formatJsonWithFields,
14
+ formatPdf,
15
+ formatExcel,
16
+ } from './lib/list-options';
17
+
18
+ const program = new Command();
19
+ const tokenManager = new TokenManager();
20
+
21
+ async function getApi(token?: string): Promise<HuntrPersonalApi> {
22
+ const provider = await tokenManager.getTokenProvider({ token });
23
+ return new HuntrPersonalApi(provider);
24
+ }
25
+
26
+ program
27
+ .name('huntr')
28
+ .description('CLI tool for Huntr')
29
+ .version('1.0.0')
30
+ .option('-t, --token <token>', 'API token (overrides all other sources)');
31
+
32
+ // ── me ───────────────────────────────────────────────────────────────────────
33
+
34
+ program
35
+ .command('me')
36
+ .description('Show your user profile')
37
+ .option('-j, --json', 'Output as JSON')
38
+ .action(async (options, command) => {
39
+ try {
40
+ const api = await getApi(command.parent?.opts().token);
41
+ const profile = await api.user.getProfile();
42
+ if (options.json) {
43
+ console.log(JSON.stringify(profile, null, 2));
44
+ } else {
45
+ console.log(`Name: ${profile.givenName ?? profile.firstName ?? ''} ${profile.familyName ?? profile.lastName ?? ''}`);
46
+ console.log(`Email: ${profile.email}`);
47
+ console.log(`ID: ${profile.id}`);
48
+ }
49
+ } catch (error) {
50
+ console.error('Error:', error instanceof Error ? error.message : error);
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ // ── boards ───────────────────────────────────────────────────────────────────
56
+
57
+ const boards = program.command('boards').description('Manage your boards');
58
+
59
+ boards
60
+ .command('list')
61
+ .description('List all your boards')
62
+ .option('-f, --format <format>', 'Output format: table (default), json, csv, pdf, excel')
63
+ .option('-j, --json', 'Output as JSON (legacy, same as --format json)')
64
+ .option('--fields <fields>', 'Comma-separated list of fields to include')
65
+ .action(async (options, command) => {
66
+ try {
67
+ const AVAILABLE_FIELDS = ['ID', 'Name', 'Created'];
68
+ const listOpts = parseListOptions(options);
69
+ const fields = validateFields(AVAILABLE_FIELDS, listOpts.fields);
70
+
71
+ const api = await getApi(command.parent?.parent?.opts().token);
72
+ const response = await api.boards.list();
73
+ const boardsList = Array.isArray(response) ? response : (response as any).data ?? [];
74
+
75
+ if (boardsList.length === 0) {
76
+ console.log('No boards found.');
77
+ return;
78
+ }
79
+
80
+ const rows = boardsList.map((b: any) => ({
81
+ ID: b.id,
82
+ Name: b.name ?? 'N/A',
83
+ Created: new Date(b.createdAt).toLocaleDateString(),
84
+ }));
85
+
86
+ if (listOpts.format === 'json') {
87
+ console.log(formatJsonWithFields(rows, fields));
88
+ } else if (listOpts.format === 'csv') {
89
+ console.log(formatCsvWithFields(rows, fields));
90
+ } else if (listOpts.format === 'pdf') {
91
+ const buffer = formatPdf(rows, fields, 'Boards List');
92
+ process.stdout.write(buffer);
93
+ } else if (listOpts.format === 'excel') {
94
+ const buffer = await formatExcel(rows, fields, 'Boards');
95
+ process.stdout.write(buffer);
96
+ } else {
97
+ console.log(formatTableWithFields(rows, fields));
98
+ }
99
+ } catch (error) {
100
+ console.error('Error:', error instanceof Error ? error.message : error);
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ boards
106
+ .command('get')
107
+ .description('Get details of a specific board')
108
+ .argument('<board-id>', 'Board ID')
109
+ .option('-j, --json', 'Output as JSON')
110
+ .action(async (boardId, options, command) => {
111
+ try {
112
+ const api = await getApi(command.parent?.parent?.opts().token);
113
+ const board = await api.boards.get(boardId);
114
+ if (options.json) {
115
+ console.log(JSON.stringify(board, null, 2));
116
+ } else {
117
+ console.log(`Board: ${board.name}`);
118
+ console.log(`ID: ${board.id}`);
119
+ console.log(`Created: ${new Date(board.createdAt).toLocaleString()}`);
120
+ if (board.lists?.length) {
121
+ console.log('\nLists:');
122
+ board.lists.forEach(l => console.log(` - ${l.name}`));
123
+ }
124
+ }
125
+ } catch (error) {
126
+ console.error('Error:', error instanceof Error ? error.message : error);
127
+ process.exit(1);
128
+ }
129
+ });
130
+
131
+ // ── jobs ─────────────────────────────────────────────────────────────────────
132
+
133
+ const jobs = program.command('jobs').description('Manage jobs on your boards');
134
+
135
+ jobs
136
+ .command('list')
137
+ .description('List jobs on a board')
138
+ .argument('<board-id>', 'Board ID')
139
+ .option('-f, --format <format>', 'Output format: table (default), json, csv, pdf, excel')
140
+ .option('-j, --json', 'Output as JSON (legacy, same as --format json)')
141
+ .option('--fields <fields>', 'Comma-separated list of fields to include')
142
+ .action(async (boardId, options, command) => {
143
+ try {
144
+ const AVAILABLE_FIELDS = ['ID', 'Title', 'URL', 'Created'];
145
+ const listOpts = parseListOptions(options);
146
+ const fields = validateFields(AVAILABLE_FIELDS, listOpts.fields);
147
+
148
+ const api = await getApi(command.parent?.parent?.opts().token);
149
+ const jobsList = await api.jobs.listByBoardFlat(boardId);
150
+
151
+ if (jobsList.length === 0) {
152
+ console.log('No jobs found.');
153
+ return;
154
+ }
155
+
156
+ const rows = jobsList.map(j => ({
157
+ ID: j.id,
158
+ Title: j.title,
159
+ URL: j.url ?? 'N/A',
160
+ Created: new Date(j.createdAt).toLocaleDateString(),
161
+ }));
162
+
163
+ if (listOpts.format === 'json') {
164
+ console.log(formatJsonWithFields(rows, fields));
165
+ } else if (listOpts.format === 'csv') {
166
+ console.log(formatCsvWithFields(rows, fields));
167
+ } else if (listOpts.format === 'pdf') {
168
+ const buffer = formatPdf(rows, fields, 'Jobs List');
169
+ process.stdout.write(buffer);
170
+ } else if (listOpts.format === 'excel') {
171
+ const buffer = await formatExcel(rows, fields, 'Jobs');
172
+ process.stdout.write(buffer);
173
+ } else {
174
+ console.log(formatTableWithFields(rows, fields));
175
+ }
176
+ } catch (error) {
177
+ console.error('Error:', error instanceof Error ? error.message : error);
178
+ process.exit(1);
179
+ }
180
+ });
181
+
182
+ jobs
183
+ .command('get')
184
+ .description('Get details of a specific job')
185
+ .argument('<board-id>', 'Board ID')
186
+ .argument('<job-id>', 'Job ID')
187
+ .option('-j, --json', 'Output as JSON')
188
+ .action(async (boardId, jobId, options, command) => {
189
+ try {
190
+ const api = await getApi(command.parent?.parent?.opts().token);
191
+ const job = await api.jobs.get(boardId, jobId);
192
+ if (options.json) {
193
+ console.log(JSON.stringify(job, null, 2));
194
+ } else {
195
+ console.log('\nJob Details:');
196
+ console.log(` Title: ${job.title}`);
197
+ console.log(` URL: ${job.url ?? 'N/A'}`);
198
+ console.log(` Location: ${job.location?.address ?? 'N/A'}`);
199
+ if (job.salary) {
200
+ console.log(` Salary: ${job.salary.min ?? 'N/A'} - ${job.salary.max ?? 'N/A'} ${job.salary.currency ?? ''}`);
201
+ }
202
+ console.log(` Created: ${new Date(job.createdAt).toLocaleString()}`);
203
+ }
204
+ } catch (error) {
205
+ console.error('Error:', error instanceof Error ? error.message : error);
206
+ process.exit(1);
207
+ }
208
+ });
209
+
210
+ // ── activities ───────────────────────────────────────────────────────────────
211
+
212
+ const activities = program.command('activities').description('View your board activity log');
213
+
214
+ activities
215
+ .command('list')
216
+ .description('List actions for a board')
217
+ .argument('<board-id>', 'Board ID')
218
+ .option('-f, --format <format>', 'Output format: table (default), json, csv, pdf, excel')
219
+ .option('-d, --days <days>', 'Filter to last N days (e.g. 7 for past week)')
220
+ .option('-w, --week', 'Filter to last 7 days (legacy, same as --days 7)')
221
+ .option('--types <types>', 'Comma-separated action types (e.g. JOB_MOVED,NOTE_CREATED)')
222
+ .option('--fields <fields>', 'Comma-separated list of fields to include')
223
+ .option('-j, --json', 'Output as JSON (legacy, same as --format json)')
224
+ .action(async (boardId, options, command) => {
225
+ try {
226
+ const AVAILABLE_FIELDS = ['Date', 'Type', 'Company', 'Job', 'Status'];
227
+ const listOpts = parseListOptions(options);
228
+ const fields = validateFields(AVAILABLE_FIELDS, listOpts.fields);
229
+
230
+ const api = await getApi(command.parent?.parent?.opts().token);
231
+
232
+ const apiOpts: { since?: Date; types?: string[] } = {};
233
+ if (listOpts.days) {
234
+ apiOpts.since = new Date(Date.now() - listOpts.days * 24 * 60 * 60 * 1000);
235
+ }
236
+ if (listOpts.types) {
237
+ apiOpts.types = listOpts.types;
238
+ }
239
+
240
+ const actions = await api.actions.listByBoardFlat(boardId, apiOpts);
241
+
242
+ if (actions.length === 0) {
243
+ console.log('No activities found.');
244
+ return;
245
+ }
246
+
247
+ const rows = actions.map(a => ({
248
+ Date: new Date(a.date || a.createdAt).toISOString().substring(0, 16),
249
+ Type: a.actionType,
250
+ Company: a.data?.company?.name ?? '',
251
+ Job: (a.data?.job?.title ?? '').substring(0, 40),
252
+ Status: a.data?.toList?.name ?? '',
253
+ }));
254
+
255
+ if (listOpts.format === 'json') {
256
+ console.log(formatJsonWithFields(rows, fields));
257
+ } else if (listOpts.format === 'csv') {
258
+ console.log(formatCsvWithFields(rows, fields));
259
+ } else if (listOpts.format === 'pdf') {
260
+ const buffer = formatPdf(rows, fields, 'Activities List');
261
+ process.stdout.write(buffer);
262
+ } else if (listOpts.format === 'excel') {
263
+ const buffer = await formatExcel(rows, fields, 'Activities');
264
+ process.stdout.write(buffer);
265
+ } else {
266
+ console.log(formatTableWithFields(rows, fields));
267
+ }
268
+ } catch (error) {
269
+ console.error('Error:', error instanceof Error ? error.message : error);
270
+ process.exit(1);
271
+ }
272
+ });
273
+
274
+ activities
275
+ .command('week-csv')
276
+ .description('Export last 7 days of activity as CSV')
277
+ .argument('<board-id>', 'Board ID')
278
+ .action(async (boardId, options, command) => {
279
+ try {
280
+ const api = await getApi(command.parent?.parent?.opts().token);
281
+ const rows = await api.actions.weekSummary(boardId);
282
+ const lines = ['Date,Action,Company,Job Title,Status,Job URL'];
283
+ for (const r of rows) {
284
+ lines.push([
285
+ r.date,
286
+ r.actionType,
287
+ `"${r.company.replace(/"/g, '""')}"`,
288
+ `"${r.jobTitle.replace(/"/g, '""')}"`,
289
+ r.status,
290
+ r.url,
291
+ ].join(','));
292
+ }
293
+ console.log(lines.join('\n'));
294
+ } catch (error) {
295
+ console.error('Error:', error instanceof Error ? error.message : error);
296
+ process.exit(1);
297
+ }
298
+ });
299
+
300
+ // ── config ───────────────────────────────────────────────────────────────────
301
+
302
+ const config = program.command('config').description('Manage CLI configuration');
303
+
304
+ config
305
+ .command('set-token')
306
+ .description('Save API token to config file or keychain')
307
+ .argument('<token>', 'API token to save')
308
+ .option('-k, --keychain', 'Save to macOS Keychain instead of config file')
309
+ .action(async (token, options) => {
310
+ try {
311
+ const location = options.keychain ? 'keychain' : 'config';
312
+ await tokenManager.saveToken(token, location);
313
+ const locationName = options.keychain ? 'macOS Keychain' : '~/.huntr/config.json';
314
+ console.log(`✓ Token saved to ${locationName}`);
315
+ } catch (error) {
316
+ console.error('Error:', error instanceof Error ? error.message : error);
317
+ process.exit(1);
318
+ }
319
+ });
320
+
321
+ config
322
+ .command('capture-session')
323
+ .description('Capture Clerk session from your browser automatically (recommended)')
324
+ .action(async () => {
325
+ try {
326
+ await captureSession();
327
+ } catch (error) {
328
+ console.error('Error:', error instanceof Error ? error.message : error);
329
+ process.exit(1);
330
+ }
331
+ });
332
+
333
+ config
334
+ .command('check-cdp')
335
+ .description('Check Chrome DevTools + Clerk cookie visibility for session capture')
336
+ .action(async () => {
337
+ try {
338
+ await checkCdpSession();
339
+ } catch (error) {
340
+ console.error('Error:', error instanceof Error ? error.message : error);
341
+ process.exit(1);
342
+ }
343
+ });
344
+
345
+ config
346
+ .command('set-session')
347
+ .description('Save Clerk session cookie for automatic JWT refresh (recommended)')
348
+ .argument('<session-cookie>', 'Value of the __session cookie from your browser')
349
+ .argument('[session-id]', 'Clerk session ID (sess_...) — auto-detected from cookie if omitted')
350
+ .addHelpText('after', `
351
+ How to get your __session cookie:
352
+ 1. Open huntr.co in Chrome and log in
353
+ 2. Open DevTools → Application → Cookies → https://huntr.co
354
+ 3. Find the cookie named __session and copy its Value (the long JWT string)
355
+ 4. Run: huntr config set-session <value>
356
+
357
+ The session ID (sess_...) is extracted automatically from the JWT.
358
+ The session persists for weeks; re-run set-session if you get auth errors.
359
+ `)
360
+ .action(async (sessionCookie, sessionId) => {
361
+ try {
362
+ const mgr = tokenManager.clerkSession;
363
+
364
+ // Auto-detect session ID from the JWT payload (sid claim)
365
+ let resolvedSessionId = sessionId as string | undefined;
366
+ if (!resolvedSessionId) {
367
+ const detected = ClerkSessionManager.extractSessionId(sessionCookie);
368
+ if (detected) {
369
+ resolvedSessionId = detected;
370
+ console.log(` Session ID detected: ${resolvedSessionId}`);
371
+ } else {
372
+ console.error(
373
+ 'Could not auto-detect session ID from the __session JWT.\n' +
374
+ 'Find it in the browser console: Clerk.session.id\n' +
375
+ 'Then run: huntr config set-session <cookie> <session-id>',
376
+ );
377
+ process.exit(1);
378
+ }
379
+ }
380
+
381
+ // Strip prefix if user pasted "__session=..." prefix
382
+ const rawCookie = sessionCookie.startsWith('__session=')
383
+ ? sessionCookie.slice('__session='.length)
384
+ : sessionCookie;
385
+
386
+ await mgr.saveSession(rawCookie, resolvedSessionId!);
387
+
388
+ // Verify it works immediately
389
+ console.log(' Testing session…');
390
+ try {
391
+ const token = await mgr.getFreshToken();
392
+ console.log(`✓ Session saved and verified (token starts with: ${token.substring(0, 20)}…)`);
393
+ console.log(' Tokens will auto-refresh before each command.');
394
+ } catch (err) {
395
+ console.warn(`⚠ Session saved but test refresh failed: ${err instanceof Error ? err.message : err}`);
396
+ console.warn(' Your cookie may be expired. Try extracting it again from the browser.');
397
+ }
398
+ } catch (error) {
399
+ console.error('Error:', error instanceof Error ? error.message : error);
400
+ process.exit(1);
401
+ }
402
+ });
403
+
404
+ config
405
+ .command('test-session')
406
+ .description('Test the stored Clerk session by fetching a fresh token and calling /me')
407
+ .action(async () => {
408
+ try {
409
+ const mgr = tokenManager.clerkSession;
410
+ if (!(await mgr.hasSession())) {
411
+ console.error('No session stored. Run: huntr config set-session <__session-cookie>');
412
+ process.exit(1);
413
+ }
414
+ process.stdout.write(' Refreshing token from Clerk… ');
415
+ const token = await mgr.getFreshToken();
416
+ console.log(`✓ (${token.substring(0, 20)}…)`);
417
+ process.stdout.write(' Calling Huntr API /me… ');
418
+ const api = new (await import('./api/personal')).HuntrPersonalApi(token);
419
+ const profile = await api.user.getProfile();
420
+ console.log('✓');
421
+ console.log(`\n Logged in as: ${profile.givenName ?? profile.firstName ?? ''} ${profile.familyName ?? profile.lastName ?? ''} <${profile.email}>`);
422
+ console.log(' Session is working correctly. Tokens auto-refresh before each command.');
423
+ } catch (error) {
424
+ console.error('\nFailed:', error instanceof Error ? error.message : error);
425
+ process.exit(1);
426
+ }
427
+ });
428
+
429
+ config
430
+ .command('show-token')
431
+ .description('Show which authentication sources are configured')
432
+ .action(async () => {
433
+ try {
434
+ const sources = await tokenManager.showTokenSources();
435
+ console.log('\nConfigured authentication sources:');
436
+ console.log(` Environment variable (HUNTR_API_TOKEN): ${sources.env ? '✓ Set' : '✗ Not set'}`);
437
+ console.log(` Clerk session (auto-refresh): ${sources.clerkSession ? '✓ Set' : '✗ Not set'}`);
438
+ console.log(` Config file (~/.huntr/config.json): ${sources.config ? '✓ Set' : '✗ Not set'}`);
439
+ console.log(` macOS Keychain: ${sources.keychain ? '✓ Set' : '✗ Not set'}`);
440
+ if (!sources.env && !sources.clerkSession && !sources.config && !sources.keychain) {
441
+ console.log('\nNo credentials found.');
442
+ console.log('Recommended: huntr config set-session <__session-cookie>');
443
+ console.log('Alternative: huntr config set-token <token> [--keychain]');
444
+ } else if (sources.clerkSession) {
445
+ console.log('\n✓ Clerk session active — tokens refresh automatically.');
446
+ }
447
+ } catch (error) {
448
+ console.error('Error:', error instanceof Error ? error.message : error);
449
+ process.exit(1);
450
+ }
451
+ });
452
+
453
+ config
454
+ .command('clear-token')
455
+ .description('Remove saved API token')
456
+ .option('-k, --keychain', 'Clear from macOS Keychain only')
457
+ .option('-c, --config', 'Clear from config file only')
458
+ .action(async (options) => {
459
+ try {
460
+ let location: 'config' | 'keychain' | 'all' = 'all';
461
+ if (options.keychain && !options.config) location = 'keychain';
462
+ else if (options.config && !options.keychain) location = 'config';
463
+ await tokenManager.clearToken(location);
464
+ const message = location === 'all' ? 'all locations'
465
+ : location === 'keychain' ? 'macOS Keychain'
466
+ : 'config file';
467
+ console.log(`✓ Token cleared from ${message}`);
468
+ } catch (error) {
469
+ console.error('Error:', error instanceof Error ? error.message : error);
470
+ process.exit(1);
471
+ }
472
+ });
473
+
474
+ config
475
+ .command('clear-session')
476
+ .description('Remove saved Clerk session cookie')
477
+ .action(async () => {
478
+ try {
479
+ await tokenManager.clerkSession.clearSession();
480
+ console.log('✓ Clerk session cleared');
481
+ } catch (error) {
482
+ console.error('Error:', error instanceof Error ? error.message : error);
483
+ process.exit(1);
484
+ }
485
+ });
486
+
487
+ program.parse();