seo 0.2.5 → 0.2.7

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.d.ts CHANGED
@@ -65,7 +65,9 @@ declare function executeReport(id: string, params?: Record<string, unknown>): Pr
65
65
  declare function runReport(id: string, params?: Record<string, unknown>): Promise<ToolResult>;
66
66
  //#endregion
67
67
  //#region packages/mcp/src/index.d.ts
68
- declare function createServer(): McpServer;
68
+ declare function createServer(options?: {
69
+ telemetry?: boolean;
70
+ }): McpServer;
69
71
  declare function startMcpServer(opts?: {
70
72
  test?: boolean;
71
73
  }): Promise<void>;
package/dist/mcp.js CHANGED
@@ -1,15 +1,15 @@
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-BF_XgK7-.js";
2
+ import { n as load, t as version } from "./package-BI-bLeV4.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
- import { homedir } from "node:os";
5
+ import { arch, homedir, platform } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import { z } from "zod";
8
8
  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";
@@ -149,6 +149,7 @@ function resolveSeoCliPaths(input = {}) {
149
149
  configFile: join(configDir, "config.json"),
150
150
  tokensFile: join(configDir, "tokens.json"),
151
151
  oauthClientFile: join(configDir, "oauth-client.json"),
152
+ telemetryStateFile: join(configDir, "telemetry.json"),
152
153
  cacheDbFile: join(cacheDir, "cache.db")
153
154
  };
154
155
  }
@@ -762,6 +763,52 @@ async function createGoogleAccessTokenClient() {
762
763
  };
763
764
  }
764
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
765
812
  //#region packages/core/dist/storage/database.js
766
813
  const CREATE_SQL = `
767
814
  CREATE TABLE IF NOT EXISTS sites (
@@ -1045,10 +1092,14 @@ function getDb() {
1045
1092
  recursive: true,
1046
1093
  mode: 448
1047
1094
  });
1048
- db = new Database(path);
1095
+ db = new SqliteDatabase(path);
1049
1096
  initDb(db);
1050
1097
  return db;
1051
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
+ }
1052
1103
  function hashKey(parts) {
1053
1104
  const json = JSON.stringify(parts);
1054
1105
  return Buffer.from(json).toString("base64url");
@@ -25566,7 +25617,7 @@ async function updatePostmortemWorkflow(input, dependencies = {}) {
25566
25617
  //#endregion
25567
25618
  //#region packages/core/dist/doctor.js
25568
25619
  const REQUIRED_SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly", "https://www.googleapis.com/auth/analytics.readonly"];
25569
- async function runDoctor() {
25620
+ async function runDoctor(dependencies = {}) {
25570
25621
  const paths = getSeoCliPaths();
25571
25622
  const config = readConfig();
25572
25623
  const authMode = getAuthModeStatus();
@@ -25575,6 +25626,23 @@ async function runDoctor() {
25575
25626
  const oauthClient = readOauthClient();
25576
25627
  const tokenClientConfigured = tokens ? Boolean(getClientConfig(tokens.client_source)) : void 0;
25577
25628
  const checks = [];
25629
+ try {
25630
+ const database = (dependencies.checkDatabase ?? checkDatabaseReadiness)();
25631
+ checks.push({
25632
+ id: "local-database",
25633
+ label: "Local database",
25634
+ status: "pass",
25635
+ detail: database.dbPath
25636
+ });
25637
+ } catch (error) {
25638
+ checks.push({
25639
+ id: "local-database",
25640
+ label: "Local database",
25641
+ status: "fail",
25642
+ detail: error instanceof Error ? error.message : "The local database could not be opened.",
25643
+ fix: "Upgrade or reinstall `seo`. If this still fails, report it at https://github.com/iannuttall/seo/issues."
25644
+ });
25645
+ }
25578
25646
  checks.push({
25579
25647
  id: "config-dir",
25580
25648
  label: "Config directory",
@@ -25882,6 +25950,317 @@ new LocalCredentialsProvider();
25882
25950
  //#endregion
25883
25951
  //#region packages/core/dist/version.js
25884
25952
  const SEO_VERSION = version;
25953
+ const TELEMETRY_TIMEOUT_MS = 2e3;
25954
+ const TELEMETRY_REPORTS = [
25955
+ "affected-urls",
25956
+ "agent-readiness",
25957
+ "ai-readiness",
25958
+ "ai-referrals",
25959
+ "ai-search-scorecard",
25960
+ "audit-page",
25961
+ "audit-urls",
25962
+ "cannibalisation",
25963
+ "community-intent",
25964
+ "compare-crawls",
25965
+ "content-optimization",
25966
+ "crawl-diff",
25967
+ "site-crawl",
25968
+ "ctr-underperformers",
25969
+ "decaying-pages",
25970
+ "setup-check",
25971
+ "entity-readiness",
25972
+ "explain-crawl-issue",
25973
+ "geo-gaps",
25974
+ "crawl-report",
25975
+ "index-coverage",
25976
+ "index-coverage-plan",
25977
+ "index-monitor",
25978
+ "index-watch",
25979
+ "internal-links",
25980
+ "link-recovery",
25981
+ "crawl-history",
25982
+ "crawler-rules",
25983
+ "llms-txt-audit",
25984
+ "generate-llms-txt",
25985
+ "measure-change",
25986
+ "monthly-report",
25987
+ "okf-build",
25988
+ "okf-validate",
25989
+ "page-opportunities",
25990
+ "performance-audit",
25991
+ "pseo-audit",
25992
+ "query-clusters",
25993
+ "quick-wins",
25994
+ "redirect-trace",
25995
+ "narrative-report",
25996
+ "second-page",
25997
+ "segment-impact",
25998
+ "striking-distance",
25999
+ "seo-to-ai-query",
26000
+ "top-fixes",
26001
+ "traffic-anomaly",
26002
+ "update-correlation",
26003
+ "search-performance-overview",
26004
+ "monthly-action-plan",
26005
+ "refresh-priorities",
26006
+ "technical-watch",
26007
+ "update-postmortem"
26008
+ ];
26009
+ const RETENTION_MILESTONES = [
26010
+ {
26011
+ days: 1,
26012
+ event: "active_d1"
26013
+ },
26014
+ {
26015
+ days: 7,
26016
+ event: "active_d7"
26017
+ },
26018
+ {
26019
+ days: 30,
26020
+ event: "active_d30"
26021
+ }
26022
+ ];
26023
+ const CI_ENVIRONMENT_KEYS = [
26024
+ "GITHUB_ACTIONS",
26025
+ "GITLAB_CI",
26026
+ "CIRCLECI",
26027
+ "TRAVIS",
26028
+ "BUILDKITE",
26029
+ "JENKINS_URL",
26030
+ "TEAMCITY_VERSION",
26031
+ "TF_BUILD",
26032
+ "BITBUCKET_BUILD_NUMBER",
26033
+ "CODEBUILD_BUILD_ID"
26034
+ ];
26035
+ const TELEMETRY_NOTICE = `SEO Skill collects anonymous usage data to improve the tool (event name,
26036
+ report id, version, agent, OS, architecture, Node major, and install week;
26037
+ never URLs, report data, or identifiers of any kind).
26038
+ Docs: https://seoskill.dev/telemetry
26039
+ Disable: seo telemetry disable (or set DO_NOT_TRACK=1)\n`;
26040
+ function optionEnv(options) {
26041
+ return options.env ?? process.env;
26042
+ }
26043
+ function optionNow(options) {
26044
+ return options.now ?? /* @__PURE__ */ new Date();
26045
+ }
26046
+ function optionStateFile(options) {
26047
+ return options.stateFile ?? getSeoCliPaths().telemetryStateFile;
26048
+ }
26049
+ function isEnvEnabled(value) {
26050
+ return value === "1" || value?.toLowerCase() === "true";
26051
+ }
26052
+ function isTelemetryCi(env = process.env) {
26053
+ return env.NODE_ENV === "test" || Boolean(env.NODE_TEST_CONTEXT) || isEnvEnabled(env.CI) || CI_ENVIRONMENT_KEYS.some((key) => Boolean(env[key]));
26054
+ }
26055
+ function environmentDisableReason(env) {
26056
+ if (isTelemetryCi(env)) return "ci";
26057
+ if (isEnvEnabled(env.DO_NOT_TRACK)) return "do_not_track";
26058
+ if (isEnvEnabled(env.SEOSKILL_TELEMETRY_DISABLED) || isEnvEnabled(env.SEO_TELEMETRY_DISABLED)) return "environment";
26059
+ }
26060
+ function isTelemetryMilestone(value) {
26061
+ return typeof value === "string" && [
26062
+ "first_run",
26063
+ "setup_complete",
26064
+ "first_audit_complete",
26065
+ "active_d1",
26066
+ "active_d7",
26067
+ "active_d30"
26068
+ ].includes(value);
26069
+ }
26070
+ function isTelemetryReport(value) {
26071
+ return typeof value === "string" && TELEMETRY_REPORTS.includes(value);
26072
+ }
26073
+ function isValidCohort(value) {
26074
+ return typeof value === "string" && /^\d{4}-W(?:0[1-9]|[1-4]\d|5[0-3])$/.test(value);
26075
+ }
26076
+ function isTelemetryState(value) {
26077
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
26078
+ const state = value;
26079
+ return typeof state.telemetryEnabled === "boolean" && typeof state.firstRunAt === "string" && Number.isFinite(Date.parse(state.firstRunAt)) && isValidCohort(state.cohort) && Array.isArray(state.sentMilestones) && state.sentMilestones.every(isTelemetryMilestone);
26080
+ }
26081
+ function readState(path) {
26082
+ if (!existsSync(path)) return { kind: "missing" };
26083
+ const value = readJsonFile(path);
26084
+ return isTelemetryState(value) ? {
26085
+ kind: "valid",
26086
+ state: value
26087
+ } : { kind: "invalid" };
26088
+ }
26089
+ function writeState(path, state) {
26090
+ writeJsonAtomic(path, state, 384);
26091
+ }
26092
+ function isoWeek(date) {
26093
+ const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
26094
+ const day = utc.getUTCDay() || 7;
26095
+ utc.setUTCDate(utc.getUTCDate() + 4 - day);
26096
+ const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
26097
+ const week = Math.ceil(((utc.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
26098
+ return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
26099
+ }
26100
+ function newState(options, enabled = true) {
26101
+ const now = optionNow(options);
26102
+ return {
26103
+ telemetryEnabled: enabled,
26104
+ firstRunAt: now.toISOString(),
26105
+ cohort: isoWeek(now),
26106
+ sentMilestones: []
26107
+ };
26108
+ }
26109
+ function detectTelemetryAgent(input = {}) {
26110
+ const env = input.env ?? process.env;
26111
+ const clientName = input.clientName?.toLowerCase() ?? "";
26112
+ if (clientName.includes("claude")) return "claude-code";
26113
+ if (clientName.includes("cursor")) return "cursor";
26114
+ if (clientName.includes("codex")) return "codex";
26115
+ if (input.clientName) return "unknown";
26116
+ if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude-code";
26117
+ if (env.CURSOR_AGENT || env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID) return "cursor";
26118
+ if (env.CODEX_HOME || env.CODEX_THREAD_ID || env.CODEX_SANDBOX) return "codex";
26119
+ return "cli";
26120
+ }
26121
+ function telemetryAgent(options) {
26122
+ return options.agent ?? detectTelemetryAgent({
26123
+ clientName: options.clientName,
26124
+ env: optionEnv(options)
26125
+ });
26126
+ }
26127
+ function nodeMajor(version) {
26128
+ return version.replace(/^v/, "").split(".")[0] || "unknown";
26129
+ }
26130
+ function buildTelemetryPayload(event, state, fields = {}, options = {}) {
26131
+ return {
26132
+ event,
26133
+ version: options.version ?? SEO_VERSION,
26134
+ agent: telemetryAgent(options),
26135
+ os: options.processPlatform ?? platform(),
26136
+ arch: options.processArch ?? arch(),
26137
+ node: nodeMajor(options.nodeVersion ?? process.version),
26138
+ cohort: state.cohort,
26139
+ schema: 1,
26140
+ ...fields.errorCategory ? { errorCategory: fields.errorCategory } : {},
26141
+ ...fields.report ? { report: fields.report } : {}
26142
+ };
26143
+ }
26144
+ function dispatchTelemetry(payload, options) {
26145
+ const fetchTelemetry = options.fetch ?? globalThis.fetch;
26146
+ try {
26147
+ fetchTelemetry(options.endpoint ?? "https://seoskill.dev/api/t", {
26148
+ method: "POST",
26149
+ headers: { "content-type": "application/json" },
26150
+ body: JSON.stringify(payload),
26151
+ signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS)
26152
+ }).catch(() => void 0);
26153
+ } catch {}
26154
+ }
26155
+ function statusFromState(options) {
26156
+ const stateFile = optionStateFile(options);
26157
+ const envReason = environmentDisableReason(optionEnv(options));
26158
+ const stored = readState(stateFile);
26159
+ if (envReason) return {
26160
+ enabled: false,
26161
+ reason: envReason,
26162
+ stateFile,
26163
+ ...stored.kind === "valid" ? { state: stored.state } : {}
26164
+ };
26165
+ if (stored.kind === "invalid") return {
26166
+ enabled: false,
26167
+ reason: "invalid_state",
26168
+ stateFile
26169
+ };
26170
+ if (stored.kind === "valid" && !stored.state.telemetryEnabled) return {
26171
+ enabled: false,
26172
+ reason: "local_setting",
26173
+ stateFile,
26174
+ state: stored.state
26175
+ };
26176
+ return {
26177
+ enabled: true,
26178
+ stateFile,
26179
+ ...stored.kind === "valid" ? { state: stored.state } : {}
26180
+ };
26181
+ }
26182
+ function enabledState(options) {
26183
+ const status = statusFromState(options);
26184
+ return status.enabled ? status.state : void 0;
26185
+ }
26186
+ function initializeTelemetry(options = {}) {
26187
+ const initialStatus = statusFromState(options);
26188
+ if (!initialStatus.enabled) return initialStatus;
26189
+ const stateFile = initialStatus.stateFile;
26190
+ const state = initialStatus.state ?? newState(options);
26191
+ const events = [];
26192
+ if (!state.sentMilestones.includes("first_run")) {
26193
+ try {
26194
+ (options.notice ?? ((message) => process.stderr.write(message)))(TELEMETRY_NOTICE);
26195
+ } catch {
26196
+ return {
26197
+ enabled: false,
26198
+ reason: "invalid_state",
26199
+ stateFile
26200
+ };
26201
+ }
26202
+ state.sentMilestones.push("first_run");
26203
+ events.push("first_run");
26204
+ }
26205
+ const daysSinceFirstRun = (optionNow(options).getTime() - Date.parse(state.firstRunAt)) / 864e5;
26206
+ for (const milestone of RETENTION_MILESTONES) if (daysSinceFirstRun >= milestone.days && !state.sentMilestones.includes(milestone.event)) {
26207
+ state.sentMilestones.push(milestone.event);
26208
+ events.push(milestone.event);
26209
+ }
26210
+ try {
26211
+ writeState(stateFile, state);
26212
+ } catch {
26213
+ return {
26214
+ enabled: false,
26215
+ reason: "invalid_state",
26216
+ stateFile
26217
+ };
26218
+ }
26219
+ for (const event of events) dispatchTelemetry(buildTelemetryPayload(event, state, {}, options), options);
26220
+ return {
26221
+ enabled: true,
26222
+ stateFile,
26223
+ state
26224
+ };
26225
+ }
26226
+ function sendEvent(event, fields, options) {
26227
+ const state = enabledState(options);
26228
+ if (!state) return;
26229
+ dispatchTelemetry(buildTelemetryPayload(event, state, fields, options), options);
26230
+ }
26231
+ function trackTelemetryReportStart(report, options = {}) {
26232
+ if (!isTelemetryReport(report)) return;
26233
+ sendEvent("audit_start", { report }, options);
26234
+ }
26235
+ function trackTelemetryReportComplete(report, options = {}) {
26236
+ if (!isTelemetryReport(report)) return;
26237
+ const state = enabledState(options);
26238
+ if (!state) return;
26239
+ dispatchTelemetry(buildTelemetryPayload("audit_complete", state, { report }, options), options);
26240
+ if (state.sentMilestones.includes("first_audit_complete")) return;
26241
+ state.sentMilestones.push("first_audit_complete");
26242
+ try {
26243
+ writeState(optionStateFile(options), state);
26244
+ } catch {
26245
+ return;
26246
+ }
26247
+ dispatchTelemetry(buildTelemetryPayload("first_audit_complete", state, { report }, options), options);
26248
+ }
26249
+ function trackTelemetryReportFailed(report, errorCategory, options = {}) {
26250
+ if (!isTelemetryReport(report)) return;
26251
+ sendEvent("audit_failed", {
26252
+ errorCategory,
26253
+ report
26254
+ }, options);
26255
+ }
26256
+ function telemetryErrorCategory(error) {
26257
+ if (error instanceof Error && error.name === "AbortError") return "crawl_timeout";
26258
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
26259
+ if (code === "ACCESS_DENIED" || code === "AUTH_CONFIG_REQUIRED" || code === "AUTH_EXPIRED" || code === "AUTH_REQUIRED") return "auth";
26260
+ if (code === "INVALID_INPUT" || code === "PROPERTY_NOT_FOUND") return "config";
26261
+ if (code === "OPTIONAL_PROVIDER_UNAVAILABLE" || code === "PROVIDER_UNAVAILABLE" || code === "RATE_LIMITED") return "network";
26262
+ return "unknown";
26263
+ }
25885
26264
  //#endregion
25886
26265
  //#region packages/mcp/src/input-schemas.ts
25887
26266
  const calendarDateSchema = z$1.iso.date("Use a YYYY-MM-DD date.");
@@ -26790,7 +27169,7 @@ function registerCrawlerTools(server) {
26790
27169
  //#region packages/mcp/src/diagnosis-tools.ts
26791
27170
  function registerDiagnosisTools(server, dependencies = {}) {
26792
27171
  server.registerTool("seo_doctor", {
26793
- description: "Check local auth, scopes, config, and defaults",
27172
+ description: "Check the local database, auth, scopes, config, and defaults",
26794
27173
  inputSchema: {}
26795
27174
  }, async () => {
26796
27175
  try {
@@ -30053,7 +30432,8 @@ async function runReport(id, params = {}) {
30053
30432
  //#endregion
30054
30433
  //#region packages/mcp/src/discovery-tools.ts
30055
30434
  const openOutputSchema = z$1.looseObject({});
30056
- function registerDiscoveryTools(server) {
30435
+ const reportIds = new Set(listReports().map((report) => report.id));
30436
+ function registerDiscoveryTools(server, options = {}) {
30057
30437
  server.registerTool("seo_list_reports", {
30058
30438
  description: "List compact SEO report ids, names, and purposes, optionally by category",
30059
30439
  inputSchema: { category: z$1.enum(REPORT_CATEGORIES).optional() },
@@ -30097,17 +30477,31 @@ function registerDiscoveryTools(server) {
30097
30477
  outputSchema: openOutputSchema,
30098
30478
  annotations: { destructiveHint: false }
30099
30479
  }, async ({ id, params }) => {
30100
- return runReport(id, params);
30480
+ const telemetry = reportIds.has(id) ? options.telemetry?.() : void 0;
30481
+ if (telemetry) trackTelemetryReportStart(id, telemetry);
30482
+ try {
30483
+ const result = await runReport(id, params);
30484
+ if (telemetry) if (result.isError) trackTelemetryReportFailed(id, telemetryErrorCategory(result.structuredContent?.error), telemetry);
30485
+ else trackTelemetryReportComplete(id, telemetry);
30486
+ return result;
30487
+ } catch (error) {
30488
+ if (telemetry) trackTelemetryReportFailed(id, telemetryErrorCategory(error), telemetry);
30489
+ throw error;
30490
+ }
30101
30491
  });
30102
30492
  }
30103
30493
  //#endregion
30104
30494
  //#region packages/mcp/src/index.ts
30105
- function createServer() {
30495
+ function createServer(options = {}) {
30106
30496
  const server = new McpServer({
30107
30497
  name: "seo",
30108
30498
  version: SEO_VERSION
30109
30499
  }, { capabilities: { logging: {} } });
30110
- registerDiscoveryTools(server);
30500
+ const telemetryOptions = () => ({ clientName: server.server.getClientVersion()?.name });
30501
+ registerDiscoveryTools(server, { telemetry: options.telemetry === false ? void 0 : telemetryOptions });
30502
+ if (options.telemetry !== false) server.server.oninitialized = () => {
30503
+ initializeTelemetry(telemetryOptions());
30504
+ };
30111
30505
  return server;
30112
30506
  }
30113
30507
  async function startMcpServer(opts = {}) {