backlinkflow 0.1.1 → 0.3.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/engine/submit.ts"],
4
- "sourcesContent": ["/**\n * LinkFlow v0.2 \u2014 submission orchestrator.\n * Runs one directory submission end-to-end:\n * preflight (HTTP check) \u2192 launch browser \u2192 navigate \u2192 adapter submit \u2192 verify \u2192 track\n */\nimport { launchBrowser, closeBrowser, screenshot, delay, looksLikeLogin } from './browser.js';\nimport { findAdapter, genericAdapter, type SubmitContext } from './adapters.js';\nimport type { DirectoryEntry, Payload, SubmissionRecord } from '../types.js';\nimport { recordSubmission } from '../tracker.js';\nimport { loadConfig } from '../config.js';\n\nexport interface SubmitOptions {\n siteUrl: string;\n dryRun?: boolean;\n proofDir?: string;\n maxWaitMs?: number;\n}\n\nexport interface SubmitResult {\n directory: string;\n status: 'submitted' | 'pending' | 'failed' | 'skipped';\n note?: string;\n proof?: string;\n}\n\n/** Preflight HTTP check \u2014 catch dead sites before opening a browser. */\nasync function preflight(url: string): Promise<{ ok: boolean; status?: number }> {\n try {\n const res = await fetch(url, {\n method: 'GET',\n redirect: 'follow',\n signal: AbortSignal.timeout(12000),\n headers: { 'User-Agent': 'Mozilla/5.0 LinkFlow/0.2' },\n });\n if (res.status === 404) return { ok: false, status: 404 };\n if (res.status >= 500) return { ok: false, status: res.status };\n return { ok: true, status: res.status };\n } catch {\n return { ok: false };\n }\n}\n\n/** Submit one directory entry. */\nexport async function submitOne(dir: DirectoryEntry, payload: Payload, opts: SubmitOptions): Promise<SubmitResult> {\n const cfg = loadConfig();\n const { siteUrl, dryRun = false, proofDir = '.linkflow/proofs' } = opts;\n\n // 1. Skip known-dead\n if (dir.status === 'dead') {\n return { directory: dir.name, status: 'skipped', note: 'status=dead' };\n }\n if (dir.status === 'paid') {\n return { directory: dir.name, status: 'skipped', note: 'status=paid' };\n }\n\n // 2. Preflight\n const pf = await preflight(dir.submitUrl);\n if (!pf.ok) {\n const note = pf.status ? `preflight HTTP ${pf.status}` : 'preflight unreachable';\n // record as failed so it won't retry\n recordSubmission({ site: siteUrl, directory: dir.name, status: 'failed', submittedAt: new Date().toISOString(), notes: note });\n return { directory: dir.name, status: 'failed', note };\n }\n\n if (dryRun) {\n return { directory: dir.name, status: 'pending', note: 'dry-run (preflight OK)' };\n }\n\n // 3. Launch browser\n const { browser, page } = await launchBrowser();\n try {\n const adapter = findAdapter(dir.name) || genericAdapter;\n if (adapter.needsCredentials && !cfg.ai?.apiKey) {\n // not actually credential-related, but we check config presence loosely\n }\n\n const ctx: SubmitContext = {\n page,\n payload,\n siteUrl,\n email: cfg.siteName ? undefined : undefined,\n credentials: (cfg as any).credentials,\n log: (m) => console.log(m),\n };\n\n console.log(` \uD83D\uDCC4 Opening ${dir.submitUrl}`);\n await page.goto(dir.submitUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });\n await delay(1500);\n\n // 4. Validate page\n const body = await page.textContent('body').catch(() => '');\n const url = page.url();\n if (/404|not found|page not found/i.test(body) && /404/.test(url)) {\n return { directory: dir.name, status: 'failed', note: 'page 404' };\n }\n if (looksLikeLogin(body, url)) {\n return { directory: dir.name, status: 'failed', note: 'login wall \u2014 needs credentials/manual' };\n }\n\n // 5. Run adapter\n const result = await adapter.submit(ctx);\n const status: 'submitted' | 'pending' | 'failed' = result.ok ? 'submitted' : 'failed';\n let proof: string | undefined;\n if (result.ok && !dryRun) {\n proof = await screenshot(page, `${dir.name.replace(/[^a-z0-9]+/gi, '-')}`, proofDir);\n }\n\n const rec: SubmissionRecord = {\n site: siteUrl,\n directory: dir.name,\n status,\n submittedAt: new Date().toISOString(),\n url: dir.submitUrl,\n proof,\n notes: result.note,\n };\n recordSubmission(rec);\n\n return { directory: dir.name, status, note: result.note, proof };\n } catch (err) {\n const note = `browser error: ${(err as Error).message.slice(0, 100)}`;\n recordSubmission({ site: siteUrl, directory: dir.name, status: 'failed', submittedAt: new Date().toISOString(), notes: note });\n return { directory: dir.name, status: 'failed', note };\n } finally {\n await closeBrowser({ browser, page });\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * LinkFlow v0.2 \u2014 submission orchestrator.\n * Runs one directory submission end-to-end:\n * preflight (HTTP check) \u2192 launch browser \u2192 navigate \u2192 adapter submit \u2192 verify \u2192 track\n */\nimport { launchBrowser, closeBrowser, screenshot, delay, looksLikeLogin } from './browser.js';\nimport { findAdapter, genericAdapter, type SubmitContext } from './adapters.js';\nimport type { DirectoryEntry, Payload, SubmissionRecord } from '../types.js';\nimport { recordSubmission } from '../tracker.js';\nimport { loadConfig } from '../config.js';\n\nexport interface SubmitOptions {\n siteUrl: string;\n dryRun?: boolean;\n proofDir?: string;\n maxWaitMs?: number;\n}\n\nexport interface SubmitResult {\n directory: string;\n status: 'submitted' | 'pending' | 'failed' | 'skipped';\n note?: string;\n proof?: string;\n}\n\n/** Preflight HTTP check \u2014 catch dead sites before opening a browser. */\nasync function preflight(url: string): Promise<{ ok: boolean; status?: number }> {\n try {\n const res = await fetch(url, {\n method: 'GET',\n redirect: 'follow',\n signal: AbortSignal.timeout(12000),\n headers: { 'User-Agent': 'Mozilla/5.0 LinkFlow/0.2' },\n });\n if (res.status === 404) return { ok: false, status: 404 };\n if (res.status >= 500) return { ok: false, status: res.status };\n return { ok: true, status: res.status };\n } catch {\n return { ok: false };\n }\n}\n\nconst SUCCESS_RE = /(thank you|thanks for|submitted|submission received|we'?ve received|we'll review|we will review|got it|in review|under review|pending review|successfully (added|submitted)|your (site|tool|product|listing) (has been )?(added|submitted)|added successfully)/i;\nconst SOFT_ERROR_RE = /(error|failed|invalid|missing|required|try again|something went wrong|recaptcha|captcha)/i;\n\n/**\n * Inspect the page after submit: success message, redirect to a listing page,\n * or soft error. Pure logic \u2014 returns a verdict without touching the browser.\n */\nexport function verdictAfterSubmit(bodyText: string, url: string, wasOnForm: boolean, fieldsFilled: number): { ok: boolean; note: string } {\n const body = bodyText || '';\n const u = url.toLowerCase();\n // Success: thank-you phrasing or redirect off the submit form.\n if (SUCCESS_RE.test(body)) return { ok: true, note: 'success message detected' };\n if (!wasOnForm && fieldsFilled > 0) return { ok: true, note: 'redirected off the form (listing/thank-you page)' };\n if (SOFT_ERROR_RE.test(body)) return { ok: false, note: 'soft error text on page' };\n return { ok: false, note: 'no success signal detected' };\n}\n\n/** Truncated exponential backoff sleep: 3s, 6s, 12s\u2026 capped at 60s. */\nexport function backoffSleep(attempt: number): Promise<void> {\n const ms = Math.min(3000 * Math.pow(2, attempt), 60000);\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/** Retry an async fn up to `retries` times with backoff between attempts. */\nexport async function withRetry<T>(fn: () => Promise<T>, opts?: { retries?: number; shouldRetry?: (err: unknown) => boolean }): Promise<T> {\n const retries = opts?.retries ?? 2;\n let lastErr: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastErr = err;\n if (attempt < retries && (opts?.shouldRetry ? opts.shouldRetry(err) : true)) {\n await backoffSleep(attempt);\n } else {\n break;\n }\n }\n }\n throw lastErr;\n}\n\n/** Submit one directory entry. */\nexport async function submitOne(dir: DirectoryEntry, payload: Payload, opts: SubmitOptions): Promise<SubmitResult> {\n const cfg = loadConfig();\n const { siteUrl, dryRun = false, proofDir = '.linkflow/proofs' } = opts;\n\n // 1. Skip known-dead\n if (dir.status === 'dead') {\n return { directory: dir.name, status: 'skipped', note: 'status=dead' };\n }\n if (dir.status === 'paid') {\n return { directory: dir.name, status: 'skipped', note: 'status=paid' };\n }\n\n // 2. Preflight with retry (transient 5xx / network blips)\n let pf: { ok: boolean; status?: number };\n try {\n pf = await withRetry(() => preflight(dir.submitUrl), { retries: 2, shouldRetry: (e) => !(e instanceof Error && /404/.test(e.message)) });\n } catch {\n pf = { ok: false };\n }\n if (!pf.ok) {\n const note = pf.status ? `preflight HTTP ${pf.status}` : 'preflight unreachable';\n // record as failed so it won't retry\n recordSubmission({ site: siteUrl, directory: dir.name, status: 'failed', submittedAt: new Date().toISOString(), notes: note });\n return { directory: dir.name, status: 'failed', note };\n }\n\n if (dryRun) {\n return { directory: dir.name, status: 'pending', note: 'dry-run (preflight OK)' };\n }\n\n // 3. Launch browser (with one retry \u2014 transient launch failures happen)\n let session;\n try {\n session = await withRetry(() => launchBrowser(), { retries: 1 });\n } catch (err) {\n const note = `browser launch failed: ${(err as Error).message.slice(0, 100)}`;\n recordSubmission({ site: siteUrl, directory: dir.name, status: 'failed', submittedAt: new Date().toISOString(), notes: note });\n return { directory: dir.name, status: 'failed', note };\n }\n const { browser, page } = session;\n try {\n const adapter = findAdapter(dir.name) || genericAdapter;\n if (adapter.needsCredentials && !cfg.ai?.apiKey) {\n // not actually credential-related, but we check config presence loosely\n }\n\n const ctx: SubmitContext = {\n page,\n payload,\n siteUrl,\n email: cfg.siteName ? undefined : undefined,\n credentials: (cfg as any).credentials,\n log: (m) => console.log(m),\n };\n\n console.log(` \uD83D\uDCC4 Opening ${dir.submitUrl}`);\n await page.goto(dir.submitUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });\n await delay(1500);\n\n // 4. Validate page\n const body = await page.textContent('body').catch(() => '');\n const url = page.url();\n if (/404|not found|page not found/i.test(body) && /404/.test(url)) {\n return { directory: dir.name, status: 'failed', note: 'page 404' };\n }\n if (looksLikeLogin(body, url)) {\n return { directory: dir.name, status: 'failed', note: 'login wall \u2014 needs credentials/manual' };\n }\n\n // 5. Run adapter\n const result = await adapter.submit(ctx);\n const status: 'submitted' | 'pending' | 'failed' = result.ok ? 'submitted' : 'failed';\n let proof: string | undefined;\n if (result.ok && !dryRun) {\n proof = await screenshot(page, `${dir.name.replace(/[^a-z0-9]+/gi, '-')}`, proofDir);\n }\n\n const rec: SubmissionRecord = {\n site: siteUrl,\n directory: dir.name,\n status,\n submittedAt: new Date().toISOString(),\n url: dir.submitUrl,\n proof,\n notes: result.note,\n };\n recordSubmission(rec);\n\n return { directory: dir.name, status, note: result.note, proof };\n } catch (err) {\n const note = `browser error: ${(err as Error).message.slice(0, 100)}`;\n recordSubmission({ site: siteUrl, directory: dir.name, status: 'failed', submittedAt: new Date().toISOString(), notes: note });\n return { directory: dir.name, status: 'failed', note };\n } finally {\n await closeBrowser({ browser, page });\n }\n}\n"],
5
+ "mappings": "AAKA,SAAS,eAAe,cAAc,YAAY,OAAO,sBAAsB;AAC/E,SAAS,aAAa,sBAA0C;AAEhE,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAiB3B,eAAe,UAAU,KAAwD;AAC/E,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,QAAQ,YAAY,QAAQ,IAAK;AAAA,MACjC,SAAS,EAAE,cAAc,2BAA2B;AAAA,IACtD,CAAC;AACD,QAAI,IAAI,WAAW,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI;AACxD,QAAI,IAAI,UAAU,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,IAAI,OAAO;AAC9D,WAAO,EAAE,IAAI,MAAM,QAAQ,IAAI,OAAO;AAAA,EACxC,QAAQ;AACN,WAAO,EAAE,IAAI,MAAM;AAAA,EACrB;AACF;AAEA,MAAM,aAAa;AACnB,MAAM,gBAAgB;AAMf,SAAS,mBAAmB,UAAkB,KAAa,WAAoB,cAAqD;AACzI,QAAM,OAAO,YAAY;AACzB,QAAM,IAAI,IAAI,YAAY;AAE1B,MAAI,WAAW,KAAK,IAAI,EAAG,QAAO,EAAE,IAAI,MAAM,MAAM,2BAA2B;AAC/E,MAAI,CAAC,aAAa,eAAe,EAAG,QAAO,EAAE,IAAI,MAAM,MAAM,mDAAmD;AAChH,MAAI,cAAc,KAAK,IAAI,EAAG,QAAO,EAAE,IAAI,OAAO,MAAM,0BAA0B;AAClF,SAAO,EAAE,IAAI,OAAO,MAAM,6BAA6B;AACzD;AAGO,SAAS,aAAa,SAAgC;AAC3D,QAAM,KAAK,KAAK,IAAI,MAAO,KAAK,IAAI,GAAG,OAAO,GAAG,GAAK;AACtD,SAAO,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC7C;AAGA,eAAsB,UAAa,IAAsB,MAAkF;AACzI,QAAM,UAAU,MAAM,WAAW;AACjC,MAAI;AACJ,WAAS,UAAU,GAAG,WAAW,SAAS,WAAW;AACnD,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,KAAK;AACZ,gBAAU;AACV,UAAI,UAAU,YAAY,MAAM,cAAc,KAAK,YAAY,GAAG,IAAI,OAAO;AAC3E,cAAM,aAAa,OAAO;AAAA,MAC5B,OAAO;AACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,QAAM;AACR;AAGA,eAAsB,UAAU,KAAqB,SAAkB,MAA4C;AACjH,QAAM,MAAM,WAAW;AACvB,QAAM,EAAE,SAAS,SAAS,OAAO,WAAW,mBAAmB,IAAI;AAGnE,MAAI,IAAI,WAAW,QAAQ;AACzB,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,WAAW,MAAM,cAAc;AAAA,EACvE;AACA,MAAI,IAAI,WAAW,QAAQ;AACzB,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,WAAW,MAAM,cAAc;AAAA,EACvE;AAGA,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,UAAU,MAAM,UAAU,IAAI,SAAS,GAAG,EAAE,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE,aAAa,SAAS,MAAM,KAAK,EAAE,OAAO,GAAG,CAAC;AAAA,EACzI,QAAQ;AACN,SAAK,EAAE,IAAI,MAAM;AAAA,EACnB;AACA,MAAI,CAAC,GAAG,IAAI;AACV,UAAM,OAAO,GAAG,SAAS,kBAAkB,GAAG,MAAM,KAAK;AAEzD,qBAAiB,EAAE,MAAM,SAAS,WAAW,IAAI,MAAM,QAAQ,UAAU,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,OAAO,KAAK,CAAC;AAC7H,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,UAAU,KAAK;AAAA,EACvD;AAEA,MAAI,QAAQ;AACV,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,WAAW,MAAM,yBAAyB;AAAA,EAClF;AAGA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,UAAU,MAAM,cAAc,GAAG,EAAE,SAAS,EAAE,CAAC;AAAA,EACjE,SAAS,KAAK;AACZ,UAAM,OAAO,0BAA2B,IAAc,QAAQ,MAAM,GAAG,GAAG,CAAC;AAC3E,qBAAiB,EAAE,MAAM,SAAS,WAAW,IAAI,MAAM,QAAQ,UAAU,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,OAAO,KAAK,CAAC;AAC7H,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,UAAU,KAAK;AAAA,EACvD;AACA,QAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,MAAI;AACF,UAAM,UAAU,YAAY,IAAI,IAAI,KAAK;AACzC,QAAI,QAAQ,oBAAoB,CAAC,IAAI,IAAI,QAAQ;AAAA,IAEjD;AAEA,UAAM,MAAqB;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,IAAI,WAAW,SAAY;AAAA,MAClC,aAAc,IAAY;AAAA,MAC1B,KAAK,CAAC,MAAM,QAAQ,IAAI,CAAC;AAAA,IAC3B;AAEA,YAAQ,IAAI,uBAAgB,IAAI,SAAS,EAAE;AAC3C,UAAM,KAAK,KAAK,IAAI,WAAW,EAAE,WAAW,oBAAoB,SAAS,KAAM,CAAC;AAChF,UAAM,MAAM,IAAI;AAGhB,UAAM,OAAO,MAAM,KAAK,YAAY,MAAM,EAAE,MAAM,MAAM,EAAE;AAC1D,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,gCAAgC,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,GAAG;AACjE,aAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,UAAU,MAAM,WAAW;AAAA,IACnE;AACA,QAAI,eAAe,MAAM,GAAG,GAAG;AAC7B,aAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,UAAU,MAAM,6CAAwC;AAAA,IAChG;AAGA,UAAM,SAAS,MAAM,QAAQ,OAAO,GAAG;AACvC,UAAM,SAA6C,OAAO,KAAK,cAAc;AAC7E,QAAI;AACJ,QAAI,OAAO,MAAM,CAAC,QAAQ;AACxB,cAAQ,MAAM,WAAW,MAAM,GAAG,IAAI,KAAK,QAAQ,gBAAgB,GAAG,CAAC,IAAI,QAAQ;AAAA,IACrF;AAEA,UAAM,MAAwB;AAAA,MAC5B,MAAM;AAAA,MACN,WAAW,IAAI;AAAA,MACf;AAAA,MACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,KAAK,IAAI;AAAA,MACT;AAAA,MACA,OAAO,OAAO;AAAA,IAChB;AACA,qBAAiB,GAAG;AAEpB,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,MAAM,OAAO,MAAM,MAAM;AAAA,EACjE,SAAS,KAAK;AACZ,UAAM,OAAO,kBAAmB,IAAc,QAAQ,MAAM,GAAG,GAAG,CAAC;AACnE,qBAAiB,EAAE,MAAM,SAAS,WAAW,IAAI,MAAM,QAAQ,UAAU,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,OAAO,KAAK,CAAC;AAC7H,WAAO,EAAE,WAAW,IAAI,MAAM,QAAQ,UAAU,KAAK;AAAA,EACvD,UAAE;AACA,UAAM,aAAa,EAAE,SAAS,KAAK,CAAC;AAAA,EACtC;AACF;",
6
6
  "names": []
7
7
  }
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ async function cmdList() {
29
29
  const all = loadDirectories();
30
30
  const filtered = cat ? all.filter((d) => d.category === cat) : all;
31
31
  console.log(`
32
- LinkFlow directory database: ${all.length} sites
32
+ BacklinkFlow directory database: ${all.length} sites
33
33
  `);
34
34
  for (const [i, d] of filtered.entries()) {
35
35
  if (LIMIT && i >= LIMIT) break;
@@ -57,7 +57,7 @@ async function cmdSubmit() {
57
57
  process.exit(1);
58
58
  }
59
59
  const cfg = loadConfig(CONFIG_FILE);
60
- console.log("\nLinkFlow submit");
60
+ console.log("\nBacklinkFlow submit");
61
61
  console.log("\u2500".repeat(50));
62
62
  printConfigSummary(cfg);
63
63
  if (!hasAiConfig(cfg)) {
@@ -151,7 +151,7 @@ async function cmdPayload() {
151
151
  }
152
152
  function cmdStatus() {
153
153
  const s = trackerSummary();
154
- console.log("\nLinkFlow tracker");
154
+ console.log("\nBacklinkFlow tracker");
155
155
  console.log("\u2500".repeat(50));
156
156
  console.log(` Total records: ${s.total}`);
157
157
  for (const [k, v] of Object.entries(s.byStatus)) console.log(` ${k}: ${v}`);
@@ -169,7 +169,7 @@ function cmdReport() {
169
169
  }
170
170
  function cmdStats() {
171
171
  const s = dbStats();
172
- console.log("\nLinkFlow database stats");
172
+ console.log("\nBacklinkFlow database stats");
173
173
  console.log("\u2500".repeat(50));
174
174
  console.log(` Total directories: ${s.total}`);
175
175
  for (const [k, v] of Object.entries(s.byCategory)) console.log(` ${k}: ${v}`);
@@ -228,6 +228,103 @@ async function cmdDbRegenerate() {
228
228
  console.log(" Regenerate failed:", err.message);
229
229
  }
230
230
  }
231
+ async function cmdIndexNow() {
232
+ const cfg = loadConfig(CONFIG_FILE);
233
+ const siteUrl = VERB_ARG || cfg.siteUrl;
234
+ if (!siteUrl) {
235
+ console.log("Usage: backlinkflow indexnow <url> (or set siteUrl in config)");
236
+ process.exit(1);
237
+ }
238
+ const { pingSite, resolveIndexNowKey, toHost } = await import("./indexnow.js");
239
+ const key = resolveIndexNowKey(cfg);
240
+ const host = toHost(siteUrl);
241
+ console.log("\nBacklinkFlow IndexNow ping");
242
+ console.log("\u2500".repeat(50));
243
+ console.log(` host: ${host}`);
244
+ console.log(` key: ${key}`);
245
+ console.log(` key file: https://${host}/${key}.txt (publish this file, see README)`);
246
+ const urls = flag("--urls")?.split(" ") || void 0;
247
+ const r = await pingSite(siteUrl, { urls, skipVerify: has("--skip-verify") });
248
+ if (r.indexnow) {
249
+ console.log(`
250
+ IndexNow (Bing/Yandex/Naver/Seznam) \u2192 ${r.indexnow.endpoint}`);
251
+ if (r.indexnow.ok) {
252
+ console.log(` \u2705 ${r.indexnow.submitted} URL(s) accepted (HTTP ${r.indexnow.statusCode})`);
253
+ for (const u of urls || [`https://${host}/`, `https://${host}/sitemap.xml`]) console.log(` \u2022 ${u}`);
254
+ } else {
255
+ console.log(` \u274C ${r.indexnow.error || "rejected"}`);
256
+ if (!r.keyLocation && !has("--skip-verify")) {
257
+ console.log(" \u2192 is the key file published? Run with --skip-verify to force-submit anyway.");
258
+ }
259
+ }
260
+ }
261
+ if (r.google) {
262
+ console.log(`
263
+ Google sitemap ping \u2192 https://www.google.com/ping?sitemap=`);
264
+ if (r.google.ok) console.log(` \u2705 ping accepted (HTTP ${r.google.statusCode})`);
265
+ else console.log(` \u26A0\uFE0F ${r.google.error || "failed"} (Google often returns non-200; harmless)`);
266
+ }
267
+ console.log("\n Note: Google does not consume IndexNow. The sitemap ping above is the");
268
+ console.log(" free Google signal. Key file must stay published at the keyLocation.");
269
+ }
270
+ async function cmdAwesome() {
271
+ const repo = VERB_ARG;
272
+ if (!repo || !DRY_RUN) {
273
+ console.log("Usage: backlinkflow awesome <owner/repo> --dry-run");
274
+ console.log(" (dry-run only \u2014 this command never opens PRs or pushes)");
275
+ process.exit(1);
276
+ }
277
+ const cfg = loadConfig(CONFIG_FILE);
278
+ const { buildSnippet, buildPrBody, buildInstructions } = await import("./awesome.js");
279
+ const snippet = buildSnippet(cfg);
280
+ console.log("\nBacklinkFlow awesome-list generator (DRY RUN)");
281
+ console.log("\u2500".repeat(50));
282
+ console.log(`
283
+ Target repo: ${repo}`);
284
+ console.log(` Suggested section: ${snippet.suggestedSection}`);
285
+ console.log("\n \u2500\u2500 Markdown snippet to add \u2500\u2500");
286
+ console.log(`
287
+ ${snippet.markdown}`);
288
+ console.log("\n \u2500\u2500 PR body \u2500\u2500");
289
+ console.log("\n" + buildPrBody(cfg, repo));
290
+ console.log("\n \u2500\u2500 How to turn this into a real contribution \u2500\u2500");
291
+ console.log("\n" + buildInstructions(repo, snippet));
292
+ console.log("\n (dry-run complete \u2014 nothing was pushed, no PR opened)");
293
+ }
294
+ async function cmdMeasure() {
295
+ const cfg = loadConfig(CONFIG_FILE);
296
+ const siteUrl = VERB_ARG || cfg.siteUrl;
297
+ if (!siteUrl) {
298
+ console.log("Usage: backlinkflow measure <url> (or set siteUrl in config)");
299
+ process.exit(1);
300
+ }
301
+ const { measureBacklinks, saveMeasurement } = await import("./measure.js");
302
+ const m = await measureBacklinks(siteUrl);
303
+ const file = saveMeasurement(m);
304
+ if (has("--json")) {
305
+ console.log(JSON.stringify(m, null, 2));
306
+ return;
307
+ }
308
+ console.log("\nBacklinkFlow backlink measurement");
309
+ console.log("\u2500".repeat(50));
310
+ console.log(` domain: ${m.domain}`);
311
+ console.log(` sources: ${m.source}`);
312
+ console.log(` measured: ${m.measuredAt}`);
313
+ if (m.unavailable) {
314
+ console.log(" \u26A0\uFE0F All free sources unavailable \u2014 no numbers to report.");
315
+ if (m.note) console.log(` ${m.note}`);
316
+ } else {
317
+ console.log(` total backlinks: ${m.totalBacklinks ?? "n/a"}`);
318
+ console.log(` referring domains: ${m.referringDomains ?? "n/a"}`);
319
+ if (m.pagesInIndex !== null && m.pagesInIndex !== void 0) console.log(` pages in CC index: ${m.pagesInIndex}`);
320
+ if (m.inCommonCrawl !== void 0) console.log(` in Common Crawl: ${m.inCommonCrawl}`);
321
+ if (m.pagerank !== null && m.pagerank !== void 0) console.log(` CC PageRank: ${m.pagerank}`);
322
+ if (m.harmonicCentrality !== null && m.harmonicCentrality !== void 0) console.log(` CC harmonic centr: ${m.harmonicCentrality}`);
323
+ }
324
+ if (m.note) console.log(` note: ${m.note}`);
325
+ console.log(` cached: ${file}`);
326
+ console.log("\n Tip: run again after submissions to diff before/after.");
327
+ }
231
328
  async function main() {
232
329
  switch (VERB) {
233
330
  case "list":
@@ -257,17 +354,26 @@ async function main() {
257
354
  case "db:regenerate":
258
355
  await cmdDbRegenerate();
259
356
  break;
357
+ case "indexnow":
358
+ await cmdIndexNow();
359
+ break;
360
+ case "awesome":
361
+ await cmdAwesome();
362
+ break;
363
+ case "measure":
364
+ await cmdMeasure();
365
+ break;
260
366
  case "init":
261
367
  cmdInit();
262
368
  break;
263
369
  default:
264
370
  console.log(`Unknown command: ${VERB}
265
- Run 'backlinkflow' with: list | search | submit | payload | status | report | stats | db:review | db:regenerate | init`);
371
+ Run 'backlinkflow' with: list | search | submit | payload | status | report | stats | db:review | db:regenerate | indexnow | awesome | measure | init`);
266
372
  process.exit(1);
267
373
  }
268
374
  }
269
375
  main().catch((err) => {
270
- console.error("LinkFlow error:", err);
376
+ console.error("BacklinkFlow error:", err);
271
377
  process.exit(1);
272
378
  });
273
379
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\n/**\n * LinkFlow \u2014 zero-cost backlink & directory submission engine.\n *\n * Commands:\n * linkflow list [--category X] [--limit N] List directories in the database\n * linkflow search <query> Search directories\n * linkflow submit <site-url> [--dry-run] [--limit N] [--category X] Generate payloads + submit plan\n * linkflow payload <site-url> [--directory X] Generate AI-tailored payload only (no submission)\n * linkflow status Show submission tracker summary\n * linkflow report Regenerate report from tracker\n * linkflow stats Database stats\n * linkflow db:review Flag DB quality issues (dead links, homepage-as-submit)\n * linkflow db:regenerate Rebuild DB from source lists (scripts/regenerate-db.py)\n * linkflow init Print config template\n */\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { loadDirectories, searchDirectories, categories, dbStats } from './database.js';\nimport { loadConfig, hasAiConfig, printConfigSummary } from './config.js';\nimport { resetAiCallCount, getAiCallCount } from './ai.js';\nimport { generatePayload } from './payload.js';\nimport { loadTracker, alreadySubmitted, recordSubmission, trackerSummary } from './tracker.js';\nimport { writeReport, reportPath } from './report.js';\nimport { submitOne } from './engine/submit.js';\n\nconst rawArgs = process.argv.slice(2);\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst VERB = rawArgs[0] && !rawArgs[0].startsWith('--') ? rawArgs[0] : 'list';\nconst VERB_ARG = rawArgs[1] && !rawArgs[1].startsWith('--') ? rawArgs[1] : null;\n\nconst flag = (name: string): string | undefined => {\n const i = rawArgs.indexOf(name);\n return i !== -1 ? rawArgs[i + 1] : undefined;\n};\nconst has = (name: string): boolean => rawArgs.includes(name);\n\nconst DRY_RUN = has('--dry-run');\nconst LIMIT = parseInt(flag('--limit') || '0') || 0;\nconst CONFIG_FILE = flag('--config');\n\nfunction formatDir(d: any, i: number): string {\n return `${String(i + 1).padStart(3)}. ${d.name.padEnd(28)} [${d.category.padEnd(22)}] auto=${d.auto} ${d.dr ? `DR${d.dr}` : ''} ${d.status && d.status !== 'active' ? `(${d.status})` : ''}`;\n}\n\nasync function cmdList(): Promise<void> {\n const cat = flag('--category');\n const all = loadDirectories();\n const filtered = cat ? all.filter((d) => d.category === cat) : all;\n console.log(`\\nLinkFlow directory database: ${all.length} sites\\n`);\n for (const [i, d] of filtered.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n }\n console.log(`\\n Categories: ${categories().join(', ')}`);\n}\n\nasync function cmdSearch(): Promise<void> {\n const q = VERB_ARG || '';\n const results = searchDirectories(q);\n console.log(`\\nSearch \"${q}\": ${results.length} matches\\n`);\n for (const [i, d] of results.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n console.log(` ${d.submitUrl}`);\n }\n}\n\nasync function cmdSubmit(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: backlinkflow submit <site-url> [--dry-run] [--limit N] [--category X] [--go]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n console.log('\\nLinkFlow submit');\n console.log('\u2500'.repeat(50));\n printConfigSummary(cfg);\n\n if (!hasAiConfig(cfg)) {\n console.log(' \u26A0\uFE0F No AI config (.env.local AI_BASE_URL/AI_API_KEY) \u2014 using template payloads.');\n }\n\n const cat = flag('--category');\n const targets = loadDirectories().filter((d) => {\n if (cat && d.category !== cat) return false;\n return d.auto === 'yes' || d.auto === 'manual'; // skip dead/paid-only\n });\n const selected = LIMIT ? targets.slice(0, LIMIT) : targets;\n\n console.log(`\\n Target directories: ${selected.length} (auto+manual, ${cat || 'all categories'})\\n`);\n resetAiCallCount();\n\n const records = loadTracker();\n let submitted = 0, skipped = 0, failed = 0;\n\n for (const [i, dir] of selected.entries()) {\n if (alreadySubmitted(siteUrl, dir.name)) {\n console.log(` [${i + 1}/${selected.length}] \u23ED\uFE0F ${dir.name} \u2014 already submitted (tracked)`);\n skipped++;\n continue;\n }\n const payload = await generatePayload(dir, cfg);\n console.log(` [${i + 1}/${selected.length}] \uD83D\uDE80 ${dir.name}`);\n console.log(` tagline: ${payload.tagline.slice(0, 80)}`);\n console.log(` submit: ${dir.submitUrl}`);\n\n if (has('--go')) {\n // REAL automation via Playwright\n const result = await submitOne(dir, payload, {\n siteUrl,\n proofDir: '.linkflow/proofs',\n });\n console.log(` \u2192 ${result.status}: ${result.note || ''}${result.proof ? ` (proof: ${result.proof})` : ''}`);\n if (result.status === 'submitted') submitted++;\n else if (result.status === 'failed') failed++;\n\n // pacing between submissions (respect per-day limit)\n const perDay = cfg.pacing?.perDay ?? 10;\n if (i + 1 >= perDay && i + 1 < selected.length) {\n console.log(` \u23F8\uFE0F Daily pacing limit (${perDay}) reached \u2014 stopping.`);\n break;\n }\n if (i + 1 < selected.length) {\n const pause = (cfg.pacing?.minSeconds ?? 60) * 1000;\n console.log(` \u23F3 pacing ${pause / 1000}s before next\u2026`);\n await new Promise((r) => setTimeout(r, pause));\n }\n } else {\n // plan-only mode (v0.1 behavior)\n console.log(` desc: ${payload.description.slice(0, 100)}`);\n if (!DRY_RUN) {\n recordSubmission({\n site: siteUrl,\n directory: dir.name,\n status: 'pending',\n submittedAt: new Date().toISOString(),\n url: dir.submitUrl,\n notes: 'planned \u2014 run with --go for automation',\n });\n submitted++;\n }\n }\n }\n\n console.log(`\\n Done: ${submitted} submitted/planned, ${skipped} skipped, ${failed} failed. AI calls: ${getAiCallCount()}`);\n\n if (!DRY_RUN) {\n const all = loadTracker();\n writeReport(siteUrl, all.filter((r) => r.site === siteUrl));\n const { md } = reportPath();\n console.log(` Report: ${md}`);\n } else {\n console.log(' (dry-run \u2014 nothing recorded)');\n }\n}\n\nasync function cmdPayload(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: backlinkflow payload <site-url> [--directory X]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n const dirName = flag('--directory');\n const dirs = dirName ? loadDirectories().filter((d) => d.name.toLowerCase().includes(dirName.toLowerCase())) : loadDirectories();\n if (!dirs.length) {\n console.log(' No matching directory.');\n return;\n }\n for (const dir of dirs.slice(0, 3)) {\n const payload = await generatePayload(dir, cfg);\n console.log(`\\n=== ${dir.name} ===`);\n console.log(JSON.stringify(payload, null, 2));\n }\n}\n\nfunction cmdStatus(): void {\n const s = trackerSummary();\n console.log('\\nLinkFlow tracker');\n console.log('\u2500'.repeat(50));\n console.log(` Total records: ${s.total}`);\n for (const [k, v] of Object.entries(s.byStatus)) console.log(` ${k}: ${v}`);\n if (s.sites.length) console.log(` Sites: ${s.sites.join(', ')}`);\n}\n\nfunction cmdReport(): void {\n const cfg = loadConfig();\n const site = VERB_ARG || cfg.siteUrl || 'all';\n const all = loadTracker();\n const filtered = site === 'all' ? all : all.filter((r) => r.site === site);\n writeReport(site, filtered);\n const { md } = reportPath();\n console.log(`\\n Report written: ${md}`);\n}\n\nfunction cmdStats(): void {\n const s = dbStats();\n console.log('\\nLinkFlow database stats');\n console.log('\u2500'.repeat(50));\n console.log(` Total directories: ${s.total}`);\n for (const [k, v] of Object.entries(s.byCategory)) console.log(` ${k}: ${v}`);\n console.log(` Auto-submittable: ${s.auto}`);\n}\n\nfunction cmdInit(): void {\n const tpl = {\n siteName: 'My Product',\n siteUrl: 'https://example.com',\n siteDescription: 'A short, honest description of what it is and who it is for.',\n tags: ['saas', 'devtools'],\n contentDomain: 'SaaS product',\n writingSample: 'Paste 2-3 sentences in your site voice here.',\n ai: {\n provider: 'openai',\n baseUrl: 'http://192.168.0.254:20128/v1',\n apiKey: 'your-omniroute-key',\n model: 'auto/best-free',\n maxCallsPerRun: 20,\n },\n pacing: { minSeconds: 60, perDay: 10 },\n };\n const p = path.join(process.cwd(), 'linkflow.config.json');\n if (fs.existsSync(p)) {\n console.log(` ${p} already exists \u2014 not overwriting.`);\n } else {\n fs.writeFileSync(p, JSON.stringify(tpl, null, 2));\n console.log(` Created ${p}`);\n }\n}\n\nasync function cmdDbReview(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'review-db.py');\n if (!fs.existsSync(script)) {\n console.log(' review-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Review failed:', (err as Error).message);\n }\n}\n\nasync function cmdDbRegenerate(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'regenerate-db.py');\n if (!fs.existsSync(script)) {\n console.log(' regenerate-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Regenerate failed:', (err as Error).message);\n }\n}\n\nasync function main(): Promise<void> {\n switch (VERB) {\n case 'list': await cmdList(); break;\n case 'search': await cmdSearch(); break;\n case 'submit': await cmdSubmit(); break;\n case 'payload': await cmdPayload(); break;\n case 'status': cmdStatus(); break;\n case 'report': cmdReport(); break;\n case 'stats': cmdStats(); break;\n case 'db:review': await cmdDbReview(); break;\n case 'db:regenerate': await cmdDbRegenerate(); break;\n case 'init': cmdInit(); break;\n default:\n console.log(`Unknown command: ${VERB}\\nRun 'backlinkflow' with: list | search | submit | payload | status | report | stats | db:review | db:regenerate | init`);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error('LinkFlow error:', err);\n process.exit(1);\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["#!/usr/bin/env node\n/**\n * LinkFlow \u2014 zero-cost backlink & directory submission engine.\n *\n * Commands:\n * linkflow list [--category X] [--limit N] List directories in the database\n * linkflow search <query> Search directories\n * linkflow submit <site-url> [--dry-run] [--limit N] [--category X] Generate payloads + submit plan\n * linkflow payload <site-url> [--directory X] Generate AI-tailored payload only (no submission)\n * linkflow status Show submission tracker summary\n * linkflow report Regenerate report from tracker\n * linkflow stats Database stats\n * linkflow db:review Flag DB quality issues (dead links, homepage-as-submit)\n * linkflow db:regenerate Rebuild DB from source lists (scripts/regenerate-db.py)\n * linkflow indexnow <url> [--skip-verify] [--urls a b c] Ping IndexNow + Google (v0.3)\n * linkflow awesome <repo> --dry-run Generate awesome-list PR snippet (dry-run only, v0.3)\n * linkflow measure <url> [--json] Measure backlinks via free sources (v0.4)\n * linkflow init Print config template\n */\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { loadDirectories, searchDirectories, categories, dbStats } from './database.js';\nimport { loadConfig, hasAiConfig, printConfigSummary } from './config.js';\nimport { resetAiCallCount, getAiCallCount } from './ai.js';\nimport { generatePayload } from './payload.js';\nimport { loadTracker, alreadySubmitted, recordSubmission, trackerSummary } from './tracker.js';\nimport { writeReport, reportPath } from './report.js';\nimport { submitOne } from './engine/submit.js';\n\nconst rawArgs = process.argv.slice(2);\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst VERB = rawArgs[0] && !rawArgs[0].startsWith('--') ? rawArgs[0] : 'list';\nconst VERB_ARG = rawArgs[1] && !rawArgs[1].startsWith('--') ? rawArgs[1] : null;\n\nconst flag = (name: string): string | undefined => {\n const i = rawArgs.indexOf(name);\n return i !== -1 ? rawArgs[i + 1] : undefined;\n};\nconst has = (name: string): boolean => rawArgs.includes(name);\n\nconst DRY_RUN = has('--dry-run');\nconst LIMIT = parseInt(flag('--limit') || '0') || 0;\nconst CONFIG_FILE = flag('--config');\n\nfunction formatDir(d: any, i: number): string {\n return `${String(i + 1).padStart(3)}. ${d.name.padEnd(28)} [${d.category.padEnd(22)}] auto=${d.auto} ${d.dr ? `DR${d.dr}` : ''} ${d.status && d.status !== 'active' ? `(${d.status})` : ''}`;\n}\n\nasync function cmdList(): Promise<void> {\n const cat = flag('--category');\n const all = loadDirectories();\n const filtered = cat ? all.filter((d) => d.category === cat) : all;\n console.log(`\\nBacklinkFlow directory database: ${all.length} sites\\n`);\n for (const [i, d] of filtered.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n }\n console.log(`\\n Categories: ${categories().join(', ')}`);\n}\n\nasync function cmdSearch(): Promise<void> {\n const q = VERB_ARG || '';\n const results = searchDirectories(q);\n console.log(`\\nSearch \"${q}\": ${results.length} matches\\n`);\n for (const [i, d] of results.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n console.log(` ${d.submitUrl}`);\n }\n}\n\nasync function cmdSubmit(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: backlinkflow submit <site-url> [--dry-run] [--limit N] [--category X] [--go]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n console.log('\\nBacklinkFlow submit');\n console.log('\u2500'.repeat(50));\n printConfigSummary(cfg);\n\n if (!hasAiConfig(cfg)) {\n console.log(' \u26A0\uFE0F No AI config (.env.local AI_BASE_URL/AI_API_KEY) \u2014 using template payloads.');\n }\n\n const cat = flag('--category');\n const targets = loadDirectories().filter((d) => {\n if (cat && d.category !== cat) return false;\n return d.auto === 'yes' || d.auto === 'manual'; // skip dead/paid-only\n });\n const selected = LIMIT ? targets.slice(0, LIMIT) : targets;\n\n console.log(`\\n Target directories: ${selected.length} (auto+manual, ${cat || 'all categories'})\\n`);\n resetAiCallCount();\n\n const records = loadTracker();\n let submitted = 0, skipped = 0, failed = 0;\n\n for (const [i, dir] of selected.entries()) {\n if (alreadySubmitted(siteUrl, dir.name)) {\n console.log(` [${i + 1}/${selected.length}] \u23ED\uFE0F ${dir.name} \u2014 already submitted (tracked)`);\n skipped++;\n continue;\n }\n const payload = await generatePayload(dir, cfg);\n console.log(` [${i + 1}/${selected.length}] \uD83D\uDE80 ${dir.name}`);\n console.log(` tagline: ${payload.tagline.slice(0, 80)}`);\n console.log(` submit: ${dir.submitUrl}`);\n\n if (has('--go')) {\n // REAL automation via Playwright\n const result = await submitOne(dir, payload, {\n siteUrl,\n proofDir: '.linkflow/proofs',\n });\n console.log(` \u2192 ${result.status}: ${result.note || ''}${result.proof ? ` (proof: ${result.proof})` : ''}`);\n if (result.status === 'submitted') submitted++;\n else if (result.status === 'failed') failed++;\n\n // pacing between submissions (respect per-day limit)\n const perDay = cfg.pacing?.perDay ?? 10;\n if (i + 1 >= perDay && i + 1 < selected.length) {\n console.log(` \u23F8\uFE0F Daily pacing limit (${perDay}) reached \u2014 stopping.`);\n break;\n }\n if (i + 1 < selected.length) {\n const pause = (cfg.pacing?.minSeconds ?? 60) * 1000;\n console.log(` \u23F3 pacing ${pause / 1000}s before next\u2026`);\n await new Promise((r) => setTimeout(r, pause));\n }\n } else {\n // plan-only mode (v0.1 behavior)\n console.log(` desc: ${payload.description.slice(0, 100)}`);\n if (!DRY_RUN) {\n recordSubmission({\n site: siteUrl,\n directory: dir.name,\n status: 'pending',\n submittedAt: new Date().toISOString(),\n url: dir.submitUrl,\n notes: 'planned \u2014 run with --go for automation',\n });\n submitted++;\n }\n }\n }\n\n console.log(`\\n Done: ${submitted} submitted/planned, ${skipped} skipped, ${failed} failed. AI calls: ${getAiCallCount()}`);\n\n if (!DRY_RUN) {\n const all = loadTracker();\n writeReport(siteUrl, all.filter((r) => r.site === siteUrl));\n const { md } = reportPath();\n console.log(` Report: ${md}`);\n } else {\n console.log(' (dry-run \u2014 nothing recorded)');\n }\n}\n\nasync function cmdPayload(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: backlinkflow payload <site-url> [--directory X]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n const dirName = flag('--directory');\n const dirs = dirName ? loadDirectories().filter((d) => d.name.toLowerCase().includes(dirName.toLowerCase())) : loadDirectories();\n if (!dirs.length) {\n console.log(' No matching directory.');\n return;\n }\n for (const dir of dirs.slice(0, 3)) {\n const payload = await generatePayload(dir, cfg);\n console.log(`\\n=== ${dir.name} ===`);\n console.log(JSON.stringify(payload, null, 2));\n }\n}\n\nfunction cmdStatus(): void {\n const s = trackerSummary();\n console.log('\\nBacklinkFlow tracker');\n console.log('\u2500'.repeat(50));\n console.log(` Total records: ${s.total}`);\n for (const [k, v] of Object.entries(s.byStatus)) console.log(` ${k}: ${v}`);\n if (s.sites.length) console.log(` Sites: ${s.sites.join(', ')}`);\n}\n\nfunction cmdReport(): void {\n const cfg = loadConfig();\n const site = VERB_ARG || cfg.siteUrl || 'all';\n const all = loadTracker();\n const filtered = site === 'all' ? all : all.filter((r) => r.site === site);\n writeReport(site, filtered);\n const { md } = reportPath();\n console.log(`\\n Report written: ${md}`);\n}\n\nfunction cmdStats(): void {\n const s = dbStats();\n console.log('\\nBacklinkFlow database stats');\n console.log('\u2500'.repeat(50));\n console.log(` Total directories: ${s.total}`);\n for (const [k, v] of Object.entries(s.byCategory)) console.log(` ${k}: ${v}`);\n console.log(` Auto-submittable: ${s.auto}`);\n}\n\nfunction cmdInit(): void {\n const tpl = {\n siteName: 'My Product',\n siteUrl: 'https://example.com',\n siteDescription: 'A short, honest description of what it is and who it is for.',\n tags: ['saas', 'devtools'],\n contentDomain: 'SaaS product',\n writingSample: 'Paste 2-3 sentences in your site voice here.',\n ai: {\n provider: 'openai',\n baseUrl: 'http://192.168.0.254:20128/v1',\n apiKey: 'your-omniroute-key',\n model: 'auto/best-free',\n maxCallsPerRun: 20,\n },\n pacing: { minSeconds: 60, perDay: 10 },\n };\n const p = path.join(process.cwd(), 'linkflow.config.json');\n if (fs.existsSync(p)) {\n console.log(` ${p} already exists \u2014 not overwriting.`);\n } else {\n fs.writeFileSync(p, JSON.stringify(tpl, null, 2));\n console.log(` Created ${p}`);\n }\n}\n\nasync function cmdDbReview(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'review-db.py');\n if (!fs.existsSync(script)) {\n console.log(' review-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Review failed:', (err as Error).message);\n }\n}\n\nasync function cmdDbRegenerate(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'regenerate-db.py');\n if (!fs.existsSync(script)) {\n console.log(' regenerate-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Regenerate failed:', (err as Error).message);\n }\n}\n\n/** v0.3 \u2014 IndexNow + Google sitemap ping. Real API call, no key (UUID host key). */\nasync function cmdIndexNow(): Promise<void> {\n const cfg = loadConfig(CONFIG_FILE);\n const siteUrl = VERB_ARG || cfg.siteUrl;\n if (!siteUrl) {\n console.log('Usage: backlinkflow indexnow <url> (or set siteUrl in config)');\n process.exit(1);\n }\n const { pingSite, resolveIndexNowKey, toHost } = await import('./indexnow.js');\n const key = resolveIndexNowKey(cfg);\n const host = toHost(siteUrl);\n\n console.log('\\nBacklinkFlow IndexNow ping');\n console.log('\u2500'.repeat(50));\n console.log(` host: ${host}`);\n console.log(` key: ${key}`);\n console.log(` key file: https://${host}/${key}.txt (publish this file, see README)`);\n\n const urls = flag('--urls')?.split(' ') || undefined;\n const r = await pingSite(siteUrl, { urls, skipVerify: has('--skip-verify') });\n\n if (r.indexnow) {\n console.log(`\\n IndexNow (Bing/Yandex/Naver/Seznam) \u2192 ${r.indexnow.endpoint}`);\n if (r.indexnow.ok) {\n console.log(` \u2705 ${r.indexnow.submitted} URL(s) accepted (HTTP ${r.indexnow.statusCode})`);\n for (const u of urls || [`https://${host}/`, `https://${host}/sitemap.xml`]) console.log(` \u2022 ${u}`);\n } else {\n console.log(` \u274C ${r.indexnow.error || 'rejected'}`);\n if (!r.keyLocation && !has('--skip-verify')) {\n console.log(' \u2192 is the key file published? Run with --skip-verify to force-submit anyway.');\n }\n }\n }\n if (r.google) {\n console.log(`\\n Google sitemap ping \u2192 https://www.google.com/ping?sitemap=`);\n if (r.google.ok) console.log(` \u2705 ping accepted (HTTP ${r.google.statusCode})`);\n else console.log(` \u26A0\uFE0F ${r.google.error || 'failed'} (Google often returns non-200; harmless)`);\n }\n console.log('\\n Note: Google does not consume IndexNow. The sitemap ping above is the');\n console.log(' free Google signal. Key file must stay published at the keyLocation.');\n}\n\n/** v0.3 \u2014 awesome-list PR generator (DRY-RUN ONLY \u2014 never opens PRs). */\nasync function cmdAwesome(): Promise<void> {\n const repo = VERB_ARG;\n if (!repo || !DRY_RUN) {\n console.log('Usage: backlinkflow awesome <owner/repo> --dry-run');\n console.log(' (dry-run only \u2014 this command never opens PRs or pushes)');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n const { buildSnippet, buildPrBody, buildInstructions } = await import('./awesome.js');\n const snippet = buildSnippet(cfg);\n\n console.log('\\nBacklinkFlow awesome-list generator (DRY RUN)');\n console.log('\u2500'.repeat(50));\n console.log(`\\n Target repo: ${repo}`);\n console.log(` Suggested section: ${snippet.suggestedSection}`);\n console.log('\\n \u2500\u2500 Markdown snippet to add \u2500\u2500');\n console.log(`\\n ${snippet.markdown}`);\n console.log('\\n \u2500\u2500 PR body \u2500\u2500');\n console.log('\\n' + buildPrBody(cfg, repo));\n console.log('\\n \u2500\u2500 How to turn this into a real contribution \u2500\u2500');\n console.log('\\n' + buildInstructions(repo, snippet));\n console.log('\\n (dry-run complete \u2014 nothing was pushed, no PR opened)');\n}\n\n/** v0.4 \u2014 measure backlinks via free sources (Common Crawl + OpenLinkProfiles). */\nasync function cmdMeasure(): Promise<void> {\n const cfg = loadConfig(CONFIG_FILE);\n const siteUrl = VERB_ARG || cfg.siteUrl;\n if (!siteUrl) {\n console.log('Usage: backlinkflow measure <url> (or set siteUrl in config)');\n process.exit(1);\n }\n const { measureBacklinks, saveMeasurement } = await import('./measure.js');\n const m = await measureBacklinks(siteUrl);\n const file = saveMeasurement(m);\n\n if (has('--json')) {\n console.log(JSON.stringify(m, null, 2));\n return;\n }\n\n console.log('\\nBacklinkFlow backlink measurement');\n console.log('\u2500'.repeat(50));\n console.log(` domain: ${m.domain}`);\n console.log(` sources: ${m.source}`);\n console.log(` measured: ${m.measuredAt}`);\n if (m.unavailable) {\n console.log(' \u26A0\uFE0F All free sources unavailable \u2014 no numbers to report.');\n if (m.note) console.log(` ${m.note}`);\n } else {\n console.log(` total backlinks: ${m.totalBacklinks ?? 'n/a'}`);\n console.log(` referring domains: ${m.referringDomains ?? 'n/a'}`);\n if (m.pagesInIndex !== null && m.pagesInIndex !== undefined) console.log(` pages in CC index: ${m.pagesInIndex}`);\n if (m.inCommonCrawl !== undefined) console.log(` in Common Crawl: ${m.inCommonCrawl}`);\n if (m.pagerank !== null && m.pagerank !== undefined) console.log(` CC PageRank: ${m.pagerank}`);\n if (m.harmonicCentrality !== null && m.harmonicCentrality !== undefined) console.log(` CC harmonic centr: ${m.harmonicCentrality}`);\n }\n if (m.note) console.log(` note: ${m.note}`);\n console.log(` cached: ${file}`);\n console.log('\\n Tip: run again after submissions to diff before/after.');\n}\n\nasync function main(): Promise<void> {\n switch (VERB) {\n case 'list': await cmdList(); break;\n case 'search': await cmdSearch(); break;\n case 'submit': await cmdSubmit(); break;\n case 'payload': await cmdPayload(); break;\n case 'status': cmdStatus(); break;\n case 'report': cmdReport(); break;\n case 'stats': cmdStats(); break;\n case 'db:review': await cmdDbReview(); break;\n case 'db:regenerate': await cmdDbRegenerate(); break;\n case 'indexnow': await cmdIndexNow(); break;\n case 'awesome': await cmdAwesome(); break;\n case 'measure': await cmdMeasure(); break;\n case 'init': cmdInit(); break;\n default:\n console.log(`Unknown command: ${VERB}\\nRun 'backlinkflow' with: list | search | submit | payload | status | report | stats | db:review | db:regenerate | indexnow | awesome | measure | init`);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error('BacklinkFlow error:', err);\n process.exit(1);\n});\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,122 @@
1
+ import crypto from "crypto";
2
+ import { loadConfig } from "./config.js";
3
+ const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
4
+ const GOOGLE_PING_ENDPOINT = "https://www.google.com/ping";
5
+ function toHost(url) {
6
+ return url.replace(/^[a-z]+:\/\//i, "").replace(/^\/\//, "").split(/[/?#]/)[0].split("@").pop().toLowerCase();
7
+ }
8
+ function resolveIndexNowKey(cfg) {
9
+ const envKey = process.env.INDEXNOW_KEY;
10
+ const cfgKey = cfg.indexNow?.key;
11
+ if (envKey) return envKey;
12
+ if (cfgKey) return cfgKey;
13
+ return crypto.randomUUID();
14
+ }
15
+ function keyLocationFor(host, key) {
16
+ return `https://${host}/${key}.txt`;
17
+ }
18
+ async function verifyKeyPublished(keyLocation, key) {
19
+ try {
20
+ const res = await fetch(keyLocation, {
21
+ signal: AbortSignal.timeout(15e3),
22
+ headers: { "User-Agent": "Mozilla/5.0 BacklinkFlow/0.3" }
23
+ });
24
+ if (res.status !== 200) return { ok: false, error: `keyLocation returned HTTP ${res.status}` };
25
+ const body = (await res.text()).trim();
26
+ if (body !== key) return { ok: false, error: "keyLocation contents do not match key" };
27
+ return { ok: true };
28
+ } catch (err) {
29
+ return { ok: false, error: `fetch failed: ${err.message}` };
30
+ }
31
+ }
32
+ async function submitIndexNow(host, key, keyLocation, urls) {
33
+ const urlList = [...new Set(urls.map((u) => u.trim()).filter(Boolean))];
34
+ if (urlList.length === 0) {
35
+ return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: "empty url list" };
36
+ }
37
+ if (urlList.length > 1e4) {
38
+ return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: "IndexNow caps batches at 10,000 URLs" };
39
+ }
40
+ const crossHost = urlList.filter((u) => !u.includes(host));
41
+ if (crossHost.length) {
42
+ return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: `${crossHost.length} URL(s) don't contain host ${host}: ${crossHost.slice(0, 3).join(", ")}` };
43
+ }
44
+ const payload = { host, key, keyLocation, urlList };
45
+ try {
46
+ const res = await fetch(INDEXNOW_ENDPOINT, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json; charset=utf-8", "User-Agent": "Mozilla/5.0 BacklinkFlow/0.3" },
49
+ body: JSON.stringify(payload),
50
+ signal: AbortSignal.timeout(3e4)
51
+ });
52
+ const body = await res.text().catch(() => "");
53
+ const ok = res.status === 200 || res.status === 202;
54
+ return {
55
+ endpoint: INDEXNOW_ENDPOINT,
56
+ statusCode: res.status,
57
+ ok,
58
+ submitted: ok ? urlList.length : 0,
59
+ engines: ["Bing", "Yandex", "Naver", "Seznam"],
60
+ responseBody: body.slice(0, 200) || void 0,
61
+ error: ok ? void 0 : `IndexNow rejected with HTTP ${res.status}: ${body.slice(0, 150)}`
62
+ };
63
+ } catch (err) {
64
+ return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: `HTTP error: ${err.message}` };
65
+ }
66
+ }
67
+ async function pingGoogleSitemap(sitemapUrl) {
68
+ try {
69
+ const res = await fetch(`${GOOGLE_PING_ENDPOINT}?sitemap=${encodeURIComponent(sitemapUrl)}`, {
70
+ signal: AbortSignal.timeout(3e4),
71
+ headers: { "User-Agent": "Mozilla/5.0 BacklinkFlow/0.3" }
72
+ });
73
+ return { ok: res.status === 200, statusCode: res.status, error: res.status === 200 ? void 0 : `Google ping returned HTTP ${res.status}` };
74
+ } catch (err) {
75
+ return { ok: false, error: `HTTP error: ${err.message}` };
76
+ }
77
+ }
78
+ function defaultUrls(host) {
79
+ return [`https://${host}/`, `https://${host}/sitemap.xml`];
80
+ }
81
+ async function pingSite(siteUrl, opts) {
82
+ const cfg = loadConfig();
83
+ const host = toHost(siteUrl);
84
+ const key = resolveIndexNowKey(cfg);
85
+ const keyLocation = keyLocationFor(host, key);
86
+ const urls = opts?.urls?.length ? opts.urls : defaultUrls(host);
87
+ let keyOk = true;
88
+ let keyError;
89
+ if (!opts?.skipVerify) {
90
+ const v = await verifyKeyPublished(keyLocation, key);
91
+ keyOk = v.ok;
92
+ keyError = v.error;
93
+ }
94
+ let indexnow = null;
95
+ if (keyOk) {
96
+ indexnow = await submitIndexNow(host, key, keyLocation, urls);
97
+ } else {
98
+ indexnow = {
99
+ endpoint: INDEXNOW_ENDPOINT,
100
+ statusCode: 0,
101
+ ok: false,
102
+ submitted: 0,
103
+ engines: [],
104
+ error: keyError || "key verification failed"
105
+ };
106
+ }
107
+ const google = await pingGoogleSitemap(`https://${host}/sitemap.xml`);
108
+ return { indexnow, google, key, keyLocation, host };
109
+ }
110
+ export {
111
+ GOOGLE_PING_ENDPOINT,
112
+ INDEXNOW_ENDPOINT,
113
+ defaultUrls,
114
+ keyLocationFor,
115
+ pingGoogleSitemap,
116
+ pingSite,
117
+ resolveIndexNowKey,
118
+ submitIndexNow,
119
+ toHost,
120
+ verifyKeyPublished
121
+ };
122
+ //# sourceMappingURL=indexnow.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/indexnow.ts"],
4
+ "sourcesContent": ["/**\n * BacklinkFlow v0.3 \u2014 IndexNow URL pinger.\n *\n * Pings the free IndexNow API (https://api.indexnow.org/indexnow) which\n * forwards one POST to Bing, Yandex, Naver and Seznam. Google does not\n * consume IndexNow, so Google gets a separate sitemap ping via\n * https://www.google.com/ping?sitemap= (the legacy free endpoint).\n *\n * No API key needed: you generate a UUID as the host key and publish it as\n * a plain-text file at https://<host>/<key>.txt. This module generates the\n * key (or reads INDEXNOW_KEY from env / config) and verifies the key file is\n * published before submitting \u2014 it never hosts the file for you.\n */\nimport crypto from 'crypto';\nimport { loadConfig } from './config.js';\n\nexport const INDEXNOW_ENDPOINT = 'https://api.indexnow.org/indexnow';\nexport const GOOGLE_PING_ENDPOINT = 'https://www.google.com/ping';\n\nexport interface IndexNowResult {\n endpoint: string;\n statusCode: number;\n ok: boolean;\n submitted: number;\n engines: string[];\n responseBody?: string;\n error?: string;\n}\n\nexport interface PingResult {\n indexnow: IndexNowResult | null;\n google: { ok: boolean; statusCode?: number; error?: string } | null;\n key: string;\n keyLocation: string;\n host: string;\n}\n\n/** Strip scheme/path from a site URL \u2192 bare host (no www normalization). */\nexport function toHost(url: string): string {\n return url\n .replace(/^[a-z]+:\\/\\//i, '')\n .replace(/^\\/\\//, '')\n .split(/[/?#]/)[0]\n .split('@')\n .pop()!\n .toLowerCase();\n}\n\n/** Load a configured IndexNow key: env > config.indexNow.key > generated. */\nexport function resolveIndexNowKey(cfg: ReturnType<typeof loadConfig>): string {\n const envKey = process.env.INDEXNOW_KEY;\n const cfgKey = (cfg as any).indexNow?.key;\n if (envKey) return envKey;\n if (cfgKey) return cfgKey;\n // Generate a fresh UUID per run \u2014 stable once saved to config.\n return crypto.randomUUID();\n}\n\n/** Build the key-file location URL for a host. */\nexport function keyLocationFor(host: string, key: string): string {\n return `https://${host}/${key}.txt`;\n}\n\n/** Fetch the published key file and confirm it matches. */\nexport async function verifyKeyPublished(keyLocation: string, key: string): Promise<{ ok: boolean; error?: string }> {\n try {\n const res = await fetch(keyLocation, {\n signal: AbortSignal.timeout(15000),\n headers: { 'User-Agent': 'Mozilla/5.0 BacklinkFlow/0.3' },\n });\n if (res.status !== 200) return { ok: false, error: `keyLocation returned HTTP ${res.status}` };\n const body = (await res.text()).trim();\n if (body !== key) return { ok: false, error: 'keyLocation contents do not match key' };\n return { ok: true };\n } catch (err) {\n return { ok: false, error: `fetch failed: ${(err as Error).message}` };\n }\n}\n\n/**\n * POST a URL list to the IndexNow umbrella endpoint.\n * IndexNow accepts 200/202 as success.\n */\nexport async function submitIndexNow(\n host: string,\n key: string,\n keyLocation: string,\n urls: string[]\n): Promise<IndexNowResult> {\n const urlList = [...new Set(urls.map((u) => u.trim()).filter(Boolean))];\n if (urlList.length === 0) {\n return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: 'empty url list' };\n }\n if (urlList.length > 10000) {\n return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: 'IndexNow caps batches at 10,000 URLs' };\n }\n // Every URL must belong to the declared host (IndexNow rejects cross-host).\n const crossHost = urlList.filter((u) => !u.includes(host));\n if (crossHost.length) {\n return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: `${crossHost.length} URL(s) don't contain host ${host}: ${crossHost.slice(0, 3).join(', ')}` };\n }\n\n const payload = { host, key, keyLocation, urlList };\n try {\n const res = await fetch(INDEXNOW_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': 'Mozilla/5.0 BacklinkFlow/0.3' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(30000),\n });\n const body = await res.text().catch(() => '');\n const ok = res.status === 200 || res.status === 202;\n return {\n endpoint: INDEXNOW_ENDPOINT,\n statusCode: res.status,\n ok,\n submitted: ok ? urlList.length : 0,\n engines: ['Bing', 'Yandex', 'Naver', 'Seznam'],\n responseBody: body.slice(0, 200) || undefined,\n error: ok ? undefined : `IndexNow rejected with HTTP ${res.status}: ${body.slice(0, 150)}`,\n };\n } catch (err) {\n return { endpoint: INDEXNOW_ENDPOINT, statusCode: 0, ok: false, submitted: 0, engines: [], error: `HTTP error: ${(err as Error).message}` };\n }\n}\n\n/** Ping Google's legacy sitemap endpoint (free, no key). */\nexport async function pingGoogleSitemap(sitemapUrl: string): Promise<{ ok: boolean; statusCode?: number; error?: string }> {\n try {\n const res = await fetch(`${GOOGLE_PING_ENDPOINT}?sitemap=${encodeURIComponent(sitemapUrl)}`, {\n signal: AbortSignal.timeout(30000),\n headers: { 'User-Agent': 'Mozilla/5.0 BacklinkFlow/0.3' },\n });\n return { ok: res.status === 200, statusCode: res.status, error: res.status === 200 ? undefined : `Google ping returned HTTP ${res.status}` };\n } catch (err) {\n return { ok: false, error: `HTTP error: ${(err as Error).message}` };\n }\n}\n\n/** Default URL list for a host: homepage + sitemap. */\nexport function defaultUrls(host: string): string[] {\n return [`https://${host}/`, `https://${host}/sitemap.xml`];\n}\n\n/**\n * Full ping: verify key, submit to IndexNow, ping Google sitemap.\n * Returns null when the host is invalid (no config siteUrl and no arg).\n */\nexport async function pingSite(siteUrl: string, opts?: { urls?: string[]; skipVerify?: boolean }): Promise<PingResult> {\n const cfg = loadConfig();\n const host = toHost(siteUrl);\n const key = resolveIndexNowKey(cfg);\n const keyLocation = keyLocationFor(host, key);\n const urls = opts?.urls?.length ? opts.urls : defaultUrls(host);\n\n let keyOk = true;\n let keyError: string | undefined;\n if (!opts?.skipVerify) {\n const v = await verifyKeyPublished(keyLocation, key);\n keyOk = v.ok;\n keyError = v.error;\n }\n\n let indexnow: IndexNowResult | null = null;\n if (keyOk) {\n indexnow = await submitIndexNow(host, key, keyLocation, urls);\n } else {\n indexnow = {\n endpoint: INDEXNOW_ENDPOINT,\n statusCode: 0,\n ok: false,\n submitted: 0,\n engines: [],\n error: keyError || 'key verification failed',\n };\n }\n\n const google = await pingGoogleSitemap(`https://${host}/sitemap.xml`);\n\n return { indexnow, google, key, keyLocation, host };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }