@relvehq/agent-ready 0.2.1 → 0.3.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.
@@ -6,6 +6,14 @@
6
6
  * post_types string[] default post, page
7
7
  * excluded_paths string one path prefix per line
8
8
  * llms_mode string auto|on|off, default auto
9
+ * openai_key string the site's own OpenAI key (stored here only, never
10
+ * shown in full; sent only to the Relve refine endpoint)
11
+ * refine_model string gpt-4o-mini (default), gpt-4o, gpt-4.1, gpt-4.1-mini
12
+ * refine_on_save bool default true (only acts once a key is saved)
13
+ *
14
+ * The settings form never posts `openai_key`; it posts `openai_key_new` (a
15
+ * replacement, empty = keep) and `openai_key_remove`. A submission without
16
+ * either keeps the saved key.
9
17
  *
10
18
  * @package Relve_Agent_Ready
11
19
  */
@@ -24,6 +32,12 @@ class Relve_AR_Settings {
24
32
  /** Paths never worth serving as Markdown (segment-aware prefixes). */
25
33
  const DEFAULT_EXCLUDED = "/wp-admin\n/wp-login.php\n/wp-json\n/wp-content\n/wp-includes\n/feed\n/cart\n/checkout\n/my-account\n/account";
26
34
 
35
+ /** Models the Relve refine endpoint accepts (contract.md). */
36
+ const MODELS = array( 'gpt-4o-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini' );
37
+
38
+ /** Default model: fast enough for the endpoint's ~30 s request limit. */
39
+ const DEFAULT_MODEL = 'gpt-4o-mini';
40
+
27
41
  /**
28
42
  * Defaults.
29
43
  *
@@ -35,9 +49,43 @@ class Relve_AR_Settings {
35
49
  'post_types' => array( 'post', 'page' ),
36
50
  'excluded_paths' => self::DEFAULT_EXCLUDED,
37
51
  'llms_mode' => 'auto',
52
+ 'openai_key' => '',
53
+ 'refine_model' => self::DEFAULT_MODEL,
54
+ 'refine_on_save' => true,
38
55
  );
39
56
  }
40
57
 
58
+ /**
59
+ * Masked key for display: "sk-...abcd". Never the full key.
60
+ *
61
+ * @param string $key Key.
62
+ * @return string
63
+ */
64
+ public static function mask_key( $key ) {
65
+ $key = (string) $key;
66
+ if ( '' === $key ) {
67
+ return '';
68
+ }
69
+ if ( strlen( $key ) < 12 ) {
70
+ return '****';
71
+ }
72
+ return substr( $key, 0, 3 ) . '...' . substr( $key, -4 );
73
+ }
74
+
75
+ /**
76
+ * A cleaned key, or null when it does not look like an OpenAI key.
77
+ *
78
+ * @param string $raw Raw input.
79
+ * @return string|null
80
+ */
81
+ public static function clean_key( $raw ) {
82
+ $key = preg_replace( '/\s+/', '', (string) $raw );
83
+ if ( null === $key || ! preg_match( '/^[A-Za-z0-9_\-]{20,300}$/', $key ) ) {
84
+ return null;
85
+ }
86
+ return $key;
87
+ }
88
+
41
89
  /**
42
90
  * Current settings merged over the defaults, plus "excluded" as a list.
43
91
  *
@@ -46,9 +94,12 @@ class Relve_AR_Settings {
46
94
  public static function get() {
47
95
  $stored = get_option( self::OPTION, array() );
48
96
  $s = array_merge( self::defaults(), is_array( $stored ) ? $stored : array() );
49
- $s['enabled'] = (bool) $s['enabled'];
50
- $s['post_types'] = array_values( array_filter( (array) $s['post_types'], 'is_string' ) );
51
- $s['excluded'] = self::lines( $s['excluded_paths'] );
97
+ $s['enabled'] = (bool) $s['enabled'];
98
+ $s['post_types'] = array_values( array_filter( (array) $s['post_types'], 'is_string' ) );
99
+ $s['excluded'] = self::lines( $s['excluded_paths'] );
100
+ $s['openai_key'] = is_string( $s['openai_key'] ) ? $s['openai_key'] : '';
101
+ $s['refine_model'] = in_array( $s['refine_model'], self::MODELS, true ) ? $s['refine_model'] : self::DEFAULT_MODEL;
102
+ $s['refine_on_save'] = (bool) $s['refine_on_save'];
52
103
  return $s;
53
104
  }
54
105
 
@@ -110,6 +161,41 @@ class Relve_AR_Settings {
110
161
 
111
162
  $mode = isset( $input['llms_mode'] ) ? (string) $input['llms_mode'] : 'auto';
112
163
  $out['llms_mode'] = in_array( $mode, array( 'auto', 'on', 'off' ), true ) ? $mode : 'auto';
164
+
165
+ // OpenAI key: replace, remove, or keep. WordPress may run this callback
166
+ // a second time on its own output (first save), which carries
167
+ // `openai_key` itself; that value is re-validated and kept.
168
+ $stored = get_option( self::OPTION, array() );
169
+ $current = ( is_array( $stored ) && isset( $stored['openai_key'] ) && is_string( $stored['openai_key'] ) ) ? $stored['openai_key'] : '';
170
+ $new = isset( $input['openai_key_new'] ) ? trim( (string) $input['openai_key_new'] ) : '';
171
+ $changed = false;
172
+ if ( '' !== $new ) {
173
+ $clean = self::clean_key( $new );
174
+ if ( null === $clean ) {
175
+ $out['openai_key'] = $current;
176
+ if ( function_exists( 'add_settings_error' ) ) {
177
+ add_settings_error( self::OPTION, 'relve_ar_bad_key', __( 'That does not look like an OpenAI API key. The saved key was kept.', 'relve-agent-ready' ) );
178
+ }
179
+ } else {
180
+ $out['openai_key'] = $clean;
181
+ $changed = $clean !== $current;
182
+ }
183
+ } elseif ( ! empty( $input['openai_key_remove'] ) ) {
184
+ $out['openai_key'] = '';
185
+ $changed = '' !== $current;
186
+ } elseif ( isset( $input['openai_key'] ) ) {
187
+ $clean = self::clean_key( (string) $input['openai_key'] );
188
+ $out['openai_key'] = null === $clean ? $current : $clean;
189
+ } else {
190
+ $out['openai_key'] = $current;
191
+ }
192
+ if ( $changed && class_exists( 'Relve_AR_Refiner' ) ) {
193
+ Relve_AR_Refiner::key_changed( '' !== $out['openai_key'] );
194
+ }
195
+
196
+ $model = isset( $input['refine_model'] ) ? (string) $input['refine_model'] : self::DEFAULT_MODEL;
197
+ $out['refine_model'] = in_array( $model, self::MODELS, true ) ? $model : self::DEFAULT_MODEL;
198
+ $out['refine_on_save'] = ! empty( $input['refine_on_save'] );
113
199
  return $out;
114
200
  }
115
201
  }
@@ -288,6 +288,46 @@ class Relve_AR_Store {
288
288
  return false !== file_put_contents( $file, $content ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
289
289
  }
290
290
 
291
+ /**
292
+ * Is there a stored file at this relative path?
293
+ *
294
+ * @param string $rel Relative path ("blog/my-post.md").
295
+ * @return bool
296
+ */
297
+ public function exists( $rel ) {
298
+ $check = self::validate_entry( 'md/' . $rel );
299
+ if ( 'store' !== $check['status'] ) {
300
+ return false;
301
+ }
302
+ $file = $this->md_dir() . '/' . $check['path'];
303
+ return is_file( $file ) && $this->inside( $file );
304
+ }
305
+
306
+ /**
307
+ * Delete one stored file.
308
+ *
309
+ * @param string $rel Relative path.
310
+ * @return bool Whether a file was deleted.
311
+ */
312
+ public function delete( $rel ) {
313
+ if ( ! $this->exists( $rel ) ) {
314
+ return false;
315
+ }
316
+ return unlink( $this->md_dir() . '/' . $rel ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
317
+ }
318
+
319
+ /**
320
+ * Relative file the router looks up first for a URL path (the full path,
321
+ * as uploads and the npm refine name them), or null for a bad path.
322
+ *
323
+ * @param string $path URL path ("/blog/my-post/").
324
+ * @return string|null
325
+ */
326
+ public static function rel_for_path( $path ) {
327
+ $list = self::candidates( $path );
328
+ return $list ? $list[0] : null;
329
+ }
330
+
291
331
  /**
292
332
  * Delete every stored file (keeps the folder and its protection files).
293
333
  *
@@ -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.1.0
7
+ Stable tag: 0.2.0
8
8
  License: MIT
9
9
  License URI: https://opensource.org/licenses/MIT
10
10
 
@@ -25,18 +25,33 @@ What it serves:
25
25
  * Posts and pages (published, public, not password-protected, of the post types you enable): YAML front matter (title, canonical, author, published, modified, categories, tags, description, source), one `# Title` heading, the featured image and the content converted to Markdown (headings, lists, links, images, tables, code blocks).
26
26
  * The posts front page and category, tag and author archives: a heading and a list of their latest 50 posts.
27
27
  * `llms.txt` under your home path: your site name, tagline and links to the `.md` version of up to 200 recent posts and pages.
28
- * Refined files: upload the zip Relve produces for your site and those hand-checked Markdown files are served first.
28
+ * Rewritten files: with your own OpenAI key, each post is rewritten into clean Markdown with Relve's prompt (see below) and served first.
29
+ * Refined files: upload a zip of Markdown files (a top-level `md/` folder) and those files are served first.
29
30
 
30
31
  Response headers: `Content-Type: text/markdown; charset=utf-8`, `Vary: Accept`, `Link: <permalink>; rel="canonical"`, `X-Robots-Tag: noindex` (search engines keep indexing your HTML page), `x-markdown-tokens`, `ETag` (304 on `If-None-Match`), `x-agent-ready-source: native | stored | llms`. Markdown asked for by `Accept` is sent with `Cache-Control: private, max-age=300`; `.md`, `?format=md` and llms.txt with `Cache-Control: public, s-maxage=300, stale-while-revalidate=86400`.
31
32
 
32
33
  The plugin is installed switched off. Nothing changes until you tick "Enabled" in Settings -> Agent Ready.
33
34
 
35
+ = Better Markdown with your own OpenAI key =
36
+
37
+ The Markdown built from WordPress is faithful but plain. For agent-ready pages that read well (a clear summary, clean headings, tables kept, theme clutter gone), save your OpenAI API key in Settings -> Agent Ready:
38
+
39
+ * When a post of an enabled type is published or updated, WP-Cron sends it about 30 seconds later (never while you wait on the editor) to Relve's refine service at `https://relvehq.com/api/agent-ready/v1/refine`: the permalink, and a small HTML page with the title, description, canonical link, structured data and the rendered content.
40
+ * Relve rewrites it with its own prompt, using your key for that single OpenAI call, checks the result and sends back the Markdown. The plugin stores it as that post's file in `wp-content/uploads/relve-agent-ready/md/`; it is served first (`x-agent-ready-source: stored`).
41
+ * A post that has not changed since its last rewrite is answered "unchanged" without a model call, so it costs nothing.
42
+ * "Rewrite all now" queues every published post of the enabled types; WP-Cron works through them 5 at a time. The settings page shows progress, posts rewritten, the last run and the last error code.
43
+ * If OpenAI rejects the key or the account is out of quota, rewriting pauses and an admin notice says so. Saving a new key resumes it.
44
+ * Unpublishing or deleting a post deletes its rewritten file. If a rewrite fails, the previous file (or the Markdown built from WordPress) keeps being served.
45
+ * Models: `gpt-4o-mini` (default, the fastest), `gpt-4o`, `gpt-4.1`, `gpt-4.1-mini`. Every call spends your own OpenAI key; no Relve account is needed.
46
+
47
+ The key is stored in your site's own options, shown masked (`sk-...abcd`) and only ever sent to the Relve endpoint over https, with redirects disabled. A developer can point the plugin at another endpoint with the `RELVE_AR_REFINE_ENDPOINT` constant or the `relve_ar_refine_endpoint` filter; anything other than https (or http on localhost, for testing) is refused and the key is not sent.
48
+
34
49
  == Installation ==
35
50
 
36
51
  1. Upload the `relve-agent-ready` zip in Plugins -> Add New -> Upload Plugin, then activate it.
37
52
  2. Go to Settings -> Agent Ready, tick "Enabled" and save.
38
53
  3. Optional: choose post types, add excluded paths, pick the llms.txt mode.
39
- 4. Optional: upload the refined-files zip Relve sent you.
54
+ 4. Optional: paste your OpenAI API key, save, then click "Rewrite all now".
40
55
  5. Run the three test commands shown in the Status panel.
41
56
 
42
57
  == Frequently Asked Questions ==
@@ -57,6 +72,18 @@ The plugin sets `DONOTCACHEPAGE` on every Markdown response and tells WP Rocket
57
72
 
58
73
  The description in the front matter uses your Yoast meta description when there is one. If Yoast's own llms.txt feature is on, the "auto" mode leaves llms.txt to Yoast; choose "On" to serve this plugin's version instead.
59
74
 
75
+ = Does Relve see my OpenAI key? =
76
+
77
+ 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
+
79
+ = What does rewriting cost? =
80
+
81
+ Your OpenAI usage for each rewrite (one model call per changed post, typically a few thousand tokens). Unchanged posts cost nothing.
82
+
83
+ = Why does the status say "rejected" or "too_little_text" for a post? =
84
+
85
+ `rejected` means the rewrite did not pass Relve's quality check twice; `too_little_text` means the post is too short to be worth rewriting. Nothing is stored for that post and the Markdown built from WordPress keeps being served.
86
+
60
87
  = What is in the refined-files zip? =
61
88
 
62
89
  A top-level `md/` folder with one `.md` file per URL path (`/` is `md/index.md`, `/blog/my-post/` is `md/blog/my-post.md`). Other files (such as `.json` sidecars) are skipped. Entries outside `md/`, absolute paths and paths containing `..` are rejected. Uploading replaces the stored set. Files are kept in `wp-content/uploads/relve-agent-ready/md/`, behind a deny-all `.htaccess`, and are only ever sent as text.
@@ -67,9 +94,15 @@ Drafts, private and password-protected posts, post types you did not enable, sea
67
94
 
68
95
  = What happens when I uninstall? =
69
96
 
70
- Deactivating flushes rewrite rules and removes the WP Rocket rules. Deleting the plugin also removes its settings and the refined-files folder.
97
+ Deactivating flushes rewrite rules, removes the WP Rocket rules and cancels scheduled rewrites. Deleting the plugin also removes its settings (including the OpenAI key), its rewrite status and post meta, and the refined-files folder.
71
98
 
72
99
  == Changelog ==
73
100
 
101
+ = 0.2.0 =
102
+ * Better Markdown with your own OpenAI key: posts are rewritten with Relve's prompt through Relve's refine API, in WP-Cron, on publish and update.
103
+ * "Rewrite all now" works through all posts 5 at a time, with progress on the settings page.
104
+ * Status: key saved, posts rewritten, last run, last error code. Rewriting pauses with an admin notice when OpenAI rejects the key or the quota runs out.
105
+ * Unpublished and deleted posts lose their rewritten file. "Clear refined files" also resets the rewrite fingerprints.
106
+
74
107
  = 0.1.0 =
75
108
  * First release: Markdown for posts, pages, the front page and archives via Accept, `.md` and `?format=md`; llms.txt; refined-files upload; WP Rocket and Yoast awareness; settings page with status and test commands.
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * Plugin Name: Relve Agent Ready
4
4
  * Plugin URI: https://relvehq.com/agent-md
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. Browsers are never affected. Off until enabled in Settings -> Agent Ready.
6
- * Version: 0.1.0
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.0
7
7
  * Requires at least: 6.0
8
8
  * Requires PHP: 7.4
9
9
  * Author: Relve
@@ -19,10 +19,17 @@ if ( ! defined( 'ABSPATH' ) ) {
19
19
  exit;
20
20
  }
21
21
 
22
- define( 'RELVE_AR_VERSION', '0.1.0' );
22
+ define( 'RELVE_AR_VERSION', '0.2.0' );
23
23
  define( 'RELVE_AR_FILE', __FILE__ );
24
24
  define( 'RELVE_AR_DIR', plugin_dir_path( __FILE__ ) );
25
25
 
26
+ // The Relve refine endpoint. Define it in wp-config.php to override (https
27
+ // only; http is accepted for localhost). Also filterable:
28
+ // relve_ar_refine_endpoint.
29
+ if ( ! defined( 'RELVE_AR_REFINE_ENDPOINT' ) ) {
30
+ define( 'RELVE_AR_REFINE_ENDPOINT', 'https://relvehq.com/api/agent-ready/v1/refine' );
31
+ }
32
+
26
33
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-settings.php';
27
34
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-negotiation.php';
28
35
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-converter.php';
@@ -31,6 +38,7 @@ require_once RELVE_AR_DIR . 'includes/class-relve-ar-response.php';
31
38
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-llms.php';
32
39
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-store.php';
33
40
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-router.php';
41
+ require_once RELVE_AR_DIR . 'includes/class-relve-ar-refiner.php';
34
42
 
35
43
  /**
36
44
  * Activation: default settings (disabled), the refined-files folder with its
@@ -48,12 +56,14 @@ function relve_ar_activate() {
48
56
  }
49
57
 
50
58
  /**
51
- * Deactivation: flush rewrite rules and drop our WP Rocket reject rules.
52
- * Settings and refined files are kept (uninstall removes them).
59
+ * Deactivation: flush rewrite rules, drop our WP Rocket reject rules and
60
+ * every scheduled rewrite. Settings and refined files are kept (uninstall
61
+ * removes them).
53
62
  *
54
63
  * @return void
55
64
  */
56
65
  function relve_ar_deactivate() {
66
+ Relve_AR_Refiner::unschedule_all();
57
67
  flush_rewrite_rules();
58
68
  remove_filter( 'rocket_cache_reject_uri', array( 'Relve_AR_Router', 'rocket_reject_uri' ) );
59
69
  Relve_AR_Router::refresh_rocket();
@@ -63,6 +73,7 @@ register_activation_hook( __FILE__, 'relve_ar_activate' );
63
73
  register_deactivation_hook( __FILE__, 'relve_ar_deactivate' );
64
74
 
65
75
  Relve_AR_Router::init();
76
+ Relve_AR_Refiner::init();
66
77
 
67
78
  if ( is_admin() ) {
68
79
  require_once RELVE_AR_DIR . 'includes/class-relve-ar-admin.php';
@@ -1,6 +1,8 @@
1
1
  <?php
2
2
  /**
3
- * Uninstall: remove the option and the refined-files folder.
3
+ * Uninstall: remove the options (settings, including the OpenAI key; refine
4
+ * state; refine queue), the refine post meta, any scheduled rewrites and the
5
+ * refined-files folder.
4
6
  *
5
7
  * @package Relve_Agent_Ready
6
8
  */
@@ -12,12 +14,21 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
12
14
  require_once __DIR__ . '/includes/class-relve-ar-negotiation.php';
13
15
  require_once __DIR__ . '/includes/class-relve-ar-store.php';
14
16
 
15
- delete_option( 'relve_agent_ready' );
17
+ foreach ( array( 'relve_agent_ready', 'relve_agent_ready_refine', 'relve_agent_ready_refine_queue' ) as $relve_ar_option ) {
18
+ delete_option( $relve_ar_option );
19
+ if ( is_multisite() ) {
20
+ delete_site_option( $relve_ar_option );
21
+ }
22
+ }
23
+ delete_transient( 'relve_ar_refine_lock' );
16
24
 
17
- if ( is_multisite() ) {
18
- delete_site_option( 'relve_agent_ready' );
25
+ foreach ( array( '_relve_ar_fingerprint', '_relve_ar_refined_at', '_relve_ar_refined_path' ) as $relve_ar_meta_key ) {
26
+ delete_post_meta_by_key( $relve_ar_meta_key );
19
27
  }
20
28
 
29
+ wp_unschedule_hook( 'relve_ar_refine_post' );
30
+ wp_unschedule_hook( 'relve_ar_refine_batch' );
31
+
21
32
  $relve_ar_store = new Relve_AR_Store();
22
33
  $relve_ar_store->delete_all();
23
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relvehq/agent-ready",
3
- "version": "0.2.1",
3
+ "version": "0.3.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": [