@relvehq/agent-ready 0.3.1 → 0.4.1
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 +21 -1
- package/dist/core/report.d.ts +9 -3
- package/dist/core/report.js +30 -10
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/refine/model.d.ts +6 -0
- package/dist/refine/model.js +2 -0
- package/dist/refine/remote.d.ts +3 -2
- package/dist/refine/remote.js +3 -2
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +10 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-reporter.php +147 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +44 -24
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +6 -0
- package/dist/wp/relve-agent-ready/readme.txt +12 -2
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +3 -2
- package/package.json +2 -2
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
|
|
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`
|
package/dist/core/report.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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;
|
package/dist/core/report.js
CHANGED
|
@@ -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=
|
|
6
|
-
// AGENT_READY_ENDPOINT=https://.../hits
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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(
|
|
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
|
-
/**
|
|
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
|
|
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.1";
|
|
@@ -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.1";
|
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.
|
|
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; } });
|
package/dist/refine/model.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface ProviderOptions {
|
|
|
17
17
|
baseUrl?: string;
|
|
18
18
|
fetch?: typeof fetch;
|
|
19
19
|
timeoutMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Sampling temperature; left out of the request when unset (provider
|
|
22
|
+
* default). The refine API uses a low one so articles are copied, not
|
|
23
|
+
* reworded (changes/agent-ready-article-prompt).
|
|
24
|
+
*/
|
|
25
|
+
temperature?: number;
|
|
20
26
|
}
|
|
21
27
|
export declare const DEFAULT_CLAUDE_MODEL = "claude-opus-5-5";
|
|
22
28
|
export declare const DEFAULT_OPENAI_MODEL = "gpt-4o";
|
package/dist/refine/model.js
CHANGED
|
@@ -29,6 +29,7 @@ function anthropicModel(opts) {
|
|
|
29
29
|
}, {
|
|
30
30
|
model,
|
|
31
31
|
max_tokens: req.maxTokens ?? 8000,
|
|
32
|
+
...(opts.temperature === undefined ? {} : { temperature: opts.temperature }),
|
|
32
33
|
system: req.system,
|
|
33
34
|
messages: [{ role: "user", content: req.user }],
|
|
34
35
|
});
|
|
@@ -44,6 +45,7 @@ function openaiModel(opts) {
|
|
|
44
45
|
const json = await post(opts, `${base}/v1/chat/completions`, { authorization: `Bearer ${opts.apiKey}` }, {
|
|
45
46
|
model: opts.model,
|
|
46
47
|
max_completion_tokens: req.maxTokens ?? 8000,
|
|
48
|
+
...(opts.temperature === undefined ? {} : { temperature: opts.temperature }),
|
|
47
49
|
messages: [
|
|
48
50
|
{ role: "system", content: req.system },
|
|
49
51
|
{ role: "user", content: req.user },
|
package/dist/refine/remote.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
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.
|
|
5
|
+
export declare const USER_AGENT = "agent-ready/0.4.1 (+https://relvehq.com/agent-ready)";
|
|
5
6
|
export type RemoteStatus = "ok" | "unchanged" | "rejected" | "error" | "skipped";
|
|
6
7
|
export interface RemoteItem {
|
|
7
8
|
path: string;
|
package/dist/refine/remote.js
CHANGED
|
@@ -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
|
-
|
|
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/${
|
|
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
|
|
|
@@ -215,6 +215,14 @@ class Relve_AR_Router {
|
|
|
215
215
|
$source = 'native';
|
|
216
216
|
$store = new Relve_AR_Store();
|
|
217
217
|
$stored = $store->get( $d['path'], self::home_path() );
|
|
218
|
+
if ( null === $stored ) {
|
|
219
|
+
// Asked by "?p=12&format=md" (or ?post_type=...): the refined
|
|
220
|
+
// file is stored under the permalink's path, not the request's.
|
|
221
|
+
$canon_path = self::canonical_path( $target['canonical'] );
|
|
222
|
+
if ( null !== $canon_path && $canon_path !== $d['path'] ) {
|
|
223
|
+
$stored = $store->get( $canon_path, self::home_path() );
|
|
224
|
+
}
|
|
225
|
+
}
|
|
218
226
|
if ( null !== $stored ) {
|
|
219
227
|
$source = 'stored';
|
|
220
228
|
$body = Relve_AR_Markdown_Builder::for_stored( $stored, $target['canonical'], isset( $target['post'] ) ? $target['post'] : null, $target['title'] );
|
|
@@ -236,21 +244,32 @@ class Relve_AR_Router {
|
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
self::no_page_cache();
|
|
239
|
-
Relve_AR_Response::
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'source' => $source,
|
|
248
|
-
)
|
|
247
|
+
$response = Relve_AR_Response::build(
|
|
248
|
+
$body,
|
|
249
|
+
array(
|
|
250
|
+
'method' => $method,
|
|
251
|
+
'if_none_match' => $inm,
|
|
252
|
+
'canonical' => $target['canonical'],
|
|
253
|
+
'cache_control' => Relve_AR_Response::cache_control_for( $d['via'] ),
|
|
254
|
+
'source' => $source,
|
|
249
255
|
)
|
|
250
256
|
);
|
|
257
|
+
Relve_AR_Response::send( $response );
|
|
258
|
+
Relve_AR_Reporter::report( $settings, $d['path'], $d['via'], $source, $response, $body );
|
|
251
259
|
exit;
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Normalised path of a canonical URL, or null.
|
|
264
|
+
*
|
|
265
|
+
* @param string $canonical Canonical URL.
|
|
266
|
+
* @return string|null
|
|
267
|
+
*/
|
|
268
|
+
public static function canonical_path( $canonical ) {
|
|
269
|
+
$path = wp_parse_url( (string) $canonical, PHP_URL_PATH );
|
|
270
|
+
return is_string( $path ) && '' !== $path ? Relve_AR_Negotiation::normalize_path( $path ) : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
254
273
|
/**
|
|
255
274
|
* What the main query resolved to, if it is something we serve.
|
|
256
275
|
*
|
|
@@ -348,26 +367,27 @@ class Relve_AR_Router {
|
|
|
348
367
|
*
|
|
349
368
|
* @param array $settings Settings.
|
|
350
369
|
* @param string $method GET or HEAD.
|
|
370
|
+
* @param string $path Requested path, for the visit report.
|
|
351
371
|
* @return void
|
|
352
372
|
*/
|
|
353
|
-
private static function serve_llms( $settings, $method ) {
|
|
373
|
+
private static function serve_llms( $settings, $method, $path = '/llms.txt' ) {
|
|
354
374
|
$inm = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? wp_unslash( $_SERVER['HTTP_IF_NONE_MATCH'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- compared only.
|
|
355
375
|
$body = Relve_AR_Llms::generate( $settings );
|
|
356
376
|
self::no_page_cache();
|
|
357
|
-
Relve_AR_Response::
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
'source' => 'llms',
|
|
368
|
-
)
|
|
377
|
+
$response = Relve_AR_Response::build(
|
|
378
|
+
$body,
|
|
379
|
+
array(
|
|
380
|
+
'method' => $method,
|
|
381
|
+
'if_none_match' => $inm,
|
|
382
|
+
'canonical' => null, // llms.txt is meant to be found and read.
|
|
383
|
+
'noindex' => false,
|
|
384
|
+
'content_type' => Relve_AR_Response::TEXT_CONTENT_TYPE,
|
|
385
|
+
'cache_control' => Relve_AR_Response::cache_control_for( 'llms' ),
|
|
386
|
+
'source' => 'llms',
|
|
369
387
|
)
|
|
370
388
|
);
|
|
389
|
+
Relve_AR_Response::send( $response );
|
|
390
|
+
Relve_AR_Reporter::report( $settings, $path, 'llms', 'llms', $response, $body );
|
|
371
391
|
}
|
|
372
392
|
|
|
373
393
|
// ── 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.
|
|
7
|
+
Stable tag: 0.3.1
|
|
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,12 @@ Deactivating flushes rewrite rules, removes the WP Rocket rules and cancels sche
|
|
|
99
103
|
|
|
100
104
|
== Changelog ==
|
|
101
105
|
|
|
106
|
+
= 0.3.1 =
|
|
107
|
+
* A rewritten post is also served when asked by its ID (`?p=12&format=md`, `?post_type=news&p=12&format=md`), not only by its permalink.
|
|
108
|
+
|
|
109
|
+
= 0.3.0 =
|
|
110
|
+
* 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.
|
|
111
|
+
|
|
102
112
|
= 0.2.1 =
|
|
103
113
|
* 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
114
|
|
|
@@ -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.
|
|
6
|
+
* Version: 0.3.1
|
|
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.
|
|
22
|
+
define( 'RELVE_AR_VERSION', '0.3.1' );
|
|
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
|
+
"version": "0.4.1",
|
|
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": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"perplexity",
|
|
21
21
|
"seo"
|
|
22
22
|
],
|
|
23
|
-
"homepage": "https://relvehq.com/agent-
|
|
23
|
+
"homepage": "https://relvehq.com/agent-ready",
|
|
24
24
|
"author": "Relve (https://relvehq.com)",
|
|
25
25
|
"main": "./dist/index.js",
|
|
26
26
|
"types": "./dist/index.d.ts",
|