seo 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as load, t as version } from "./package-B9-D646K.js";
2
+ import { n as load, t as version } from "./package-BDez60C3.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform } from "node:os";
@@ -9,7 +9,7 @@ import lockfile from "proper-lockfile";
9
9
  import { createHash, randomUUID, sign } from "node:crypto";
10
10
  import "node:http";
11
11
  import "open";
12
- import Database from "better-sqlite3";
12
+ import Libsql from "libsql";
13
13
  import { Readability } from "@mozilla/readability";
14
14
  import Defuddle from "defuddle";
15
15
  import { parseHTML } from "linkedom";
@@ -763,6 +763,52 @@ async function createGoogleAccessTokenClient() {
763
763
  };
764
764
  }
765
765
  //#endregion
766
+ //#region packages/core/dist/storage/sqlite.js
767
+ function withoutDriverMetadata(row) {
768
+ if (!row || typeof row !== "object" || Array.isArray(row)) return row;
769
+ const { _metadata: _driverMetadata, ...data } = row;
770
+ return data;
771
+ }
772
+ var Statement = class {
773
+ statement;
774
+ constructor(statement) {
775
+ this.statement = statement;
776
+ }
777
+ run(...params) {
778
+ return this.statement.run(...params);
779
+ }
780
+ get(...params) {
781
+ return withoutDriverMetadata(this.statement.get(...params));
782
+ }
783
+ all(...params) {
784
+ return this.statement.all(...params).map(withoutDriverMetadata);
785
+ }
786
+ };
787
+ var SqliteDatabase = class {
788
+ database;
789
+ constructor(filename) {
790
+ this.database = new Libsql(filename);
791
+ }
792
+ prepare(source) {
793
+ return new Statement(this.database.prepare(source));
794
+ }
795
+ transaction(fn) {
796
+ return this.database.transaction(fn);
797
+ }
798
+ exec(source) {
799
+ this.database.exec(source);
800
+ return this;
801
+ }
802
+ pragma(source) {
803
+ const result = this.database.pragma(source);
804
+ return Array.isArray(result) ? result.map(withoutDriverMetadata) : withoutDriverMetadata(result);
805
+ }
806
+ close() {
807
+ this.database.close();
808
+ return this;
809
+ }
810
+ };
811
+ //#endregion
766
812
  //#region packages/core/dist/storage/database.js
767
813
  const CREATE_SQL = `
768
814
  CREATE TABLE IF NOT EXISTS sites (
@@ -1046,10 +1092,14 @@ function getDb() {
1046
1092
  recursive: true,
1047
1093
  mode: 448
1048
1094
  });
1049
- db = new Database(path);
1095
+ db = new SqliteDatabase(path);
1050
1096
  initDb(db);
1051
1097
  return db;
1052
1098
  }
1099
+ function checkDatabaseReadiness() {
1100
+ if (getDb().prepare("SELECT 1 AS ready").get()?.ready !== 1) throw new Error("The local SQLite database did not return a readiness row.");
1101
+ return { dbPath: getSeoCliPaths().cacheDbFile };
1102
+ }
1053
1103
  function hashKey(parts) {
1054
1104
  const json = JSON.stringify(parts);
1055
1105
  return Buffer.from(json).toString("base64url");
@@ -6241,16 +6291,15 @@ async function inspectMarkdownPage(input) {
6241
6291
  repeatedHashStable: null,
6242
6292
  markdownCanonicalMatchesHtml: null
6243
6293
  };
6244
- if (advertisedUrls.length !== 1) return observation;
6245
- const explicitUrl = advertisedUrls[0];
6294
+ const explicitUrl = advertisedUrls.length === 1 ? advertisedUrls[0] : void 0;
6246
6295
  const [explicit, negotiated, repeated] = await Promise.all([
6247
- fetchRepresentation({
6296
+ explicitUrl ? fetchRepresentation({
6248
6297
  url: explicitUrl,
6249
6298
  accept: "text/markdown",
6250
6299
  timeoutMs: input.timeoutMs,
6251
6300
  fetch: input.fetch,
6252
6301
  signal: input.signal
6253
- }),
6302
+ }) : Promise.resolve(void 0),
6254
6303
  fetchRepresentation({
6255
6304
  url: input.page.finalUrl,
6256
6305
  accept: "text/markdown",
@@ -6259,20 +6308,21 @@ async function inspectMarkdownPage(input) {
6259
6308
  signal: input.signal
6260
6309
  }),
6261
6310
  fetchRepresentation({
6262
- url: explicitUrl,
6311
+ url: explicitUrl ?? input.page.finalUrl,
6263
6312
  accept: "text/markdown",
6264
6313
  timeoutMs: input.timeoutMs,
6265
6314
  fetch: input.fetch,
6266
6315
  signal: input.signal
6267
6316
  })
6268
6317
  ]);
6269
- observation.explicit = explicit.observation;
6318
+ if (explicit) observation.explicit = explicit.observation;
6270
6319
  observation.negotiated = negotiated.observation;
6271
6320
  observation.repeated = repeated.observation;
6272
- observation.explicitMatchesNegotiated = explicit.observation.sha256 && negotiated.observation.sha256 ? explicit.observation.sha256 === negotiated.observation.sha256 : null;
6273
- observation.repeatedHashStable = explicit.observation.sha256 && repeated.observation.sha256 ? explicit.observation.sha256 === repeated.observation.sha256 : null;
6274
- observation.markdownCanonicalMatchesHtml = sameDocument(explicit.observation.canonicalUrl, input.page.finalUrl);
6275
- if (explicit.body) observation.quality = markdownQuality(explicit.body, input.page);
6321
+ observation.explicitMatchesNegotiated = explicit?.observation.sha256 && negotiated.observation.sha256 ? explicit.observation.sha256 === negotiated.observation.sha256 : null;
6322
+ const primary = explicit ?? negotiated;
6323
+ observation.repeatedHashStable = primary.observation.sha256 && repeated.observation.sha256 ? primary.observation.sha256 === repeated.observation.sha256 : null;
6324
+ observation.markdownCanonicalMatchesHtml = sameDocument(primary.observation.canonicalUrl, input.page.finalUrl);
6325
+ if (primary.body) observation.quality = markdownQuality(primary.body, input.page);
6276
6326
  return observation;
6277
6327
  }
6278
6328
  async function fetchText(input) {
@@ -6608,10 +6658,13 @@ async function collectAgentDiscovery(input) {
6608
6658
  const wwwLocation = www.location ? new URL(www.location, wwwUrl).toString() : void 0;
6609
6659
  const qZeroContentType = qZero.observation.contentType;
6610
6660
  const advertisedPages = observations.filter((observation) => observation.htmlAlternateUnique).length;
6611
- const evaluatedPages = observations.filter((observation) => observation.explicit?.status !== void 0).length;
6661
+ const evaluatedPages = observations.filter((observation) => {
6662
+ const representation = observation.explicit ?? observation.negotiated;
6663
+ return representation?.status !== void 0 && representation.status >= 200 && representation.status < 300 && /^\s*text\/markdown\b/iu.test(representation.contentType ?? "");
6664
+ }).length;
6612
6665
  const warnings = [...pages.length !== input.pages.length ? [`Agent representations were evaluated for ${pages.length} successful HTML pages from ${input.pages.length} retained documents.`] : [], ...routeManifest.valid ? [] : ["No valid public agent route manifest was available for orphan checks."]].sort();
6613
6666
  const htmlContentSignals = pages.map((page) => headerValue$1(page.responseHeaders, "content-signal")).filter((value) => Boolean(value));
6614
- const markdownContentSignals = observations.map((observation) => observation.explicit?.contentSignal).filter((value) => Boolean(value));
6667
+ const markdownContentSignals = observations.map((observation) => observation.explicit?.contentSignal ?? observation.negotiated?.contentSignal).filter((value) => Boolean(value));
6615
6668
  const contentSignalValues = [.../* @__PURE__ */ new Set([...htmlContentSignals, ...markdownContentSignals])].sort();
6616
6669
  return {
6617
6670
  profile: "content",
@@ -6694,7 +6747,11 @@ function section$1(id, title, checks) {
6694
6747
  };
6695
6748
  }
6696
6749
  function failedMarkdownUrls(discovery) {
6697
- return discovery.markdownAlternates.pages.filter((page) => !page.htmlAlternateUnique || page.httpAlternateUrls.length !== 1 || page.explicit?.status !== 200 || !/^\s*text\/markdown\b/iu.test(page.explicit.contentType ?? "") || page.explicitMatchesNegotiated !== true || page.repeatedHashStable !== true || page.markdownCanonicalMatchesHtml !== true || page.explicit?.varyAccept !== true || page.negotiated?.varyAccept !== true).map((page) => page.htmlUrl);
6750
+ return discovery.markdownAlternates.pages.filter((page) => {
6751
+ const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
6752
+ const explicitOk = !page.htmlAlternateUnique ? true : page.httpAlternateUrls.length === 1 && page.explicit?.status === 200 && /^\s*text\/markdown\b/iu.test(page.explicit.contentType ?? "") && page.explicitMatchesNegotiated === true && page.markdownCanonicalMatchesHtml === true;
6753
+ return !negotiatedOk || !explicitOk || page.repeatedHashStable !== true;
6754
+ }).map((page) => page.htmlUrl);
6698
6755
  }
6699
6756
  function qualityFailureUrls(discovery) {
6700
6757
  return discovery.markdownAlternates.pages.filter((page) => {
@@ -6736,20 +6793,25 @@ function markdownChecks(discovery) {
6736
6793
  const markdown = discovery.markdownAlternates;
6737
6794
  const failedUrls = failedMarkdownUrls(discovery);
6738
6795
  const qualityUrls = qualityFailureUrls(discovery);
6739
- const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.advertisedPages === markdown.eligibleHtmlPages && markdown.evaluatedPages === markdown.eligibleHtmlPages;
6740
- const bytesMatch = markdown.eligibleHtmlPages > 0 && markdown.exactByteMatches === markdown.eligibleHtmlPages;
6796
+ const negotiatedPages = markdown.pages.filter((page) => page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "")).length;
6797
+ const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
6798
+ const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
6799
+ const negotiationComplete = markdown.eligibleHtmlPages > 0 && negotiatedPages === markdown.eligibleHtmlPages && markdown.pages.every((page) => page.negotiated?.varyAccept) && markdown.pages.every((page) => !page.htmlAlternateUnique || page.explicitMatchesNegotiated === true);
6741
6800
  const stable = markdown.eligibleHtmlPages > 0 && markdown.stableResponses === markdown.eligibleHtmlPages;
6742
- const tokenHeaders = markdown.pages.filter((page) => page.explicit?.bytes !== void 0 && page.explicit?.markdownTokens !== void 0 && page.explicit.markdownTokens === Math.ceil(page.explicit.bytes / 4) && page.explicit.markdownTokens === page.negotiated?.markdownTokens).length;
6801
+ const tokenHeaders = markdown.pages.filter((page) => {
6802
+ return (page.explicit ?? page.negotiated)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
6803
+ }).length;
6743
6804
  return [
6744
6805
  check$1("representations", {
6745
6806
  id: "markdown-coverage",
6746
6807
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
6747
- title: coverageComplete ? "Every successful HTML page advertises one Markdown alternative" : "Markdown alternative coverage is incomplete",
6748
- plainEnglish: `${markdown.advertisedPages} of ${markdown.eligibleHtmlPages} successful HTML pages advertised one Markdown alternative, and ${markdown.evaluatedPages} were fetched.`,
6749
- action: coverageComplete ? "Keep route generation tied to the same deterministic page manifest." : "Add one text/markdown alternate for each public HTML page and make every advertised URL return successfully.",
6808
+ title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
6809
+ plainEnglish: `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation.`,
6810
+ action: coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
6750
6811
  evidence: {
6751
6812
  eligibleHtmlPages: markdown.eligibleHtmlPages,
6752
6813
  advertisedPages: markdown.advertisedPages,
6814
+ negotiatedPages,
6753
6815
  evaluatedPages: markdown.evaluatedPages
6754
6816
  },
6755
6817
  urls: failedUrls.slice(0, 25)
@@ -6757,18 +6819,22 @@ function markdownChecks(discovery) {
6757
6819
  check$1("representations", {
6758
6820
  id: "markdown-token-estimates",
6759
6821
  status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "warning",
6760
- title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes the same token estimate" : "Some Markdown responses are missing a stable token estimate",
6761
- plainEnglish: `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned matching X-Markdown-Tokens values on the explicit and negotiated responses. The value is a quick size estimate, not an exact model-specific token count.`,
6762
- action: "Generate the estimate from the stored Markdown bytes and return the same value for explicit and negotiated responses.",
6763
- urls: markdown.pages.filter((page) => page.explicit?.markdownTokens === void 0 || page.explicit.markdownTokens !== page.negotiated?.markdownTokens).map((page) => page.htmlUrl).slice(0, 25)
6822
+ title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
6823
+ plainEnglish: `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count.`,
6824
+ action: "Return a stable token estimate and keep it consistent when both explicit and negotiated responses exist.",
6825
+ urls: markdown.pages.filter((page) => {
6826
+ return (page.explicit ?? page.negotiated)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
6827
+ }).map((page) => page.htmlUrl).slice(0, 25)
6764
6828
  }),
6765
6829
  check$1("representations", {
6766
6830
  id: "markdown-negotiation",
6767
- status: markdown.eligibleHtmlPages === 0 ? "unknown" : bytesMatch && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
6768
- title: bytesMatch && discovery.contentNegotiation.qZeroHonoured ? "Explicit and negotiated Markdown use the same bytes" : "Content negotiation does not match the explicit Markdown contract",
6769
- plainEnglish: `${markdown.exactByteMatches} of ${markdown.eligibleHtmlPages} pages returned identical explicit and negotiated Markdown. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.`,
6770
- action: "Use one stored Markdown artifact for both routes, honour Accept q-values, and send Vary: Accept on negotiated responses.",
6831
+ status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
6832
+ title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
6833
+ plainEnglish: `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.`,
6834
+ action: "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist.",
6771
6835
  evidence: {
6836
+ negotiatedPages,
6837
+ explicitPages,
6772
6838
  exactByteMatches: markdown.exactByteMatches,
6773
6839
  qZero: discovery.contentNegotiation
6774
6840
  },
@@ -6778,8 +6844,8 @@ function markdownChecks(discovery) {
6778
6844
  id: "markdown-determinism",
6779
6845
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : stable ? "pass" : "fail",
6780
6846
  title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
6781
- plainEnglish: `${markdown.stableResponses} of ${markdown.eligibleHtmlPages} repeated explicit responses kept the same SHA-256 digest.`,
6782
- action: "Generate Markdown during the build and remove runtime rewriting, timestamps, or random output.",
6847
+ plainEnglish: `${markdown.stableResponses} of ${markdown.eligibleHtmlPages} repeated Markdown responses kept the same SHA-256 digest.`,
6848
+ action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
6783
6849
  evidence: { stableResponses: markdown.stableResponses },
6784
6850
  urls: failedUrls.slice(0, 25)
6785
6851
  }),
@@ -6893,10 +6959,10 @@ function identityChecks(report) {
6893
6959
  const officialProfiles = [...new Set(report.pages.flatMap((page) => [...page.schemaSameAs ?? [], ...page.socialProfileLinks ?? []]))];
6894
6960
  return [check$1("identity", {
6895
6961
  id: "identity-graph",
6896
- status: hasSoftware && hasWebsite && hasCreator && pageTypes.length > 0 ? "pass" : "warning",
6897
- title: hasSoftware && hasWebsite && hasCreator && pageTypes.length > 0 ? "Structured identity covers the software, website, creator, and pages" : "The structured identity graph is missing a useful entity layer",
6898
- plainEnglish: `Software identity: ${hasSoftware ? "present" : "missing"}. Website identity: ${hasWebsite ? "present" : "missing"}. Creator or publisher identity: ${hasCreator ? "present" : "missing"}. Page-level types: ${pageTypes.length ? pageTypes.join(", ") : "missing"}.`,
6899
- action: "Connect truthful SoftwareApplication, WebSite, creator or publisher, and page nodes. Use sameAs only for official profiles that identify the same entity.",
6962
+ status: hasWebsite && hasCreator && pageTypes.length > 0 ? "pass" : "warning",
6963
+ title: hasWebsite && hasCreator && pageTypes.length > 0 ? "Structured identity covers the website, creator, and pages" : "The structured identity graph is missing a useful entity layer",
6964
+ plainEnglish: `Website identity: ${hasWebsite ? "present" : "missing"}. Creator or publisher identity: ${hasCreator ? "present" : "missing"}. Page-level types: ${pageTypes.length ? pageTypes.join(", ") : "missing"}. Software identity: ${hasSoftware ? "present" : "not observed and not required for a content site"}.`,
6965
+ action: "Connect truthful WebSite, creator or publisher, and page nodes. Add SoftwareApplication only when the site actually represents software. Use sameAs only for official profiles that identify the same entity.",
6900
6966
  evidence: {
6901
6967
  schemaTypes: [...types].sort(),
6902
6968
  officialProfiles: officialProfiles.sort()
@@ -7384,7 +7450,7 @@ function aiReadiness(report) {
7384
7450
  score: 0,
7385
7451
  evaluated: Boolean(agentDiscovery),
7386
7452
  title: agentDiscovery ? "Markdown alternatives were checked" : "Markdown alternatives were not evaluated",
7387
- plainEnglish: agentDiscovery ? `${agentDiscovery.markdownAlternates.advertisedPages} of ${agentDiscovery.markdownAlternates.eligibleHtmlPages} successful HTML pages advertised one Markdown alternative. ${agentDiscovery.markdownAlternates.exactByteMatches} explicit responses matched content negotiation byte for byte.` : "This crawl does not include the focused representation checks, so it cannot say whether HTML pages publish stable Markdown alternatives.",
7453
+ plainEnglish: agentDiscovery ? `${agentDiscovery.markdownAlternates.evaluatedPages} of ${agentDiscovery.markdownAlternates.eligibleHtmlPages} successful HTML pages returned Markdown through an explicit alternative or content negotiation. ${agentDiscovery.markdownAlternates.exactByteMatches} paired explicit responses matched content negotiation byte for byte.` : "This crawl does not include the focused representation checks, so it cannot say whether HTML pages publish stable Markdown alternatives.",
7388
7454
  action: "Use the focused agent-readiness report when you need route coverage, content negotiation, byte stability, or extraction-quality evidence.",
7389
7455
  evidence: agentDiscovery ? {
7390
7456
  eligibleHtmlPages: agentDiscovery.markdownAlternates.eligibleHtmlPages,
@@ -25567,7 +25633,7 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
25567
25633
  //#endregion
25568
25634
  //#region packages/core/dist/doctor.js
25569
25635
  const REQUIRED_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
25570
- async function runDoctor() {
25636
+ async function runDoctor(dependencies = {}) {
25571
25637
  const paths = getSeoCliPaths();
25572
25638
  const config = readConfig();
25573
25639
  const authMode = getAuthModeStatus();
@@ -25576,6 +25642,23 @@ async function runDoctor() {
25576
25642
  const oauthClient = readOauthClient();
25577
25643
  const tokenClientConfigured = tokens ? Boolean(getClientConfig(tokens.client_source)) : void 0;
25578
25644
  const checks = [];
25645
+ try {
25646
+ const database = (dependencies.checkDatabase ?? checkDatabaseReadiness)();
25647
+ checks.push({
25648
+ id: "local-database",
25649
+ label: "Local database",
25650
+ status: "pass",
25651
+ detail: database.dbPath
25652
+ });
25653
+ } catch (error) {
25654
+ checks.push({
25655
+ id: "local-database",
25656
+ label: "Local database",
25657
+ status: "fail",
25658
+ detail: error instanceof Error ? error.message : "The local database could not be opened.",
25659
+ fix: "Upgrade or reinstall `seo`. If this still fails, report it at https://github.com/iannuttall/seo/issues."
25660
+ });
25661
+ }
25579
25662
  checks.push({
25580
25663
  id: "config-dir",
25581
25664
  label: "Config directory",
@@ -27102,7 +27185,7 @@ function registerCrawlerTools(server) {
27102
27185
  //#region packages/mcp/src/diagnosis-tools.ts
27103
27186
  function registerDiagnosisTools(server, dependencies = {}) {
27104
27187
  server.registerTool("seo_doctor", {
27105
- description: "Check local auth, scopes, config, and defaults",
27188
+ description: "Check the local database, auth, scopes, config, and defaults",
27106
27189
  inputSchema: {}
27107
27190
  }, async () => {
27108
27191
  try {