@relvehq/agent-ready 0.3.1 → 0.4.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.
package/README.md CHANGED
@@ -104,7 +104,7 @@ frameworks" below. For WordPress, the plugin ships inside this package
104
104
  | `enabled` | `AGENT_READY_ENABLED` is `1` or `true` |
105
105
  | `exclude` | `/api`, `/_next`, auth and admin paths, plus common private pages (account, checkout, cart, dashboard, settings, billing…) and yours; `excludeCommon: false` drops the common list |
106
106
  | `llms` | `/llms.txt` built from your `sitemap.xml`; `llms: false` for none |
107
- | `onHit` | sends each visit to a dashboard when `AGENT_READY_KEY` and `AGENT_READY_ENDPOINT` are set |
107
+ | `onHit` | sends each visit's stats to Relve when `AGENT_READY_KEY` is set (opt-in; `AGENT_READY_ENDPOINT` for another dashboard), see [Share visit stats](#share-visit-stats-with-relve-opt-in) |
108
108
 
109
109
  Anything you pass wins:
110
110
 
@@ -190,6 +190,9 @@ Markdown. The command saves `<out>/<path>.md` plus `<out>/<path>.json`
190
190
  page). Relve charges nothing.
191
191
  - **Your key:** used for that call only. Relve does not store or log it, and
192
192
  it is never written to the output files.
193
+ - **What Relve logs:** for each call, the page URL, the model, the outcome
194
+ and the size of the result (bytes, words, lines, tokens, headings, links,
195
+ tables). Not the HTML, not the Markdown, not your key.
193
196
  - **Re-run when content changes.** Each saved `.json` holds the page's
194
197
  fingerprint; it is sent along, and an unchanged page comes back as
195
198
  `unchanged` with no model call (free). `--force` rewrites everything.
@@ -288,6 +291,23 @@ writes a log that `npx agent-ready stats --log <file>` summarises. On
288
291
  serverless hosts (Vercel, Amplify) memory and local files don't last between
289
292
  requests: implement `HitStore` on your database.
290
293
 
294
+ ### Share visit stats with Relve (opt-in)
295
+
296
+ ```
297
+ AGENT_READY_KEY=ar_live_yoursite
298
+ ```
299
+
300
+ With that one variable set, `createAgentReady` reports each Markdown request
301
+ an agent makes to `https://relvehq.com/api/agent-ready/v1/hits`: your key, the
302
+ package version, the time, host, path, how the Markdown was asked for, where
303
+ it came from, status, tokens, bytes, the agent's user agent and the time
304
+ taken. Never the Markdown and never the visitor's IP. The report is
305
+ fire-and-forget with a 2 second timeout and never changes the agent's
306
+ response. The key is a label you choose (`ar_` plus 3 to 64 letters, digits,
307
+ `_` or `-`), not a secret. Without it nothing is sent.
308
+ `AGENT_READY_ENDPOINT` sends the same reports to another dashboard instead;
309
+ in code, `reporter({ key, endpoint? })` is the `onHit`.
310
+
291
311
  ## Other frameworks
292
312
 
293
313
  Express, Connect, Polka, Fastify (via `@fastify/middie`) or a plain `http`
@@ -1,9 +1,11 @@
1
1
  import type { HitEvent } from "./config";
2
+ /** Relve's ingest URL, used when AGENT_READY_ENDPOINT is not set. */
3
+ export declare const DEFAULT_HITS_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/hits";
2
4
  export interface ReporterOptions {
3
5
  /** The venture's dashboard key. */
4
6
  key: string;
5
- /** Ingest URL that receives { key, event }. */
6
- endpoint: string;
7
+ /** Ingest URL that receives { key, client, event }. Default: Relve's. */
8
+ endpoint?: string;
7
9
  /** Give up after this long. Default 2000 ms. */
8
10
  timeoutMs?: number;
9
11
  /** Injected for tests. */
@@ -11,5 +13,9 @@ export interface ReporterOptions {
11
13
  }
12
14
  /** An `onHit` that sends each event to the dashboard. */
13
15
  export declare function reporter(opts: ReporterOptions): (event: HitEvent) => Promise<void>;
14
- /** A reporter from env (AGENT_READY_KEY + AGENT_READY_ENDPOINT), or null when either is missing. */
16
+ /**
17
+ * A reporter from env, or null when AGENT_READY_KEY is missing.
18
+ * AGENT_READY_ENDPOINT is optional (Relve's by default); when set it must be
19
+ * http(s), else null.
20
+ */
15
21
  export declare function reporterFromEnv(env: Record<string, string | undefined>): ((event: HitEvent) => Promise<void>) | null;
@@ -2,28 +2,42 @@
2
2
  // Measure, the easy way: send every agent visit to a dashboard with one key
3
3
  // (changes/agent-ready-engine, "easy setup"). Replaces writing an `onHit`.
4
4
  //
5
- // AGENT_READY_KEY=ar_live_xxx (the venture's key)
6
- // AGENT_READY_ENDPOINT=https://.../hits (the dashboard's ingest URL)
5
+ // AGENT_READY_KEY=ar_live_yoursite (your key: ar_ + 3-64 of A-Z a-z 0-9 _ -)
6
+ // AGENT_READY_ENDPOINT=https://.../hits (optional: another dashboard)
7
7
  //
8
- // `reporter()` returns an `onHit` that POSTs { key, event } as JSON, with a
9
- // short timeout and keepalive. Fire-and-forget like every `onHit`: it never
10
- // throws and the response never waits for it.
8
+ // With only AGENT_READY_KEY set, visits go to Relve's dashboard
9
+ // (DEFAULT_HITS_ENDPOINT, changes/agent-ready-telemetry). Nothing is sent
10
+ // unless the key is set: reporting is opt-in.
11
+ //
12
+ // What one report holds: the key, the package version and the HitEvent
13
+ // (time, host, path, how the Markdown was asked for and where it came from,
14
+ // status, tokens, bytes, the visitor's user agent, duration). Never the
15
+ // Markdown, never the visitor's IP.
16
+ //
17
+ // `reporter()` returns an `onHit` that POSTs { key, client, event } as JSON,
18
+ // with a short timeout and keepalive. Fire-and-forget like every `onHit`: it
19
+ // never throws and the response never waits for it.
11
20
  //
12
21
  // EDGE-SAFE. No Node APIs.
13
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.DEFAULT_HITS_ENDPOINT = void 0;
14
24
  exports.reporter = reporter;
15
25
  exports.reporterFromEnv = reporterFromEnv;
26
+ const version_1 = require("./version");
27
+ /** Relve's ingest URL, used when AGENT_READY_ENDPOINT is not set. */
28
+ exports.DEFAULT_HITS_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/hits";
16
29
  /** An `onHit` that sends each event to the dashboard. */
17
30
  function reporter(opts) {
18
31
  const f = opts.fetch ?? fetch;
32
+ const endpoint = opts.endpoint ?? exports.DEFAULT_HITS_ENDPOINT;
19
33
  return async (event) => {
20
34
  const ctl = new AbortController();
21
35
  const timer = setTimeout(() => ctl.abort(), opts.timeoutMs ?? 2000);
22
36
  try {
23
- await f(opts.endpoint, {
37
+ await f(endpoint, {
24
38
  method: "POST",
25
39
  headers: { "content-type": "application/json", authorization: `Bearer ${opts.key}` },
26
- body: JSON.stringify({ key: opts.key, event }),
40
+ body: JSON.stringify({ key: opts.key, client: `npm/${version_1.PACKAGE_VERSION}`, event }),
27
41
  keepalive: true,
28
42
  signal: ctl.signal,
29
43
  });
@@ -36,11 +50,17 @@ function reporter(opts) {
36
50
  }
37
51
  };
38
52
  }
39
- /** A reporter from env (AGENT_READY_KEY + AGENT_READY_ENDPOINT), or null when either is missing. */
53
+ /**
54
+ * A reporter from env, or null when AGENT_READY_KEY is missing.
55
+ * AGENT_READY_ENDPOINT is optional (Relve's by default); when set it must be
56
+ * http(s), else null.
57
+ */
40
58
  function reporterFromEnv(env) {
41
59
  const key = env.AGENT_READY_KEY?.trim();
42
60
  const endpoint = env.AGENT_READY_ENDPOINT?.trim();
43
- if (!key || !endpoint || !/^https?:\/\//.test(endpoint))
61
+ if (!key)
62
+ return null;
63
+ if (endpoint && !/^https?:\/\//.test(endpoint))
44
64
  return null;
45
- return reporter({ key, endpoint });
65
+ return reporter({ key, endpoint: endpoint || undefined });
46
66
  }
@@ -0,0 +1 @@
1
+ export declare const PACKAGE_VERSION = "0.4.0";
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ // The package version, in one place: the refine user agent and the visit
3
+ // reporter both send it, so Relve can tell which release a venture runs
4
+ // (changes/agent-ready-telemetry). Keep in step with package.json.
5
+ // EDGE-SAFE.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.PACKAGE_VERSION = void 0;
8
+ exports.PACKAGE_VERSION = "0.4.0";
package/dist/index.d.ts CHANGED
@@ -6,7 +6,8 @@ export { buildLlmsTxt, type BuildLlmsOptions } from "./core/llms";
6
6
  export { byteLength, etagFor, ifNoneMatchHits, markdownResponse, MARKDOWN_CONTENT_TYPE, type RespondOptions } from "./core/respond";
7
7
  export { COMMON_PRIVATE_PATHS, createAgentReady, enabledFromEnv, siteFromEnv, type EasyOptions, type Env } from "./core/easy";
8
8
  export { entriesFromUrls, humanize, parseSitemap, sitemapEntries, type ParsedSitemap, type SitemapEntriesOptions } from "./core/sitemap";
9
- export { reporter, reporterFromEnv, type ReporterOptions } from "./core/report";
9
+ export { DEFAULT_HITS_ENDPOINT, reporter, reporterFromEnv, type ReporterOptions } from "./core/report";
10
+ export { PACKAGE_VERSION } from "./core/version";
10
11
  export { ANSWER_BOT_TOKENS, classifyAgent, isAnswerBot, type Bot, type BotKind } from "./proof/bots";
11
12
  export { filterHits, memoryHitStore, teeHits, type HitQuery, type HitStore } from "./proof/hits";
12
13
  export { formatSummary, summarize, type Count, type HitSummary } from "./proof/stats";
package/dist/index.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // @relvehq/agent-ready/next/route Next.js route handler
9
9
  // and the Next.js middleware adapter lives at @relvehq/agent-ready/next.
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.formatAudit = exports.blockedBots = exports.auditSite = exports.renderDashboard = exports.summarize = exports.formatSummary = exports.teeHits = exports.memoryHitStore = exports.filterHits = exports.isAnswerBot = exports.classifyAgent = exports.ANSWER_BOT_TOKENS = exports.reporterFromEnv = exports.reporter = exports.sitemapEntries = exports.parseSitemap = exports.humanize = exports.entriesFromUrls = exports.siteFromEnv = exports.enabledFromEnv = exports.createAgentReady = exports.COMMON_PRIVATE_PATHS = exports.MARKDOWN_CONTENT_TYPE = exports.markdownResponse = exports.ifNoneMatchHits = exports.etagFor = exports.byteLength = exports.buildLlmsTxt = exports.jsonLdFacts = exports.hasFrontMatter = exports.frontMatter = exports.estimateTokens = exports.enrich = exports.wantsMarkdown = exports.stripMdSuffix = exports.normalizePath = exports.mdUrlPath = exports.isExcluded = exports.isEnabled = exports.emitHit = exports.defineAgentReady = exports.VIA_HEADER = exports.DEFAULT_NEGOTIATED_CACHE_CONTROL = exports.DEFAULT_INTERNAL_PREFIX = exports.DEFAULT_INTERNAL_HEADER = exports.DEFAULT_CACHE_CONTROL = exports.ALWAYS_EXCLUDE = exports.parseAccept = exports.chooseFormat = void 0;
11
+ exports.blockedBots = exports.auditSite = exports.renderDashboard = exports.summarize = exports.formatSummary = exports.teeHits = exports.memoryHitStore = exports.filterHits = exports.isAnswerBot = exports.classifyAgent = exports.ANSWER_BOT_TOKENS = exports.PACKAGE_VERSION = exports.reporterFromEnv = exports.reporter = exports.DEFAULT_HITS_ENDPOINT = exports.sitemapEntries = exports.parseSitemap = exports.humanize = exports.entriesFromUrls = exports.siteFromEnv = exports.enabledFromEnv = exports.createAgentReady = exports.COMMON_PRIVATE_PATHS = exports.MARKDOWN_CONTENT_TYPE = exports.markdownResponse = exports.ifNoneMatchHits = exports.etagFor = exports.byteLength = exports.buildLlmsTxt = exports.jsonLdFacts = exports.hasFrontMatter = exports.frontMatter = exports.estimateTokens = exports.enrich = exports.wantsMarkdown = exports.stripMdSuffix = exports.normalizePath = exports.mdUrlPath = exports.isExcluded = exports.isEnabled = exports.emitHit = exports.defineAgentReady = exports.VIA_HEADER = exports.DEFAULT_NEGOTIATED_CACHE_CONTROL = exports.DEFAULT_INTERNAL_PREFIX = exports.DEFAULT_INTERNAL_HEADER = exports.DEFAULT_CACHE_CONTROL = exports.ALWAYS_EXCLUDE = exports.parseAccept = exports.chooseFormat = void 0;
12
+ exports.formatAudit = void 0;
12
13
  var accept_1 = require("./core/accept");
13
14
  Object.defineProperty(exports, "chooseFormat", { enumerable: true, get: function () { return accept_1.chooseFormat; } });
14
15
  Object.defineProperty(exports, "parseAccept", { enumerable: true, get: function () { return accept_1.parseAccept; } });
@@ -54,8 +55,11 @@ Object.defineProperty(exports, "humanize", { enumerable: true, get: function ()
54
55
  Object.defineProperty(exports, "parseSitemap", { enumerable: true, get: function () { return sitemap_1.parseSitemap; } });
55
56
  Object.defineProperty(exports, "sitemapEntries", { enumerable: true, get: function () { return sitemap_1.sitemapEntries; } });
56
57
  var report_1 = require("./core/report");
58
+ Object.defineProperty(exports, "DEFAULT_HITS_ENDPOINT", { enumerable: true, get: function () { return report_1.DEFAULT_HITS_ENDPOINT; } });
57
59
  Object.defineProperty(exports, "reporter", { enumerable: true, get: function () { return report_1.reporter; } });
58
60
  Object.defineProperty(exports, "reporterFromEnv", { enumerable: true, get: function () { return report_1.reporterFromEnv; } });
61
+ var version_1 = require("./core/version");
62
+ Object.defineProperty(exports, "PACKAGE_VERSION", { enumerable: true, get: function () { return version_1.PACKAGE_VERSION; } });
59
63
  // Proof: who reads, how much, and how agent-ready a site is.
60
64
  var bots_1 = require("./proof/bots");
61
65
  Object.defineProperty(exports, "ANSWER_BOT_TOKENS", { enumerable: true, get: function () { return bots_1.ANSWER_BOT_TOKENS; } });
@@ -1,7 +1,8 @@
1
- export declare const PACKAGE_VERSION = "0.3.0";
1
+ import { PACKAGE_VERSION } from "../core/version";
2
+ export { PACKAGE_VERSION };
2
3
  export declare const DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
3
4
  export declare const REFINE_MODELS: readonly ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
4
- export declare const USER_AGENT = "agent-ready/0.3.0 (+https://relvehq.com/agent-ready)";
5
+ export declare const USER_AGENT = "agent-ready/0.4.0 (+https://relvehq.com/agent-ready)";
5
6
  export type RemoteStatus = "ok" | "unchanged" | "rejected" | "error" | "skipped";
6
7
  export interface RemoteItem {
7
8
  path: string;
@@ -52,10 +52,11 @@ exports.savedFingerprint = savedFingerprint;
52
52
  exports.refineRemote = refineRemote;
53
53
  exports.stopMessage = stopMessage;
54
54
  const file_store_1 = require("./file-store");
55
- exports.PACKAGE_VERSION = "0.3.0";
55
+ const version_1 = require("../core/version");
56
+ Object.defineProperty(exports, "PACKAGE_VERSION", { enumerable: true, get: function () { return version_1.PACKAGE_VERSION; } });
56
57
  exports.DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
57
58
  exports.REFINE_MODELS = ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
58
- exports.USER_AGENT = `agent-ready/${exports.PACKAGE_VERSION} (+https://relvehq.com/agent-ready)`;
59
+ exports.USER_AGENT = `agent-ready/${version_1.PACKAGE_VERSION} (+https://relvehq.com/agent-ready)`;
59
60
  /** Errors that mean every later call would fail the same way: the run stops. */
60
61
  const FATAL = new Set(["openai_auth", "openai_quota", "missing_key"]);
61
62
  /** The saved JSON's fingerprint, else one built from a local run's sourceHash, else null. */
@@ -318,6 +318,16 @@ class Relve_AR_Admin {
318
318
  </td>
319
319
  </tr>
320
320
  <?php self::render_refine_fields( $s ); ?>
321
+ <tr>
322
+ <th scope="row"><?php esc_html_e( 'Visit stats', 'relve-agent-ready' ); ?></th>
323
+ <td>
324
+ <label>
325
+ <input type="checkbox" name="<?php echo esc_attr( $opt ); ?>[share_stats]" value="1" <?php checked( $s['share_stats'] ); ?> />
326
+ <?php esc_html_e( 'Share visit stats with Relve', 'relve-agent-ready' ); ?>
327
+ </label>
328
+ <p class="description"><?php esc_html_e( 'For each Markdown page or llms.txt an agent reads: the path, how it was asked for, the agent\'s user agent, status, tokens, bytes and time taken. Never the Markdown itself and never the visitor\'s IP address. Off by default.', 'relve-agent-ready' ); ?></p>
329
+ </td>
330
+ </tr>
321
331
  </table>
322
332
  <?php submit_button(); ?>
323
333
  </form>
@@ -0,0 +1,147 @@
1
+ <?php
2
+ /**
3
+ * Reporter: share agent visits with Relve (changes/agent-ready-telemetry).
4
+ *
5
+ * Off unless both "Enabled" and "Share visit stats with Relve" are ticked.
6
+ * After a Markdown page or llms.txt has been sent, one small JSON report goes
7
+ * to RELVE_AR_HITS_ENDPOINT:
8
+ *
9
+ * { key: "ar_wp_<12 hex of md5(home_url())>", client: "wordpress/<ver>",
10
+ * event: { ts, host, path, via, source, status, tokens, bytes,
11
+ * userAgent, durationMs } }
12
+ *
13
+ * Never the Markdown, never the visitor's IP. The request is non-blocking
14
+ * with a 1 second timeout, and where PHP-FPM allows it the agent's response
15
+ * is finished first, so the agent never waits on Relve.
16
+ *
17
+ * @package Relve_Agent_Ready
18
+ */
19
+
20
+ if ( ! defined( 'ABSPATH' ) ) {
21
+ exit;
22
+ }
23
+
24
+ /**
25
+ * Visit reports to Relve.
26
+ */
27
+ class Relve_AR_Reporter {
28
+
29
+ const DEFAULT_ENDPOINT = 'https://relvehq.com/api/agent-ready/v1/hits';
30
+
31
+ /**
32
+ * Endpoint URL: RELVE_AR_HITS_ENDPOINT, filterable (relve_ar_hits_endpoint).
33
+ *
34
+ * @return string
35
+ */
36
+ public static function endpoint() {
37
+ $url = defined( 'RELVE_AR_HITS_ENDPOINT' ) ? (string) RELVE_AR_HITS_ENDPOINT : self::DEFAULT_ENDPOINT;
38
+ return (string) apply_filters( 'relve_ar_hits_endpoint', $url );
39
+ }
40
+
41
+ /**
42
+ * Whether this site reports at all.
43
+ *
44
+ * @param array $settings Settings.
45
+ * @return bool
46
+ */
47
+ public static function active( $settings ) {
48
+ return ! empty( $settings['enabled'] ) && ! empty( $settings['share_stats'] );
49
+ }
50
+
51
+ /**
52
+ * The site's dashboard key: stable per home URL, not a secret.
53
+ *
54
+ * @return string
55
+ */
56
+ public static function site_key() {
57
+ return 'ar_wp_' . substr( md5( (string) home_url() ), 0, 12 );
58
+ }
59
+
60
+ /**
61
+ * One visit, in the package's HitEvent shape.
62
+ *
63
+ * @param string $path Requested page path (without ".md"; "via" says how it was asked).
64
+ * @param string $via suffix|query|accept|llms.
65
+ * @param string $source native|stored|llms.
66
+ * @param array $response Relve_AR_Response::build() result.
67
+ * @param string $body The full Markdown (only its size is used).
68
+ * @param array $server $_SERVER.
69
+ * @return array
70
+ */
71
+ public static function event( $path, $via, $source, $response, $body, $server ) {
72
+ $headers = isset( $response['headers'] ) && is_array( $response['headers'] ) ? $response['headers'] : array();
73
+ $start = isset( $server['REQUEST_TIME_FLOAT'] ) ? (float) $server['REQUEST_TIME_FLOAT'] : 0.0;
74
+ $ua = isset( $server['HTTP_USER_AGENT'] ) ? substr( (string) $server['HTTP_USER_AGENT'], 0, 512 ) : '';
75
+ return array(
76
+ 'ts' => gmdate( 'Y-m-d\TH:i:s\Z' ),
77
+ 'host' => (string) wp_parse_url( home_url(), PHP_URL_HOST ),
78
+ 'path' => substr( (string) $path, 0, 2048 ),
79
+ 'via' => (string) $via,
80
+ 'source' => (string) $source,
81
+ 'status' => isset( $response['status'] ) ? (int) $response['status'] : 200,
82
+ 'tokens' => isset( $headers['x-markdown-tokens'] ) ? (int) $headers['x-markdown-tokens'] : Relve_AR_Response::estimate_tokens( (string) $body ),
83
+ 'bytes' => strlen( (string) $body ),
84
+ 'userAgent' => $ua,
85
+ 'durationMs' => $start > 0 ? max( 0, (int) round( ( microtime( true ) - $start ) * 1000 ) ) : null,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * wp_remote_post() arguments for one event.
91
+ *
92
+ * @param array $event Event.
93
+ * @return array
94
+ */
95
+ public static function request_args( $event ) {
96
+ $version = defined( 'RELVE_AR_VERSION' ) ? RELVE_AR_VERSION : '0';
97
+ $body = json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- flags wp_json_encode does not add.
98
+ array(
99
+ 'key' => self::site_key(),
100
+ 'client' => 'wordpress/' . $version,
101
+ 'event' => $event,
102
+ ),
103
+ JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE
104
+ );
105
+ $ua = 'relve-agent-ready-wp/' . $version;
106
+ return array(
107
+ 'method' => 'POST',
108
+ 'timeout' => 1,
109
+ 'blocking' => false,
110
+ 'redirection' => 0,
111
+ 'user-agent' => $ua,
112
+ 'headers' => array(
113
+ 'Content-Type' => 'application/json',
114
+ 'User-Agent' => $ua,
115
+ ),
116
+ 'body' => (string) $body,
117
+ 'data_format' => 'body',
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Report one served response. Call after Relve_AR_Response::send(); never
123
+ * throws.
124
+ *
125
+ * @param array $settings Settings.
126
+ * @param string $path Requested path.
127
+ * @param string $via How the Markdown was asked for.
128
+ * @param string $source Where it came from.
129
+ * @param array $response Relve_AR_Response::build() result.
130
+ * @param string $body The full Markdown.
131
+ * @return void
132
+ */
133
+ public static function report( $settings, $path, $via, $source, $response, $body ) {
134
+ if ( ! self::active( $settings ) ) {
135
+ return;
136
+ }
137
+ try {
138
+ $event = self::event( $path, $via, $source, $response, $body, $_SERVER );
139
+ if ( function_exists( 'fastcgi_finish_request' ) ) {
140
+ fastcgi_finish_request(); // The agent has its answer; report after.
141
+ }
142
+ wp_remote_post( self::endpoint(), self::request_args( $event ) );
143
+ } catch ( Throwable $e ) {
144
+ // A Relve outage must never touch the site.
145
+ }
146
+ }
147
+ }
@@ -144,7 +144,7 @@ class Relve_AR_Router {
144
144
  }
145
145
 
146
146
  if ( $d['llms'] ) {
147
- self::serve_llms( $settings, $method );
147
+ self::serve_llms( $settings, $method, (string) $d['path'] );
148
148
  exit;
149
149
  }
150
150
 
@@ -236,18 +236,18 @@ class Relve_AR_Router {
236
236
  }
237
237
 
238
238
  self::no_page_cache();
239
- Relve_AR_Response::send(
240
- Relve_AR_Response::build(
241
- $body,
242
- array(
243
- 'method' => $method,
244
- 'if_none_match' => $inm,
245
- 'canonical' => $target['canonical'],
246
- 'cache_control' => Relve_AR_Response::cache_control_for( $d['via'] ),
247
- 'source' => $source,
248
- )
239
+ $response = Relve_AR_Response::build(
240
+ $body,
241
+ array(
242
+ 'method' => $method,
243
+ 'if_none_match' => $inm,
244
+ 'canonical' => $target['canonical'],
245
+ 'cache_control' => Relve_AR_Response::cache_control_for( $d['via'] ),
246
+ 'source' => $source,
249
247
  )
250
248
  );
249
+ Relve_AR_Response::send( $response );
250
+ Relve_AR_Reporter::report( $settings, $d['path'], $d['via'], $source, $response, $body );
251
251
  exit;
252
252
  }
253
253
 
@@ -348,26 +348,27 @@ class Relve_AR_Router {
348
348
  *
349
349
  * @param array $settings Settings.
350
350
  * @param string $method GET or HEAD.
351
+ * @param string $path Requested path, for the visit report.
351
352
  * @return void
352
353
  */
353
- private static function serve_llms( $settings, $method ) {
354
+ private static function serve_llms( $settings, $method, $path = '/llms.txt' ) {
354
355
  $inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
355
356
  $body = Relve_AR_Llms::generate( $settings );
356
357
  self::no_page_cache();
357
- Relve_AR_Response::send(
358
- Relve_AR_Response::build(
359
- $body,
360
- array(
361
- 'method' => $method,
362
- 'if_none_match' => $inm,
363
- 'canonical' => null, // llms.txt is meant to be found and read.
364
- 'noindex' => false,
365
- 'content_type' => Relve_AR_Response::TEXT_CONTENT_TYPE,
366
- 'cache_control' => Relve_AR_Response::cache_control_for( 'llms' ),
367
- 'source' => 'llms',
368
- )
358
+ $response = Relve_AR_Response::build(
359
+ $body,
360
+ array(
361
+ 'method' => $method,
362
+ 'if_none_match' => $inm,
363
+ 'canonical' => null, // llms.txt is meant to be found and read.
364
+ 'noindex' => false,
365
+ 'content_type' => Relve_AR_Response::TEXT_CONTENT_TYPE,
366
+ 'cache_control' => Relve_AR_Response::cache_control_for( 'llms' ),
367
+ 'source' => 'llms',
369
368
  )
370
369
  );
370
+ Relve_AR_Response::send( $response );
371
+ Relve_AR_Reporter::report( $settings, $path, 'llms', 'llms', $response, $body );
371
372
  }
372
373
 
373
374
  // ── caches ────────────────────────────────────────────────────────────
@@ -10,6 +10,9 @@
10
10
  * shown in full; sent only to the Relve refine endpoint)
11
11
  * refine_model string gpt-4o (default), gpt-4.1, gpt-4.1-mini, gpt-4o-mini
12
12
  * refine_on_save bool default true (only acts once a key is saved)
13
+ * share_stats bool default false: send each agent visit's stats
14
+ * (path, bot, tokens, bytes; never the Markdown or
15
+ * the visitor's IP) to Relve (Relve_AR_Reporter)
13
16
  *
14
17
  * The settings form never posts `openai_key`; it posts `openai_key_new` (a
15
18
  * replacement, empty = keep) and `openai_key_remove`. A submission without
@@ -55,6 +58,7 @@ class Relve_AR_Settings {
55
58
  'openai_key' => '',
56
59
  'refine_model' => self::DEFAULT_MODEL,
57
60
  'refine_on_save' => true,
61
+ 'share_stats' => false,
58
62
  );
59
63
  }
60
64
 
@@ -103,6 +107,7 @@ class Relve_AR_Settings {
103
107
  $s['openai_key'] = is_string( $s['openai_key'] ) ? $s['openai_key'] : '';
104
108
  $s['refine_model'] = in_array( $s['refine_model'], self::MODELS, true ) ? $s['refine_model'] : self::DEFAULT_MODEL;
105
109
  $s['refine_on_save'] = (bool) $s['refine_on_save'];
110
+ $s['share_stats'] = (bool) $s['share_stats'];
106
111
  return $s;
107
112
  }
108
113
 
@@ -199,6 +204,7 @@ class Relve_AR_Settings {
199
204
  $model = isset( $input['refine_model'] ) ? (string) $input['refine_model'] : self::DEFAULT_MODEL;
200
205
  $out['refine_model'] = in_array( $model, self::MODELS, true ) ? $model : self::DEFAULT_MODEL;
201
206
  $out['refine_on_save'] = ! empty( $input['refine_on_save'] );
207
+ $out['share_stats'] = ! empty( $input['share_stats'] );
202
208
  return $out;
203
209
  }
204
210
  }
@@ -4,7 +4,7 @@ Tags: markdown, llms.txt, ai, agents, seo
4
4
  Requires at least: 6.0
5
5
  Tested up to: 6.7
6
6
  Requires PHP: 7.4
7
- Stable tag: 0.2.1
7
+ Stable tag: 0.3.0
8
8
  License: MIT
9
9
  License URI: https://opensource.org/licenses/MIT
10
10
 
@@ -75,7 +75,11 @@ The description in the front matter uses your Yoast meta description when there
75
75
 
76
76
  = Does Relve see my OpenAI key? =
77
77
 
78
- It passes through Relve's server for the single call that rewrites a post: the plugin sends it in a request header over https, Relve uses it for that one OpenAI call, and does not store or log it. It is never returned and never written to a database, file or cache on Relve's side. On your site it is kept in the plugin's option in your own database, like other plugin API keys, and is never shown in full on the settings page. Relve also receives the post's public permalink and content, which are public anyway. Remove the key at any time with "Remove the saved key"; deleting the plugin deletes it too.
78
+ It passes through Relve's server for the single call that rewrites a post: the plugin sends it in a request header over https, Relve uses it for that one OpenAI call, and does not store or log it. It is never returned and never written to a database, file or cache on Relve's side. On your site it is kept in the plugin's option in your own database, like other plugin API keys, and is never shown in full on the settings page. Relve also receives the post's public permalink and content, which are public anyway. Relve keeps a usage record of each rewrite (the permalink, the model, the outcome and size counts of the result such as bytes, words, tokens and headings), not the content or the Markdown. Remove the key at any time with "Remove the saved key"; deleting the plugin deletes it too.
79
+
80
+ = What does "Share visit stats with Relve" send? =
81
+
82
+ Nothing unless you tick it (and "Enabled"). Then, after each Markdown page or llms.txt is sent to an agent, one small request goes to `https://relvehq.com/api/agent-ready/v1/hits` with: a site key derived from your home URL, the plugin version, the time, your host, the page path, how the Markdown was asked for (`.md`, `?format=md`, Accept or llms.txt), where it came from (built, stored or llms), the status, tokens, bytes, the agent's user agent and the time taken. Never the Markdown and never the visitor's IP address. It is sent without waiting for an answer and never delays or changes the agent's response. Override the endpoint with the `RELVE_AR_HITS_ENDPOINT` constant or the `relve_ar_hits_endpoint` filter.
79
83
 
80
84
  = What does rewriting cost? =
81
85
 
@@ -99,6 +103,9 @@ Deactivating flushes rewrite rules, removes the WP Rocket rules and cancels sche
99
103
 
100
104
  == Changelog ==
101
105
 
106
+ = 0.3.0 =
107
+ * New, off by default: "Share visit stats with Relve". For each Markdown page or llms.txt an agent reads, the plugin sends Relve the path, how it was asked for, the agent's user agent, status, tokens, bytes and time taken, after the agent has its answer. Never the Markdown, never the visitor's IP.
108
+
102
109
  = 0.2.1 =
103
110
  * Default model is now gpt-4o: in testing, gpt-4o-mini sometimes gave the prices of example cards on a page to the page itself. gpt-4o-mini stays available.
104
111
 
@@ -3,7 +3,7 @@
3
3
  * Plugin Name: Relve Agent Ready
4
4
  * Plugin URI: https://relvehq.com/agent-md
5
5
  * Description: Serves a Markdown version of posts, pages and archives to AI agents (Accept: text/markdown, .md URLs, ?format=md) plus an llms.txt. Optionally rewrites posts into better Markdown with Relve's prompt and your own OpenAI key. Browsers are never affected. Off until enabled in Settings -> Agent Ready.
6
- * Version: 0.2.1
6
+ * Version: 0.3.0
7
7
  * Requires at least: 6.0
8
8
  * Requires PHP: 7.4
9
9
  * Author: Relve
@@ -19,7 +19,7 @@ if ( ! defined( 'ABSPATH' ) ) {
19
19
  exit;
20
20
  }
21
21
 
22
- define( 'RELVE_AR_VERSION', '0.2.1' );
22
+ define( 'RELVE_AR_VERSION', '0.3.0' );
23
23
  define( 'RELVE_AR_FILE', __FILE__ );
24
24
  define( 'RELVE_AR_DIR', plugin_dir_path( __FILE__ ) );
25
25
 
@@ -39,6 +39,7 @@ require_once RELVE_AR_DIR . 'includes/class-relve-ar-llms.php';
39
39
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-store.php';
40
40
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-router.php';
41
41
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-refiner.php';
42
+ require_once RELVE_AR_DIR . 'includes/class-relve-ar-reporter.php';
42
43
 
43
44
  /**
44
45
  * Activation: default settings (disabled), the refined-files folder with its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relvehq/agent-ready",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "description": "Make your site agent-ready: serve every page as clean Markdown to AI agents (Accept: text/markdown, .md URLs, llms.txt), refine it with AI ahead of time, and prove it with a free checker and a visits dashboard. Next.js adapter included.",
6
6
  "keywords": [