geo-new 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7311,7 +7311,7 @@ var ApiError = class extends Error {
7311
7311
  };
7312
7312
 
7313
7313
  // packages/geo-new/src/client.ts
7314
- var VERSION = true ? "0.1.0" : "0.0.0-dev";
7314
+ var VERSION = true ? "0.1.2" : "0.0.0-dev";
7315
7315
  var DEFAULT_ORIGIN = "https://geo.new";
7316
7316
  var isLoopback = (url2) => url2.hostname === "localhost" || url2.hostname === "127.0.0.1" || url2.hostname === "[::1]" || url2.hostname === "::1";
7317
7317
  var sleep = (ms, signal) => new Promise((resolve, reject) => {
@@ -7510,7 +7510,7 @@ var GeoNewClient = class {
7510
7510
  snapshot = await this.snapshot(id, { signal: options.signal });
7511
7511
  }
7512
7512
  }
7513
- /** The keyless session token, when the client holds one; the CLI prints it on request, never by default. */
7513
+ /** The last audit this machine started, so a bare `geo-new report` has something to read. */
7514
7514
  get lastAuditId() {
7515
7515
  return this.credentials.load().last_audit_id;
7516
7516
  }
@@ -7565,6 +7565,105 @@ var clock = (ms) => {
7565
7565
  function detectUtf8(env = process.env) {
7566
7566
  return /utf-?8/i.test(env.LC_ALL ?? env.LC_CTYPE ?? env.LANG ?? "") || process.platform === "win32";
7567
7567
  }
7568
+ var ACCENT = "38;5;173";
7569
+ var COMET = [ACCENT, "38;5;137", "38;5;95"];
7570
+ var BASE = "38;5;240";
7571
+ var FACE = {
7572
+ G: [".####", "#....", "#..##", "#...#", ".####"],
7573
+ E: ["#####", "#....", "####.", "#....", "#####"],
7574
+ O: [".###.", "#...#", "#...#", "#...#", ".###."],
7575
+ ".": [".", ".", ".", ".", "#"],
7576
+ n: [".....", "####.", "#...#", "#...#", "#...#"],
7577
+ e: [".....", ".###.", "#####", "#....", ".####"],
7578
+ w: [".....", "#...#", "#.#.#", "#.#.#", ".#.#."]
7579
+ };
7580
+ var WORD = "GEO.new";
7581
+ var GAP = 2;
7582
+ var ROWS = 5;
7583
+ var CARET = 4;
7584
+ var STARTS = [...WORD].reduce(
7585
+ (xs, ch) => [...xs, xs[xs.length - 1] + FACE[ch][0].length + GAP],
7586
+ [0]
7587
+ );
7588
+ var BOOT_WIDTH = STARTS[WORD.length] - GAP + 1 + CARET;
7589
+ var RING = [
7590
+ [0, 0],
7591
+ [0, 1],
7592
+ [0, 2],
7593
+ [1, 2],
7594
+ [2, 2],
7595
+ [2, 1],
7596
+ [2, 0],
7597
+ [1, 0]
7598
+ ];
7599
+ var BOOT = [
7600
+ [70, 0],
7601
+ [70, 0],
7602
+ [70, 0],
7603
+ [70, 0],
7604
+ [70, 0],
7605
+ [70, 0],
7606
+ [70, 0],
7607
+ [70, 0],
7608
+ [110, 0],
7609
+ [120, 0],
7610
+ [70, 1],
7611
+ [70, 2],
7612
+ [70, 3],
7613
+ [120, 3],
7614
+ // a beat between the name and the suffix, as in the design
7615
+ [65, 4],
7616
+ [65, 5],
7617
+ [65, 6],
7618
+ [420, 7]
7619
+ // the caret stands here, then the boot is over
7620
+ ];
7621
+ var LAST = BOOT.length - 1;
7622
+ var UP = `\r\x1B[${Math.ceil(ROWS / 2) - 1}A`;
7623
+ var sgr = (p, glyph) => p ? `\x1B[${p}m${glyph}\x1B[0m` : glyph;
7624
+ var bg = (p) => p.replace(/^38;/, "48;");
7625
+ function cell(u, l) {
7626
+ if (u === null) return l === null ? " " : sgr(l, "\u2584");
7627
+ if (l === null) return sgr(u, "\u2580");
7628
+ if (u === l) return sgr(u, "\u2588");
7629
+ if (!u) return sgr(bg(l), "\u2580");
7630
+ if (!l) return sgr(bg(u), "\u2584");
7631
+ return sgr(`${u};${bg(l)}`, "\u2580");
7632
+ }
7633
+ function bootFrame(step, done, color) {
7634
+ const grid = Array.from({ length: ROWS }, () => []);
7635
+ const ink = (shade) => color ? shade : "";
7636
+ const put = (row, col, shade) => grid[row][col] = shade;
7637
+ const typed = done ? WORD.length : BOOT[step][1];
7638
+ [...WORD].slice(0, typed).forEach(
7639
+ (ch, i) => FACE[ch].forEach(
7640
+ (row, r) => [...row].forEach((p, c) => p === "#" && put(r, STARTS[i] + c, i < 3 ? ink(ACCENT) : ""))
7641
+ )
7642
+ );
7643
+ if (!done && step <= 7)
7644
+ RING.forEach(([r, c], i) => {
7645
+ const k = (step - i + RING.length) % RING.length;
7646
+ put(r + 1, c + 1, color ? COMET[k] ?? BASE : k < COMET.length ? "" : null);
7647
+ });
7648
+ else if (!done && step === 8)
7649
+ for (let r = 1; r <= 3; r++) for (let c = 1; c <= 3; c++) put(r, c, ink(ACCENT));
7650
+ else if (!done && step === 9) {
7651
+ for (let r = 0; r < ROWS; r++)
7652
+ for (let c = 0; c < ROWS; c++) if (r === 0 || r === 4 || c === 0 || c === 4) put(r, c, ink(COMET[2]));
7653
+ }
7654
+ if (!done && step >= 10) {
7655
+ const x = STARTS[typed] - GAP + 1;
7656
+ for (let r = 0; r < ROWS; r++) for (let c = 0; c < CARET; c++) put(r, x + c, "");
7657
+ }
7658
+ const width = Math.max(0, ...grid.map((row) => row.length));
7659
+ const lines = [];
7660
+ for (let r = 0; r < ROWS; r += 2) {
7661
+ let line = "";
7662
+ for (let c = 0; c < width; c++) line += cell(grid[r][c] ?? null, grid[r + 1]?.[c] ?? null);
7663
+ lines.push(line.replace(/ +$/, ""));
7664
+ }
7665
+ return lines;
7666
+ }
7568
7667
  var Output = class {
7569
7668
  stream;
7570
7669
  tty;
@@ -7574,6 +7673,11 @@ var Output = class {
7574
7673
  verbose;
7575
7674
  lastLine = "";
7576
7675
  lineOpen = false;
7676
+ booting;
7677
+ /** A process that exits mid-boot must leave the cursor visible and the prompt on its own line. */
7678
+ abandon = () => {
7679
+ if (this.booting) this.stream.write("\x1B[?25h\n");
7680
+ };
7577
7681
  constructor(options = {}) {
7578
7682
  this.stream = options.stream ?? process.stderr;
7579
7683
  this.tty = options.tty ?? Boolean(process.stderr.isTTY);
@@ -7587,9 +7691,51 @@ var Output = class {
7587
7691
  const symbol2 = this.utf8 ? utf8 : ascii;
7588
7692
  return this.color ? `\x1B[${color}m${symbol2}\x1B[0m` : symbol2;
7589
7693
  }
7694
+ /**
7695
+ * Runs the wordmark's boot while an audit is admitted. Progress waits behind it: the first snapshot
7696
+ * lets the ring finish its turn and hand over to the name, and `settle` cuts it to the finished name.
7697
+ */
7698
+ boot() {
7699
+ if (this.quiet || !this.tty || !this.utf8 || this.booting) return;
7700
+ if ((this.stream.columns || 80) < BOOT_WIDTH) return;
7701
+ this.booting = { step: 0, seen: false };
7702
+ process.once("exit", this.abandon);
7703
+ this.stream.write(`\x1B[?25l
7704
+ ${this.frame(0)}`);
7705
+ this.tick();
7706
+ }
7707
+ frame(step, done = false) {
7708
+ return bootFrame(step, done, this.color).map((line) => `\x1B[2K${line}`).join("\n");
7709
+ }
7710
+ tick() {
7711
+ const b = this.booting;
7712
+ if (!b) return;
7713
+ b.timer = setTimeout(() => {
7714
+ if (b.step === LAST) return this.endBoot();
7715
+ b.step = b.step === 7 && !b.seen ? 0 : b.step + 1;
7716
+ this.stream.write(`${UP}${this.frame(b.step)}`);
7717
+ this.tick();
7718
+ }, BOOT[b.step][0]);
7719
+ }
7720
+ endBoot() {
7721
+ const b = this.booting;
7722
+ if (!b) return;
7723
+ clearTimeout(b.timer);
7724
+ this.booting = void 0;
7725
+ process.off("exit", this.abandon);
7726
+ this.stream.write(`${UP}${this.frame(LAST, true)}\x1B[?25h
7727
+
7728
+ `);
7729
+ if (b.pending) this.progress(...b.pending);
7730
+ }
7590
7731
  /** One live line on a TTY, one line per change otherwise. */
7591
7732
  progress(snapshot, elapsedMs) {
7592
7733
  if (this.quiet) return;
7734
+ if (this.booting) {
7735
+ this.booting.seen = true;
7736
+ this.booting.pending = [snapshot, elapsedMs];
7737
+ return;
7738
+ }
7593
7739
  const parts = Object.entries(snapshot.sections).filter(([key]) => key in SECTION_LABEL).map(([key, section]) => `${SECTION_LABEL[key]} ${this.glyph(section.state)}`);
7594
7740
  const line = `${parts.join(" ")} ${clock(elapsedMs)}`;
7595
7741
  if (this.tty) {
@@ -7604,6 +7750,7 @@ var Output = class {
7604
7750
  }
7605
7751
  /** Ends a live progress line before anything else is printed. */
7606
7752
  settle() {
7753
+ this.endBoot();
7607
7754
  if (this.lineOpen) {
7608
7755
  this.stream.write("\n");
7609
7756
  this.lineOpen = false;
@@ -7648,7 +7795,9 @@ function comparisonLines(comparison) {
7648
7795
  const c = comparison ?? {};
7649
7796
  const delta = (v) => v === null || v === void 0 ? "unmeasured" : v > 0 ? `+${v}` : `${v}`;
7650
7797
  const count = (change) => (c.checks ?? []).filter((k) => k.change === change).length;
7651
- const lines = [`geo ${delta(c.score_delta?.geo)} \xB7 seo ${delta(c.score_delta?.seo)} \xB7 ${c.state ?? "unknown"}`];
7798
+ const lines = [
7799
+ `geo ${delta(c.score_delta?.geo)} \xB7 seo ${delta(c.score_delta?.seo)} \xB7 ${c.state ?? "unknown"}`
7800
+ ];
7652
7801
  const moved = [
7653
7802
  ["fixed", count("fixed")],
7654
7803
  ["new", count("new")],
@@ -37127,6 +37276,11 @@ var CreateAuditSchema = external_exports.object({ url: external_exports.string()
37127
37276
  var IssueKeySchema = external_exports.object({
37128
37277
  label: external_exports.string().regex(/^[\x20-\x7e]{1,40}$/).optional()
37129
37278
  }).strict();
37279
+ var AdminPlanSchema = external_exports.object({
37280
+ plan: external_exports.enum(["free", "builder"]),
37281
+ days: external_exports.number().int().min(1).max(365).optional(),
37282
+ reason: external_exports.string().min(1).max(200).optional()
37283
+ }).strict();
37130
37284
  var ClaimSessionSchema = external_exports.object({ key: external_exports.string().min(1).max(128) }).strict();
37131
37285
 
37132
37286
  // src/mcp/summary.ts
@@ -37157,6 +37311,22 @@ function summarize(snapshot, actionLimit = 8) {
37157
37311
  ai: snapshot.ai.status
37158
37312
  };
37159
37313
  }
37314
+ function summarizeComparison(comparison, current = null, changedLimit = 40) {
37315
+ const c = comparison ?? {};
37316
+ const moved = (c.checks ?? []).filter((row) => row.change !== "unchanged"), before = c.baseline?.scores?.catalog?.value ?? null, after = current?.scores.catalog?.value ?? null;
37317
+ return {
37318
+ baseline_id: c.baseline_id ?? null,
37319
+ current_id: c.current_id ?? null,
37320
+ state: c.state ?? "unknown",
37321
+ reason: c.reason ?? null,
37322
+ score_delta: c.score_delta ?? null,
37323
+ score: { before, after, delta: before === null || after === null ? null : after - before },
37324
+ changed: moved.slice(0, changedLimit),
37325
+ changed_omitted: Math.max(0, moved.length - changedLimit),
37326
+ unchanged: (c.checks ?? []).length - moved.length,
37327
+ resolved_actions: c.resolved_actions ?? []
37328
+ };
37329
+ }
37160
37330
 
37161
37331
  // src/mcp/server.ts
37162
37332
  var MAX_WAIT_SECONDS = 45;
@@ -37334,7 +37504,7 @@ function createMcpServer(api, options) {
37334
37504
  "compare_reports",
37335
37505
  {
37336
37506
  title: "Compare two reports",
37337
- description: "Score, category and row deltas between a report and an earlier one of the same URL that you also own.",
37507
+ description: "Score, category and row deltas between a report and an earlier one of the same URL that you also own. The result is the rows that moved and a count of the rest, not the baseline report; read either side with get_report.",
37338
37508
  inputSchema: {
37339
37509
  audit_id: external_exports.string().regex(/^aud_[a-f0-9]{32}$/),
37340
37510
  baseline_id: external_exports.string().regex(/^aud_[a-f0-9]{32}$/)
@@ -37342,7 +37512,11 @@ function createMcpServer(api, options) {
37342
37512
  },
37343
37513
  async ({ audit_id, baseline_id }) => {
37344
37514
  try {
37345
- return text(await api.compare(audit_id, baseline_id));
37515
+ const [comparison, current] = await Promise.all([
37516
+ api.compare(audit_id, baseline_id),
37517
+ api.snapshot(audit_id)
37518
+ ]);
37519
+ return text(summarizeComparison(comparison, current));
37346
37520
  } catch (error61) {
37347
37521
  return failed(error61, { tool: "compare_reports", args: { audit_id, baseline_id } });
37348
37522
  }
@@ -37549,19 +37723,32 @@ var FLAGS = {
37549
37723
  help: false,
37550
37724
  version: false
37551
37725
  };
37552
- var COMMANDS = ["audit", "report", "fix", "share", "compare", "usage", "key", "login", "logout", "mcp"];
37726
+ var COMMANDS = [
37727
+ "audit",
37728
+ "report",
37729
+ "fix",
37730
+ "share",
37731
+ "compare",
37732
+ "usage",
37733
+ "key",
37734
+ "login",
37735
+ "logout",
37736
+ "mcp"
37737
+ ];
37553
37738
  var HELP = `geo-new ${VERSION} \u2014 audit one page for GEO, SEO and agent readiness
37554
37739
 
37555
37740
  npx -y geo-new audit <url> [--fresh] [--json] [--out file] [--wait s] [--wait-for-ai] [--share] [--open]
37556
37741
  [--min-score N] [--allow-unscored]
37557
37742
  geo-new report [audit_id] [--json] [--out file] geo-new fix [audit_id]
37558
- geo-new share [audit_id] [--revoke] geo-new usage [--json]
37559
- geo-new compare <before> <after> [--min-score N] [--allow-drop] [--allow-missing-baseline] [--json]
37743
+ geo-new share [audit_id] [--revoke] [--open] geo-new usage [--json]
37744
+ geo-new compare <before> <after> [--json] [--out file] [--min-score N] [--allow-drop]
37745
+ [--allow-missing-baseline]
37560
37746
  geo-new key create [--label name] | list | revoke <prefix> | set <key>
37561
37747
  geo-new login <key> geo-new logout geo-new mcp geo-new --version
37562
37748
 
37563
37749
  Keyless by default: the first call mints a session and keeps it in your config directory.
37564
37750
  A key (geo-new key create) is the durable identity with a larger allowance; GEO_NEW_API_KEY also works.
37751
+ --out writes JSON, the snapshot compare reads, and leaves stdout empty; the report itself is Markdown.
37565
37752
  Exit codes: 0 done \xB7 1 audit failed \xB7 2 usage \xB7 3 rate limited \xB7 4 auth or ownership \xB7 5 network or server
37566
37753
  6 below --min-score \xB7 7 score dropped \xB7 8 still running when --wait expired \xB7 130 interrupted
37567
37754
  Docs: https://geo.new/docs/api`;
@@ -37606,7 +37793,8 @@ function parseArgs(argv) {
37606
37793
  var integer2 = (value, name, fallback) => {
37607
37794
  if (value === void 0) return fallback;
37608
37795
  const n = Number(value);
37609
- if (!Number.isInteger(n) || n < 0) throw new Exit(EXIT.usage, `--${name} needs a whole number`);
37796
+ if (!Number.isInteger(n) || n < 0)
37797
+ throw new Exit(EXIT.usage, `--${name} needs a whole number of 0 or more`);
37610
37798
  return n;
37611
37799
  };
37612
37800
  function errorLine(error61) {
@@ -37673,7 +37861,7 @@ async function main(argv, deps = {}) {
37673
37861
  if (flags.help || !command) {
37674
37862
  stdout.write(`${HELP}
37675
37863
  `);
37676
- return command ? EXIT.ok : EXIT.usage;
37864
+ return flags.help ? EXIT.ok : EXIT.usage;
37677
37865
  }
37678
37866
  if (!COMMANDS.includes(command)) {
37679
37867
  const hint = nearest(command, COMMANDS);
@@ -37695,9 +37883,10 @@ async function main(argv, deps = {}) {
37695
37883
  } else if (flags.json) stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
37696
37884
  else stdout.write((await client.markdown(snapshot.audit_id)).text);
37697
37885
  };
37886
+ let floor = null;
37698
37887
  const minimum = async (snapshot) => {
37699
- if (flags["min-score"] === void 0) return EXIT.ok;
37700
- const min = integer2(flags["min-score"], "min-score", 0);
37888
+ if (floor === null) return EXIT.ok;
37889
+ const min = floor;
37701
37890
  const value = scoreOf(snapshot);
37702
37891
  if (value === null) {
37703
37892
  if (flags["allow-unscored"]) return EXIT.ok;
@@ -37713,11 +37902,13 @@ async function main(argv, deps = {}) {
37713
37902
  return EXIT.ok;
37714
37903
  };
37715
37904
  try {
37905
+ if (flags["min-score"] !== void 0) floor = integer2(flags["min-score"], "min-score", 0);
37716
37906
  switch (command) {
37717
37907
  case "audit": {
37718
37908
  const url2 = positional[0];
37719
37909
  if (!url2) throw new Exit(EXIT.usage, "usage: geo-new audit <url>");
37720
37910
  const waitSeconds = integer2(flags.wait, "wait", 90);
37911
+ if (waitSeconds > 0) output2.boot();
37721
37912
  const envelope = await client.audit(url2, {
37722
37913
  fresh: Boolean(flags.fresh),
37723
37914
  signal: deps.signal
@@ -37725,7 +37916,8 @@ async function main(argv, deps = {}) {
37725
37916
  deps.onAudit?.(envelope.audit_id);
37726
37917
  output2.debug(`admitted ${envelope.audit_id} \xB7 cached ${envelope.cached}`);
37727
37918
  if (waitSeconds === 0) {
37728
- stdout.write(JSON.stringify(envelope) + "\n");
37919
+ const { session: _session, ...admission } = envelope;
37920
+ stdout.write(JSON.stringify(admission) + "\n");
37729
37921
  return EXIT.ok;
37730
37922
  }
37731
37923
  const started = Date.now();
@@ -37754,7 +37946,7 @@ async function main(argv, deps = {}) {
37754
37946
  output2.note(line);
37755
37947
  if (shareUrl && flags.open) openBrowser(shareUrl);
37756
37948
  if (snapshot.status === "failed") return EXIT.failed;
37757
- return minimum(snapshot);
37949
+ return await minimum(snapshot);
37758
37950
  }
37759
37951
  case "report": {
37760
37952
  const snapshot = await client.snapshot(lastId(positional[0]));
@@ -37797,16 +37989,18 @@ async function main(argv, deps = {}) {
37797
37989
  const load = async (v) => isFile(v) ? readSnapshotFile(v) : client.snapshot(v);
37798
37990
  const [baseline, current] = await Promise.all([load(before), load(after)]);
37799
37991
  const comparison = isFile(before) || isFile(after) ? compareReports(current, baseline) : await client.compare(after, before);
37992
+ const from = scoreOf(baseline), to = scoreOf(current);
37993
+ const moved = from !== null && to !== null && to !== from ? ` (${to > from ? "+" : ""}${to - from})` : "";
37994
+ const scoreLine = `score ${from ?? "unmeasured"} \u2192 ${to ?? "unmeasured"}${moved}`;
37800
37995
  if (flags.out) writeFileSync2(String(flags.out), JSON.stringify(comparison, null, 2) + "\n", "utf8");
37801
37996
  else if (flags.json) stdout.write(JSON.stringify(comparison, null, 2) + "\n");
37802
- else stdout.write(comparisonLines(comparison).join("\n") + "\n");
37803
- const from = scoreOf(baseline), to = scoreOf(current);
37804
- output2.note(`score ${from ?? "unmeasured"} \u2192 ${to ?? "unmeasured"}`);
37997
+ else stdout.write([scoreLine, ...comparisonLines(comparison)].join("\n") + "\n");
37998
+ if (flags.out || flags.json) output2.note(scoreLine);
37805
37999
  if (from !== null && to !== null && to < from && !flags["allow-drop"]) {
37806
38000
  output2.line(`score dropped by ${from - to}`);
37807
38001
  return EXIT.dropped;
37808
38002
  }
37809
- return minimum(current);
38003
+ return await minimum(current);
37810
38004
  }
37811
38005
  case "usage": {
37812
38006
  const usage = await client.usage();
@@ -37909,7 +38103,7 @@ interrupted${current ? ` \xB7 the audit continues \xB7 geo-new report ${current}
37909
38103
  (error61) => {
37910
38104
  process.stderr.write(`geo-new: ${error61 instanceof Error ? error61.message : String(error61)}
37911
38105
  `);
37912
- process.exit(EXIT.network);
38106
+ process.exit(error61 instanceof Exit ? error61.code : EXIT.network);
37913
38107
  }
37914
38108
  );
37915
38109
  }
package/dist/client.d.ts CHANGED
@@ -90,7 +90,7 @@ export declare class GeoNewClient implements Api {
90
90
  }>;
91
91
  /** Polls until the report is measured (`writing` or terminal), terminal (`ai`), or the timeout. */
92
92
  waitFor(id: string, options?: WaitOptions): Promise<WaitResult>;
93
- /** The keyless session token, when the client holds one; the CLI prints it on request, never by default. */
93
+ /** The last audit this machine started, so a bare `geo-new report` has something to read. */
94
94
  get lastAuditId(): string | undefined;
95
95
  remember(patch: Partial<{
96
96
  key: string;
package/dist/index.js CHANGED
@@ -107,7 +107,7 @@ var ApiError = class extends Error {
107
107
  };
108
108
 
109
109
  // packages/geo-new/src/client.ts
110
- var VERSION = true ? "0.1.0" : "0.0.0-dev";
110
+ var VERSION = true ? "0.1.2" : "0.0.0-dev";
111
111
  var DEFAULT_ORIGIN = "https://geo.new";
112
112
  var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
113
113
  var sleep = (ms, signal) => new Promise((resolve, reject) => {
@@ -306,7 +306,7 @@ var GeoNewClient = class {
306
306
  snapshot = await this.snapshot(id, { signal: options.signal });
307
307
  }
308
308
  }
309
- /** The keyless session token, when the client holds one; the CLI prints it on request, never by default. */
309
+ /** The last audit this machine started, so a bare `geo-new report` has something to read. */
310
310
  get lastAuditId() {
311
311
  return this.credentials.load().last_audit_id;
312
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geo-new",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Audit one page for GEO, SEO and agent readiness from a terminal, a coding agent (MCP) or a script. Keyless by default; a key is optional.",
5
5
  "license": "MIT",
6
6
  "type": "module",