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
@@ -0,0 +1,263 @@
1
+ import keytar from 'keytar';
2
+ import https from 'https';
3
+
4
+ const SERVICE_NAME = 'huntr-cli';
5
+ const ACCOUNT_SESSION_COOKIE = 'clerk-session-cookie'; // __session value
6
+ const ACCOUNT_SESSION_ID = 'clerk-session-id'; // sess_...
7
+ const ACCOUNT_CLIENT_UAT = 'clerk-client-uat'; // __client_uat value (optional)
8
+ const ACCOUNT_EXTRA_COOKIES = 'clerk-extra-cookies'; // JSON map of clerk-domain cookies
9
+
10
+ const CLERK_HOST = 'clerk.huntr.co';
11
+
12
+ /**
13
+ * Manages Clerk session-based JWT refresh for the Huntr CLI.
14
+ *
15
+ * Modern Clerk architecture (v5+):
16
+ * - __session cookie : stores the short-lived session JWT (60s exp).
17
+ * The cookie itself has a long expiry (1 year);
18
+ * Clerk JS updates its *value* via FAPI silently.
19
+ * - __client_uat : Unix timestamp of last client update — not a credential.
20
+ *
21
+ * To refresh from outside a browser, we POST the __session value to Clerk's
22
+ * FAPI tokens endpoint. Clerk validates the session and returns a fresh JWT.
23
+ *
24
+ * Endpoint:
25
+ * POST https://clerk.huntr.co/v1/client/sessions/{sessionId}/tokens
26
+ * Cookie: __session=<value>
27
+ * Response: { jwt: "ey..." }
28
+ *
29
+ * The __session value must be re-extracted from the browser periodically
30
+ * (roughly every few weeks, when Clerk rotates the underlying session).
31
+ * For daily use it persists fine.
32
+ */
33
+ export class ClerkSessionManager {
34
+ // ── storage ──────────────────────────────────────────────────────────────
35
+
36
+ async saveSession(
37
+ sessionCookieValue: string,
38
+ sessionId: string,
39
+ clientUat?: string,
40
+ extraCookies?: Record<string, string>,
41
+ ): Promise<void> {
42
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_SESSION_COOKIE, sessionCookieValue);
43
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_SESSION_ID, sessionId);
44
+ if (clientUat) {
45
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_CLIENT_UAT, clientUat);
46
+ }
47
+ if (extraCookies && Object.keys(extraCookies).length > 0) {
48
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_EXTRA_COOKIES, JSON.stringify(extraCookies));
49
+ }
50
+ }
51
+
52
+ async getSessionCookie(): Promise<string | null> {
53
+ return keytar.getPassword(SERVICE_NAME, ACCOUNT_SESSION_COOKIE);
54
+ }
55
+
56
+ async getSessionId(): Promise<string | null> {
57
+ return keytar.getPassword(SERVICE_NAME, ACCOUNT_SESSION_ID);
58
+ }
59
+
60
+ async getClientUat(): Promise<string | null> {
61
+ return keytar.getPassword(SERVICE_NAME, ACCOUNT_CLIENT_UAT);
62
+ }
63
+
64
+ async getExtraCookies(): Promise<Record<string, string>> {
65
+ const raw = await keytar.getPassword(SERVICE_NAME, ACCOUNT_EXTRA_COOKIES);
66
+ if (!raw) return {};
67
+ try {
68
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
69
+ const out: Record<string, string> = {};
70
+ for (const [k, v] of Object.entries(parsed)) {
71
+ if (typeof v === 'string' && v.length > 0) out[k] = v;
72
+ }
73
+ return out;
74
+ } catch {
75
+ return {};
76
+ }
77
+ }
78
+
79
+ async clearSession(): Promise<void> {
80
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_SESSION_COOKIE);
81
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_SESSION_ID);
82
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_CLIENT_UAT);
83
+ await keytar.deletePassword(SERVICE_NAME, ACCOUNT_EXTRA_COOKIES);
84
+ }
85
+
86
+ async hasSession(): Promise<boolean> {
87
+ const cookie = await this.getSessionCookie();
88
+ const sessionId = await this.getSessionId();
89
+ return !!(cookie && sessionId);
90
+ }
91
+
92
+ // ── token refresh ─────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Returns a fresh Clerk JWT by POSTing to the Clerk FAPI tokens endpoint
96
+ * using the stored __session cookie value.
97
+ */
98
+ async getFreshToken(): Promise<string> {
99
+ const sessionCookie = await this.getSessionCookie();
100
+ const sessionId = await this.getSessionId();
101
+ const clientUat = await this.getClientUat();
102
+ const extraCookies = await this.getExtraCookies();
103
+
104
+ if (!sessionCookie || !sessionId) {
105
+ throw new Error(
106
+ 'No Clerk session stored. Run:\n' +
107
+ ' huntr config set-session <__session-cookie-value>\n' +
108
+ 'See "huntr config set-session --help" for instructions.',
109
+ );
110
+ }
111
+
112
+ return this.fetchToken(sessionCookie, sessionId, clientUat, extraCookies);
113
+ }
114
+
115
+ async refreshFromProvidedSession(
116
+ sessionCookieValue: string,
117
+ sessionId: string,
118
+ clientUat?: string | null,
119
+ extraCookies?: Record<string, string>,
120
+ ): Promise<string> {
121
+ return this.fetchToken(sessionCookieValue, sessionId, clientUat, extraCookies);
122
+ }
123
+
124
+ private fetchToken(
125
+ sessionCookieValue: string,
126
+ sessionId: string,
127
+ clientUat?: string | null,
128
+ extraCookies: Record<string, string> = {},
129
+ ): Promise<string> {
130
+ return new Promise((resolve, reject) => {
131
+ // Match the exact request Clerk JS makes (version pinned to what huntr.co loads)
132
+ const path = `/v1/client/sessions/${sessionId}/tokens?_clerk_js_version=4.73.14`;
133
+ // Strip any "__session=" prefix if user pasted it with the name
134
+ const raw = sessionCookieValue.startsWith('__session=')
135
+ ? sessionCookieValue.slice('__session='.length)
136
+ : sessionCookieValue;
137
+ // Send all cookies Clerk expects, matching browser credentials:include behaviour
138
+ const uat = clientUat && clientUat.trim() ? clientUat.trim() : '1';
139
+ const cookieParts = [`__session=${raw}`, `__client_uat=${uat}`];
140
+ for (const [name, value] of Object.entries(extraCookies)) {
141
+ if (!value || name === '__session' || name === '__client_uat') continue;
142
+ if (!/^[A-Za-z0-9_.-]+$/.test(name)) continue;
143
+ cookieParts.push(`${name}=${value}`);
144
+ }
145
+ const cookieHeader = cookieParts.join('; ');
146
+
147
+ const options = {
148
+ hostname: CLERK_HOST,
149
+ path,
150
+ method: 'POST',
151
+ headers: {
152
+ 'Cookie': cookieHeader,
153
+ 'Content-Type': 'application/x-www-form-urlencoded',
154
+ 'Content-Length': '0',
155
+ 'Accept': 'application/json',
156
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
157
+ 'Origin': 'https://huntr.co',
158
+ 'Referer': 'https://huntr.co/',
159
+ 'sec-fetch-site': 'same-site',
160
+ 'sec-fetch-mode': 'cors',
161
+ 'sec-fetch-dest': 'empty',
162
+ },
163
+ };
164
+
165
+ const req = https.request(options, (res) => {
166
+ let body = '';
167
+ res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
168
+ res.on('end', async () => {
169
+ if (res.statusCode === 200 || res.statusCode === 201) {
170
+ try {
171
+ const data = JSON.parse(body);
172
+ // Clerk FAPI response: { object: 'token', jwt: '...' }
173
+ const token = data.jwt ?? data.token ?? data.object?.jwt;
174
+ if (!token) {
175
+ reject(new Error(`Unexpected Clerk response: ${body.substring(0, 300)}`));
176
+ } else {
177
+ // Clerk may rotate __session on refresh via Set-Cookie.
178
+ // Persisting it prevents "works once, fails later" behavior.
179
+ await this.persistRotatedSessionCookie(res.headers['set-cookie'], sessionId, raw, uat, extraCookies);
180
+ resolve(token as string);
181
+ }
182
+ } catch {
183
+ reject(new Error(`Failed to parse Clerk response: ${body.substring(0, 300)}`));
184
+ }
185
+ } else if (res.statusCode === 401 || res.statusCode === 403) {
186
+ reject(new Error(
187
+ `Clerk session expired or invalid (HTTP ${res.statusCode}).\n` +
188
+ 'Re-extract your __session cookie from the browser:\n' +
189
+ ' DevTools → Application → Cookies → https://huntr.co → __session → Value\n' +
190
+ 'Then run: huntr config set-session <new-value>',
191
+ ));
192
+ } else {
193
+ reject(new Error(
194
+ `Clerk token refresh failed: HTTP ${res.statusCode}\n${body.substring(0, 300)}`,
195
+ ));
196
+ }
197
+ });
198
+ });
199
+
200
+ req.on('error', (err: Error) =>
201
+ reject(new Error(`Network error refreshing token: ${err.message}`)),
202
+ );
203
+ req.end();
204
+ });
205
+ }
206
+
207
+ private async persistRotatedSessionCookie(
208
+ setCookie: string | string[] | undefined,
209
+ sessionId: string,
210
+ currentSessionCookie: string,
211
+ clientUat?: string,
212
+ extraCookies: Record<string, string> = {},
213
+ ): Promise<void> {
214
+ if (!setCookie) return;
215
+ const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
216
+ let sessionCookie: string = currentSessionCookie;
217
+ let rotatedUat = clientUat;
218
+ const rotatedExtras: Record<string, string> = { ...extraCookies };
219
+
220
+ for (const header of cookies) {
221
+ const firstPair = header.split(';', 1)[0] ?? '';
222
+ const idx = firstPair.indexOf('=');
223
+ if (idx <= 0) continue;
224
+ const name = firstPair.slice(0, idx);
225
+ const value = firstPair.slice(idx + 1);
226
+ if (!value) continue;
227
+
228
+ if (name === '__session') {
229
+ sessionCookie = value;
230
+ } else if (name === '__client_uat') {
231
+ rotatedUat = value;
232
+ } else if (/^[A-Za-z0-9_.-]+$/.test(name)) {
233
+ rotatedExtras[name] = value;
234
+ }
235
+ }
236
+
237
+ await this.saveSession(sessionCookie, sessionId, rotatedUat, rotatedExtras);
238
+ }
239
+
240
+ // ── session-id extraction ─────────────────────────────────────────────────
241
+
242
+ /**
243
+ * Extracts the Clerk session ID (sess_...) from a __session JWT value.
244
+ * The __session cookie value IS a JWT; its payload contains `sid`.
245
+ */
246
+ static extractSessionId(sessionJwt: string): string | null {
247
+ try {
248
+ const raw = sessionJwt.startsWith('__session=')
249
+ ? sessionJwt.slice('__session='.length)
250
+ : sessionJwt;
251
+
252
+ const parts = raw.split('.');
253
+ if (parts.length < 2) return null;
254
+
255
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
256
+ const sid = payload.sid ?? payload.session_id;
257
+ if (typeof sid === 'string' && sid.startsWith('sess_')) return sid;
258
+ } catch {
259
+ // fall through
260
+ }
261
+ return null;
262
+ }
263
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export interface HuntrConfig {
6
+ apiToken?: string;
7
+ }
8
+
9
+ export class ConfigManager {
10
+ private configDir: string;
11
+ private configPath: string;
12
+
13
+ constructor() {
14
+ this.configDir = path.join(os.homedir(), '.huntr');
15
+ this.configPath = path.join(this.configDir, 'config.json');
16
+ }
17
+
18
+ private ensureConfigDir(): void {
19
+ if (!fs.existsSync(this.configDir)) {
20
+ fs.mkdirSync(this.configDir, { recursive: true });
21
+ }
22
+ }
23
+
24
+ getConfig(): HuntrConfig {
25
+ try {
26
+ if (fs.existsSync(this.configPath)) {
27
+ const content = fs.readFileSync(this.configPath, 'utf-8');
28
+ return JSON.parse(content);
29
+ }
30
+ } catch (error) {
31
+ // Return empty config if file doesn't exist or is invalid
32
+ }
33
+ return {};
34
+ }
35
+
36
+ setConfig(config: HuntrConfig): void {
37
+ this.ensureConfigDir();
38
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
39
+ }
40
+
41
+ getToken(): string | undefined {
42
+ return this.getConfig().apiToken;
43
+ }
44
+
45
+ setToken(token: string): void {
46
+ const config = this.getConfig();
47
+ config.apiToken = token;
48
+ this.setConfig(config);
49
+ }
50
+
51
+ clearToken(): void {
52
+ const config = this.getConfig();
53
+ delete config.apiToken;
54
+ this.setConfig(config);
55
+ }
56
+ }
@@ -0,0 +1,30 @@
1
+ import keytar from 'keytar';
2
+
3
+ const SERVICE_NAME = 'huntr-cli';
4
+ const ACCOUNT_NAME = 'api-token';
5
+
6
+ export class KeychainManager {
7
+ async getToken(): Promise<string | null> {
8
+ try {
9
+ return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
10
+ } catch (error) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ async setToken(token: string): Promise<void> {
16
+ try {
17
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
18
+ } catch (error) {
19
+ throw new Error('Failed to save token to keychain');
20
+ }
21
+ }
22
+
23
+ async deleteToken(): Promise<boolean> {
24
+ try {
25
+ return await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
26
+ } catch (error) {
27
+ return false;
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,233 @@
1
+ import * as http from 'http';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { TokenManager } from './token-manager';
5
+
6
+ export class TokenCaptureServer {
7
+ private server: http.Server | null = null;
8
+ private tokenManager: TokenManager;
9
+ private htmlPath: string;
10
+
11
+ constructor(tokenManager: TokenManager) {
12
+ this.tokenManager = tokenManager;
13
+ this.htmlPath = path.join(os.tmpdir(), 'huntr-token-capture.html');
14
+ }
15
+
16
+ async start(port: number = 17432): Promise<string> {
17
+ return new Promise((resolve, reject) => {
18
+ this.server = http.createServer(async (req, res) => {
19
+ // Enable CORS for file:// protocol
20
+ res.setHeader('Access-Control-Allow-Origin', '*');
21
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
22
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
23
+
24
+ if (req.method === 'OPTIONS') {
25
+ res.writeHead(200);
26
+ res.end();
27
+ return;
28
+ }
29
+
30
+ // Serve the HTML page
31
+ if (req.method === 'GET' && req.url === '/') {
32
+ res.writeHead(200, { 'Content-Type': 'text/html' });
33
+ res.end(this.getHtmlPage(port));
34
+ return;
35
+ }
36
+
37
+ if (req.method === 'POST' && req.url === '/token') {
38
+ let body = '';
39
+
40
+ req.on('data', chunk => {
41
+ body += chunk.toString();
42
+ });
43
+
44
+ req.on('end', async () => {
45
+ try {
46
+ const data = JSON.parse(body);
47
+ const token = data.token;
48
+
49
+ if (!token) {
50
+ res.writeHead(400, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ ok: false, error: 'No token provided' }));
52
+ return;
53
+ }
54
+
55
+ // Save token to config file
56
+ await this.tokenManager.saveToken(token, 'config');
57
+
58
+ const sessionId = `session_${Date.now()}`;
59
+
60
+ res.writeHead(200, { 'Content-Type': 'application/json' });
61
+ res.end(JSON.stringify({ ok: true, sessionId }));
62
+
63
+ console.log('\n✓ Token captured and saved!');
64
+ console.log('You can now use the CLI without --token flag.');
65
+
66
+ // Close server after successful capture
67
+ setTimeout(() => {
68
+ this.stop();
69
+ process.exit(0);
70
+ }, 100);
71
+
72
+ } catch (error) {
73
+ res.writeHead(500, { 'Content-Type': 'application/json' });
74
+ res.end(JSON.stringify({ ok: false, error: 'Internal server error' }));
75
+ }
76
+ });
77
+ } else {
78
+ res.writeHead(404);
79
+ res.end('Not found');
80
+ }
81
+ });
82
+
83
+ this.server.on('error', (error: any) => {
84
+ if (error.code === 'EADDRINUSE') {
85
+ reject(new Error(`Port ${port} is already in use`));
86
+ } else {
87
+ reject(error);
88
+ }
89
+ });
90
+
91
+ this.server.listen(port, () => {
92
+ resolve(`http://127.0.0.1:${port}`);
93
+ });
94
+ });
95
+ }
96
+
97
+ stop() {
98
+ if (this.server) {
99
+ this.server.close();
100
+ this.server = null;
101
+ }
102
+ }
103
+
104
+ private getHtmlPage(port: number): string {
105
+ return `<!DOCTYPE html>
106
+ <html>
107
+ <head>
108
+ <meta charset="UTF-8">
109
+ <title>Huntr Token Capture</title>
110
+ <style>
111
+ body {
112
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
113
+ max-width: 600px;
114
+ margin: 50px auto;
115
+ padding: 20px;
116
+ background: #f5f5f5;
117
+ }
118
+ .container {
119
+ background: white;
120
+ padding: 30px;
121
+ border-radius: 8px;
122
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
123
+ }
124
+ h1 { color: #333; margin-top: 0; }
125
+ .step {
126
+ margin: 20px 0;
127
+ padding: 15px;
128
+ background: #f8f9fa;
129
+ border-left: 4px solid #007bff;
130
+ border-radius: 4px;
131
+ }
132
+ code {
133
+ background: #e9ecef;
134
+ padding: 2px 6px;
135
+ border-radius: 3px;
136
+ font-family: 'Monaco', 'Courier New', monospace;
137
+ font-size: 14px;
138
+ }
139
+ .snippet {
140
+ background: #2d2d2d;
141
+ color: #f8f8f2;
142
+ padding: 15px;
143
+ border-radius: 4px;
144
+ margin: 10px 0;
145
+ overflow-x: auto;
146
+ font-family: 'Monaco', 'Courier New', monospace;
147
+ font-size: 13px;
148
+ cursor: pointer;
149
+ }
150
+ .snippet:hover { background: #3d3d3d; }
151
+ .success { color: #28a745; font-weight: bold; }
152
+ .error { color: #dc3545; }
153
+ button {
154
+ background: #007bff;
155
+ color: white;
156
+ border: none;
157
+ padding: 10px 20px;
158
+ border-radius: 4px;
159
+ cursor: pointer;
160
+ font-size: 14px;
161
+ }
162
+ button:hover { background: #0056b3; }
163
+ #status { margin-top: 20px; font-weight: bold; }
164
+ </style>
165
+ </head>
166
+ <body>
167
+ <div class="container">
168
+ <h1>🎯 Huntr Token Capture</h1>
169
+
170
+ <div class="step">
171
+ <strong>Step 1:</strong> Open <a href="https://huntr.co" target="_blank">huntr.co</a> and log in
172
+ </div>
173
+
174
+ <div class="step">
175
+ <strong>Step 2:</strong> Open DevTools Console (F12 or Cmd+Option+J)
176
+ </div>
177
+
178
+ <div class="step">
179
+ <strong>Step 3:</strong> Run this in the Huntr console:
180
+ <div class="snippet" onclick="copySnippet()" title="Click to copy">
181
+ (async()=>{var t=await window.Clerk.session.getToken({skipCache:true});window.opener?.postMessage({type:'HUNTR_TOKEN',token:t},'http://127.0.0.1:${port}');window.close();console.log('✓ Token sent!')})();
182
+ </div>
183
+ <small style="color: #666;">Click to copy, or click button below</small>
184
+ </div>
185
+
186
+ <button onclick="openHuntr()">Open Huntr & Capture Token</button>
187
+
188
+ <div id="status"></div>
189
+ </div>
190
+
191
+ <script>
192
+ let huntrWindow = null;
193
+
194
+ function copySnippet() {
195
+ const text = \`(async()=>{var t=await window.Clerk.session.getToken({skipCache:true});window.opener?.postMessage({type:'HUNTR_TOKEN',token:t},'http://127.0.0.1:${port}');window.close();console.log('✓ Token sent!')})();\`;
196
+ navigator.clipboard.writeText(text).then(() => {
197
+ document.getElementById('status').innerHTML = '<span class="success">✓ Copied! Now paste in Huntr Console</span>';
198
+ });
199
+ }
200
+
201
+ function openHuntr() {
202
+ huntrWindow = window.open('https://huntr.co', 'huntr', 'width=800,height=600');
203
+ document.getElementById('status').innerHTML = '<span style="color: #666;">Waiting for token from Huntr window...</span>';
204
+ }
205
+
206
+ window.addEventListener('message', async (event) => {
207
+ if (event.origin !== 'https://huntr.co') return;
208
+ if (event.data.type === 'HUNTR_TOKEN' && event.data.token) {
209
+ document.getElementById('status').innerHTML = '<span style="color: #666;">Sending token to CLI...</span>';
210
+
211
+ try {
212
+ const response = await fetch('http://127.0.0.1:${port}/token', {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({ token: event.data.token })
216
+ });
217
+
218
+ if (response.ok) {
219
+ document.getElementById('status').innerHTML = '<span class="success">✓ Token captured! You can close this window.</span>';
220
+ setTimeout(() => window.close(), 2000);
221
+ } else {
222
+ document.getElementById('status').innerHTML = '<span class="error">Error saving token</span>';
223
+ }
224
+ } catch (error) {
225
+ document.getElementById('status').innerHTML = '<span class="error">Error: ' + error.message + '</span>';
226
+ }
227
+ }
228
+ });
229
+ </script>
230
+ </body>
231
+ </html>`;
232
+ }
233
+ }