extract-from-sitemap 0.0.1 → 0.0.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.
Files changed (2) hide show
  1. package/cli.ts +133 -13
  2. package/package.json +1 -1
package/cli.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "fs";
13
13
  import { join, dirname, resolve } from "path";
14
14
  import { extractFromSitemap } from "./mod.js";
15
+ import { secrets } from "bun";
15
16
 
16
17
  interface Config {
17
18
  outDir: string;
@@ -30,10 +31,14 @@ interface Manifest {
30
31
  timestamp: string;
31
32
  }
32
33
 
34
+ const SECRETS_SERVICE = "extract-from-sitemap-cli";
35
+ const SECRETS_KEY = "parallel-api-key";
36
+
33
37
  class OAuth {
34
38
  private clientId: string;
35
39
  private redirectUri: string;
36
40
  private scope: string;
41
+ private server?: Bun.Server;
37
42
 
38
43
  constructor() {
39
44
  this.clientId = "extract-from-sitemap-cli";
@@ -57,8 +62,10 @@ class OAuth {
57
62
  authUrl.searchParams.set("code_challenge_method", "S256");
58
63
  authUrl.searchParams.set("state", Math.random().toString(36));
59
64
 
60
- console.log(`\nšŸ“– Please visit this URL to authorize the application:`);
61
- console.log(`${authUrl.toString()}\n`);
65
+ console.log(`\nšŸ“– Opening browser for authorization...`);
66
+
67
+ // Open browser automatically
68
+ await this.openBrowser(authUrl.toString());
62
69
 
63
70
  // Start simple HTTP server to catch the callback
64
71
  const code = await this.startCallbackServer();
@@ -90,6 +97,33 @@ class OAuth {
90
97
  return access_token;
91
98
  }
92
99
 
100
+ private async openBrowser(url: string): Promise<void> {
101
+ try {
102
+ const { spawn } = require("child_process");
103
+ const platform = process.platform;
104
+
105
+ let command: string;
106
+ let args: string[];
107
+
108
+ if (platform === "darwin") {
109
+ command = "open";
110
+ args = [url];
111
+ } else if (platform === "win32") {
112
+ command = "start";
113
+ args = ["", url];
114
+ } else {
115
+ // Linux/Unix
116
+ command = "xdg-open";
117
+ args = [url];
118
+ }
119
+
120
+ spawn(command, args, { detached: true, stdio: "ignore" });
121
+ } catch (error) {
122
+ console.log(`\nšŸ“– Please visit this URL to authorize the application:`);
123
+ console.log(`${url}\n`);
124
+ }
125
+ }
126
+
93
127
  private async generatePKCE(): Promise<{
94
128
  codeVerifier: string;
95
129
  codeChallenge: string;
@@ -111,9 +145,9 @@ class OAuth {
111
145
 
112
146
  private async startCallbackServer(): Promise<string> {
113
147
  return new Promise((resolve, reject) => {
114
- const server = Bun.serve({
148
+ this.server = Bun.serve({
115
149
  port: 3737,
116
- fetch(req) {
150
+ fetch: (req) => {
117
151
  const url = new URL(req.url);
118
152
 
119
153
  if (url.pathname === "/callback") {
@@ -130,9 +164,14 @@ class OAuth {
130
164
 
131
165
  if (code) {
132
166
  resolve(code);
133
- server.stop();
167
+ // Don't stop server here - let the cleanup happen in the finally block
134
168
  return new Response(
135
- "āœ… Authorization successful! You can close this window and return to the terminal."
169
+ "āœ… Authorization successful! You can close this window and return to the terminal.",
170
+ {
171
+ headers: {
172
+ "Content-Type": "text/html",
173
+ },
174
+ }
136
175
  );
137
176
  }
138
177
  }
@@ -143,11 +182,21 @@ class OAuth {
143
182
 
144
183
  // Timeout after 5 minutes
145
184
  setTimeout(() => {
146
- server.stop();
185
+ this.stopServer();
147
186
  reject(new Error("OAuth flow timed out"));
148
187
  }, 300000);
188
+ }).finally(() => {
189
+ // Ensure server is stopped after promise resolves or rejects
190
+ this.stopServer();
149
191
  });
150
192
  }
193
+
194
+ private stopServer(): void {
195
+ if (this.server) {
196
+ this.server.stop();
197
+ this.server = undefined;
198
+ }
199
+ }
151
200
  }
152
201
 
153
202
  async function loadConfig(): Promise<Config> {
@@ -195,7 +244,22 @@ async function loadConfig(): Promise<Config> {
195
244
  }
196
245
 
197
246
  async function getApiKey(): Promise<string> {
198
- // Check environment variables first
247
+ // Check if we have a stored API key in the keychain
248
+ try {
249
+ const storedKey = await secrets.get({
250
+ service: SECRETS_SERVICE,
251
+ name: SECRETS_KEY,
252
+ });
253
+
254
+ if (storedKey) {
255
+ console.log("šŸ”‘ Using stored API key from keychain");
256
+ return storedKey;
257
+ }
258
+ } catch (error) {
259
+ console.warn("āš ļø Could not access keychain:", error.message);
260
+ }
261
+
262
+ // Check environment variables as fallback
199
263
  let apiKey = process.env.PARALLEL_API_KEY;
200
264
 
201
265
  if (!apiKey && existsSync(".env")) {
@@ -207,13 +271,43 @@ async function getApiKey(): Promise<string> {
207
271
  }
208
272
  }
209
273
 
210
- if (!apiKey) {
211
- console.log("šŸ”‘ No API key found in environment or .env file.");
212
- const oauth = new OAuth();
213
- apiKey = await oauth.getApiKey();
274
+ if (apiKey) {
275
+ console.log("šŸ”‘ Using API key from environment");
276
+ // Store it in keychain for future use
277
+ try {
278
+ await secrets.set({
279
+ service: SECRETS_SERVICE,
280
+ name: SECRETS_KEY,
281
+ value: apiKey,
282
+ });
283
+ console.log("šŸ’¾ API key stored in keychain for future use");
284
+ } catch (error) {
285
+ console.warn("āš ļø Could not store API key in keychain:", error.message);
286
+ }
287
+ return apiKey;
288
+ }
289
+
290
+ // No API key found, start OAuth flow
291
+ console.log("šŸ”‘ No API key found. Starting OAuth flow...");
292
+ const oauth = new OAuth();
293
+ const newApiKey = await oauth.getApiKey();
294
+
295
+ // Store the new API key in keychain
296
+ try {
297
+ await secrets.set({
298
+ service: SECRETS_SERVICE,
299
+ name: SECRETS_KEY,
300
+ value: newApiKey,
301
+ });
302
+ console.log("šŸ’¾ API key stored securely in keychain");
303
+ } catch (error) {
304
+ console.warn("āš ļø Could not store API key in keychain:", error.message);
305
+ console.log(
306
+ "šŸ’” You may need to set PARALLEL_API_KEY environment variable for future runs"
307
+ );
214
308
  }
215
309
 
216
- return apiKey;
310
+ return newApiKey;
217
311
  }
218
312
 
219
313
  function loadManifest(outDir: string): Manifest {
@@ -311,9 +405,34 @@ async function processCustomUrls(
311
405
  return files;
312
406
  }
313
407
 
408
+ // Add command for clearing stored credentials
409
+ async function clearCredentials(): Promise<void> {
410
+ try {
411
+ const deleted = await secrets.delete({
412
+ service: SECRETS_SERVICE,
413
+ name: SECRETS_KEY,
414
+ });
415
+
416
+ if (deleted) {
417
+ console.log("āœ… Cleared stored API key from keychain");
418
+ } else {
419
+ console.log("ā„¹ļø No stored API key found to clear");
420
+ }
421
+ } catch (error) {
422
+ console.error("āŒ Error clearing credentials:", error.message);
423
+ }
424
+ }
425
+
314
426
  async function main() {
315
427
  console.log("šŸš€ Extract from Sitemap CLI");
316
428
 
429
+ // Check for special commands
430
+ const args = process.argv.slice(2);
431
+ if (args.includes("--clear-credentials")) {
432
+ await clearCredentials();
433
+ return;
434
+ }
435
+
317
436
  try {
318
437
  const config = await loadConfig();
319
438
  const apiKey = await getApiKey();
@@ -423,6 +542,7 @@ async function main() {
423
542
  console.log(`āš ļø Errors: ${totalErrors}`);
424
543
  }
425
544
  console.log(`šŸ“ Output directory: ${resolve(config.outDir)}`);
545
+ console.log(`\nšŸ’” Use --clear-credentials to remove stored API key`);
426
546
  } catch (error) {
427
547
  console.error("šŸ’„ Fatal error:", error.message);
428
548
  process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "extract-from-sitemap",
3
3
  "bin": "cli.ts",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "main": "mod.js",
6
6
  "description": "A module and CLI that allows extracting all pages from a sitemap into markdown and a llms.txt, using Parallel.ai APIs.",
7
7
  "files": [