@relvehq/agent-ready 0.0.0-stage → 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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -3
  3. package/SETUP.md +354 -0
  4. package/dist/bin/agent-ready.d.ts +23 -0
  5. package/dist/bin/agent-ready.js +444 -0
  6. package/dist/cli/init.d.ts +70 -0
  7. package/dist/cli/init.js +366 -0
  8. package/dist/core/accept.d.ts +8 -0
  9. package/dist/core/accept.js +76 -0
  10. package/dist/core/config.d.ts +145 -0
  11. package/dist/core/config.js +103 -0
  12. package/dist/core/convert.d.ts +22 -0
  13. package/dist/core/convert.js +334 -0
  14. package/dist/core/detect.d.ts +30 -0
  15. package/dist/core/detect.js +82 -0
  16. package/dist/core/easy.d.ts +19 -0
  17. package/dist/core/easy.js +117 -0
  18. package/dist/core/enrich.d.ts +30 -0
  19. package/dist/core/enrich.js +155 -0
  20. package/dist/core/llms.d.ts +11 -0
  21. package/dist/core/llms.js +71 -0
  22. package/dist/core/report.d.ts +15 -0
  23. package/dist/core/report.js +46 -0
  24. package/dist/core/resolve.d.ts +43 -0
  25. package/dist/core/resolve.js +204 -0
  26. package/dist/core/respond.d.ts +18 -0
  27. package/dist/core/respond.js +59 -0
  28. package/dist/core/sitemap.d.ts +31 -0
  29. package/dist/core/sitemap.js +140 -0
  30. package/dist/index.d.ts +14 -0
  31. package/dist/index.js +76 -0
  32. package/dist/next/dashboard.d.ts +14 -0
  33. package/dist/next/dashboard.js +56 -0
  34. package/dist/next/index.d.ts +42 -0
  35. package/dist/next/index.js +100 -0
  36. package/dist/next/route.d.ts +26 -0
  37. package/dist/next/route.js +167 -0
  38. package/dist/node/index.d.ts +8 -0
  39. package/dist/node/index.js +191 -0
  40. package/dist/proof/audit.d.ts +47 -0
  41. package/dist/proof/audit.js +253 -0
  42. package/dist/proof/bots.d.ts +24 -0
  43. package/dist/proof/bots.js +60 -0
  44. package/dist/proof/dashboard.d.ts +8 -0
  45. package/dist/proof/dashboard.js +77 -0
  46. package/dist/proof/hits.d.ts +18 -0
  47. package/dist/proof/hits.js +51 -0
  48. package/dist/proof/jsonl.d.ts +5 -0
  49. package/dist/proof/jsonl.js +80 -0
  50. package/dist/proof/stats.d.ts +39 -0
  51. package/dist/proof/stats.js +127 -0
  52. package/dist/refine/check.d.ts +17 -0
  53. package/dist/refine/check.js +134 -0
  54. package/dist/refine/extract.d.ts +7 -0
  55. package/dist/refine/extract.js +120 -0
  56. package/dist/refine/file-store.d.ts +6 -0
  57. package/dist/refine/file-store.js +87 -0
  58. package/dist/refine/model.d.ts +30 -0
  59. package/dist/refine/model.js +105 -0
  60. package/dist/refine/prompts.d.ts +49 -0
  61. package/dist/refine/prompts.js +178 -0
  62. package/dist/refine/refine.d.ts +38 -0
  63. package/dist/refine/refine.js +75 -0
  64. package/dist/refine/remote.d.ts +57 -0
  65. package/dist/refine/remote.js +238 -0
  66. package/dist/refine/run.d.ts +52 -0
  67. package/dist/refine/run.js +66 -0
  68. package/dist/refine/types.d.ts +17 -0
  69. package/dist/refine/types.js +8 -0
  70. package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +550 -0
  71. package/dist/wp/relve-agent-ready/includes/class-relve-ar-converter.php +762 -0
  72. package/dist/wp/relve-agent-ready/includes/class-relve-ar-llms.php +192 -0
  73. package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +455 -0
  74. package/dist/wp/relve-agent-ready/includes/class-relve-ar-negotiation.php +334 -0
  75. package/dist/wp/relve-agent-ready/includes/class-relve-ar-refiner.php +894 -0
  76. package/dist/wp/relve-agent-ready/includes/class-relve-ar-response.php +226 -0
  77. package/dist/wp/relve-agent-ready/includes/class-relve-ar-router.php +421 -0
  78. package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +201 -0
  79. package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +583 -0
  80. package/dist/wp/relve-agent-ready/includes/index.php +2 -0
  81. package/dist/wp/relve-agent-ready/index.php +2 -0
  82. package/dist/wp/relve-agent-ready/readme.txt +108 -0
  83. package/dist/wp/relve-agent-ready/relve-agent-ready.php +81 -0
  84. package/dist/wp/relve-agent-ready/uninstall.php +35 -0
  85. package/package.json +64 -6
@@ -0,0 +1,550 @@
1
+ <?php
2
+ /**
3
+ * Admin: Settings -> Agent Ready.
4
+ *
5
+ * Settings form (Settings API, sanitised by Relve_AR_Settings::sanitize), a
6
+ * status panel with this site's three test commands, and the refined-files
7
+ * upload / clear actions, the OpenAI key rows (masked, never echoed), the
8
+ * rewrite status and "Rewrite all now" (admin-post.php, each checking
9
+ * manage_options and its own nonce), and a notice while rewriting is paused.
10
+ * All output is escaped.
11
+ *
12
+ * @package Relve_Agent_Ready
13
+ */
14
+
15
+ if ( ! defined( 'ABSPATH' ) ) {
16
+ exit;
17
+ }
18
+
19
+ /**
20
+ * Settings page.
21
+ */
22
+ class Relve_AR_Admin {
23
+
24
+ const PAGE = 'relve-agent-ready';
25
+ const GROUP = 'relve_agent_ready_group';
26
+ const NOTICE = 'relve_ar_notice_';
27
+
28
+ /**
29
+ * Register hooks.
30
+ *
31
+ * @return void
32
+ */
33
+ public static function init() {
34
+ add_action( 'admin_menu', array( __CLASS__, 'menu' ) );
35
+ add_action( 'admin_init', array( __CLASS__, 'register' ) );
36
+ add_action( 'admin_post_relve_ar_upload', array( __CLASS__, 'handle_upload' ) );
37
+ add_action( 'admin_post_relve_ar_clear', array( __CLASS__, 'handle_clear' ) );
38
+ add_action( 'admin_post_relve_ar_refine_all', array( __CLASS__, 'handle_refine_all' ) );
39
+ add_action( 'admin_notices', array( __CLASS__, 'pause_notice' ) );
40
+ add_filter( 'plugin_action_links_' . plugin_basename( RELVE_AR_FILE ), array( __CLASS__, 'action_links' ) );
41
+ }
42
+
43
+ /**
44
+ * Settings link on the Plugins screen.
45
+ *
46
+ * @param array $links Links.
47
+ * @return array
48
+ */
49
+ public static function action_links( $links ) {
50
+ $url = admin_url( 'options-general.php?page=' . self::PAGE );
51
+ array_unshift( $links, '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Settings', 'relve-agent-ready' ) . '</a>' );
52
+ return $links;
53
+ }
54
+
55
+ /**
56
+ * Menu entry.
57
+ *
58
+ * @return void
59
+ */
60
+ public static function menu() {
61
+ add_options_page(
62
+ __( 'Agent Ready', 'relve-agent-ready' ),
63
+ __( 'Agent Ready', 'relve-agent-ready' ),
64
+ 'manage_options',
65
+ self::PAGE,
66
+ array( __CLASS__, 'render' )
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Register the option.
72
+ *
73
+ * @return void
74
+ */
75
+ public static function register() {
76
+ register_setting(
77
+ self::GROUP,
78
+ Relve_AR_Settings::OPTION,
79
+ array(
80
+ 'type' => 'array',
81
+ 'sanitize_callback' => array( 'Relve_AR_Settings', 'sanitize' ),
82
+ 'default' => Relve_AR_Settings::defaults(),
83
+ 'show_in_rest' => false,
84
+ )
85
+ );
86
+ }
87
+
88
+ // ── actions ───────────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * Refined-files zip upload.
92
+ *
93
+ * @return void
94
+ */
95
+ public static function handle_upload() {
96
+ if ( ! current_user_can( 'manage_options' ) ) {
97
+ wp_die( esc_html__( 'You are not allowed to do this.', 'relve-agent-ready' ), 403 );
98
+ }
99
+ check_admin_referer( 'relve_ar_upload' );
100
+
101
+ $notice = array(
102
+ 'type' => 'error',
103
+ 'message' => __( 'No file was uploaded.', 'relve-agent-ready' ),
104
+ 'reasons' => array(),
105
+ );
106
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- file array; tmp_name checked with is_uploaded_file().
107
+ $file = isset( $_FILES['relve_ar_zip'] ) && is_array( $_FILES['relve_ar_zip'] ) ? $_FILES['relve_ar_zip'] : null;
108
+ if ( $file && isset( $file['error'], $file['tmp_name'], $file['name'] ) && UPLOAD_ERR_OK === (int) $file['error'] ) {
109
+ $name = sanitize_file_name( (string) $file['name'] );
110
+ if ( '.zip' !== strtolower( substr( $name, -4 ) ) ) {
111
+ $notice['message'] = __( 'Upload a .zip file.', 'relve-agent-ready' );
112
+ } elseif ( ! is_uploaded_file( $file['tmp_name'] ) ) {
113
+ $notice['message'] = __( 'The upload could not be verified.', 'relve-agent-ready' );
114
+ } else {
115
+ $store = new Relve_AR_Store();
116
+ $store->ensure();
117
+ $r = $store->import_zip( $file['tmp_name'] );
118
+ if ( $r['ok'] ) {
119
+ $notice['type'] = 'success';
120
+ /* translators: 1: files stored, 2: files skipped, 3: entries rejected. */
121
+ $notice['message'] = sprintf( __( 'Stored %1$d refined files (skipped %2$d other files, rejected %3$d entries).', 'relve-agent-ready' ), $r['stored'], $r['skipped'], $r['rejected'] );
122
+ } else {
123
+ $notice['message'] = $r['error'];
124
+ }
125
+ $notice['reasons'] = array_slice( $r['reasons'], 0, 20 );
126
+ }
127
+ } elseif ( $file && isset( $file['error'] ) && UPLOAD_ERR_NO_FILE !== (int) $file['error'] ) {
128
+ $notice['message'] = __( 'The upload failed (it may be larger than the server allows).', 'relve-agent-ready' );
129
+ }
130
+ self::redirect_with( $notice );
131
+ }
132
+
133
+ /**
134
+ * Clear refined files.
135
+ *
136
+ * @return void
137
+ */
138
+ public static function handle_clear() {
139
+ if ( ! current_user_can( 'manage_options' ) ) {
140
+ wp_die( esc_html__( 'You are not allowed to do this.', 'relve-agent-ready' ), 403 );
141
+ }
142
+ check_admin_referer( 'relve_ar_clear' );
143
+ $store = new Relve_AR_Store();
144
+ $store->clear();
145
+ Relve_AR_Refiner::forget_all_meta();
146
+ self::redirect_with(
147
+ array(
148
+ 'type' => 'success',
149
+ 'message' => __( 'Refined files cleared.', 'relve-agent-ready' ),
150
+ 'reasons' => array(),
151
+ )
152
+ );
153
+ }
154
+
155
+ /**
156
+ * "Rewrite all now": queue every published post; WP-Cron does the work.
157
+ *
158
+ * @return void
159
+ */
160
+ public static function handle_refine_all() {
161
+ if ( ! current_user_can( 'manage_options' ) ) {
162
+ wp_die( esc_html__( 'You are not allowed to do this.', 'relve-agent-ready' ), 403 );
163
+ }
164
+ check_admin_referer( 'relve_ar_refine_all' );
165
+ $s = Relve_AR_Settings::get();
166
+ if ( '' === $s['openai_key'] ) {
167
+ self::redirect_with(
168
+ array(
169
+ 'type' => 'error',
170
+ 'message' => __( 'Save an OpenAI key first.', 'relve-agent-ready' ),
171
+ 'reasons' => array(),
172
+ )
173
+ );
174
+ }
175
+ $n = Relve_AR_Refiner::start_all();
176
+ self::redirect_with(
177
+ array(
178
+ 'type' => $n ? 'success' : 'error',
179
+ /* translators: %d: number of posts. */
180
+ 'message' => $n ? sprintf( _n( 'Rewriting %d post in the background, 5 at a time. Reload this page to see progress.', 'Rewriting %d posts in the background, 5 at a time. Reload this page to see progress.', $n, 'relve-agent-ready' ), $n ) : __( 'No published posts of the enabled types were found.', 'relve-agent-ready' ),
181
+ 'reasons' => array(),
182
+ )
183
+ );
184
+ }
185
+
186
+ /**
187
+ * Admin notice while rewriting is paused on a key problem.
188
+ *
189
+ * @return void
190
+ */
191
+ public static function pause_notice() {
192
+ if ( ! current_user_can( 'manage_options' ) ) {
193
+ return;
194
+ }
195
+ $state = Relve_AR_Refiner::state();
196
+ if ( '' === $state['paused'] ) {
197
+ return;
198
+ }
199
+ $url = admin_url( 'options-general.php?page=' . self::PAGE );
200
+ ?>
201
+ <div class="notice notice-error">
202
+ <p>
203
+ <strong><?php esc_html_e( 'Agent Ready paused rewriting posts.', 'relve-agent-ready' ); ?></strong>
204
+ <?php echo esc_html( Relve_AR_Refiner::error_label( $state['paused'] ) ); ?>
205
+ <code><?php echo esc_html( $state['paused'] ); ?></code>
206
+ <a href="<?php echo esc_url( $url ); ?>"><?php esc_html_e( 'Open Agent Ready settings', 'relve-agent-ready' ); ?></a>
207
+ </p>
208
+ </div>
209
+ <?php
210
+ }
211
+
212
+ /**
213
+ * Keep a notice for this user and go back to the settings page.
214
+ *
215
+ * @param array $notice Notice.
216
+ * @return void
217
+ */
218
+ private static function redirect_with( $notice ) {
219
+ set_transient( self::NOTICE . get_current_user_id(), $notice, 120 );
220
+ wp_safe_redirect( admin_url( 'options-general.php?page=' . self::PAGE ) );
221
+ exit;
222
+ }
223
+
224
+ // ── page ──────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Render the page.
228
+ *
229
+ * @return void
230
+ */
231
+ public static function render() {
232
+ if ( ! current_user_can( 'manage_options' ) ) {
233
+ return;
234
+ }
235
+ $s = Relve_AR_Settings::get();
236
+ $store = new Relve_AR_Store();
237
+ $count = $store->count();
238
+ $key = self::NOTICE . get_current_user_id();
239
+ $note = get_transient( $key );
240
+ if ( $note ) {
241
+ delete_transient( $key );
242
+ }
243
+ $types = get_post_types( array( 'public' => true ), 'objects' );
244
+ $opt = Relve_AR_Settings::OPTION;
245
+ ?>
246
+ <div class="wrap">
247
+ <h1><?php esc_html_e( 'Agent Ready', 'relve-agent-ready' ); ?></h1>
248
+ <p><?php esc_html_e( 'Serves a Markdown version of your posts and pages to AI agents that ask for it (Accept: text/markdown, a .md URL or ?format=md), plus an llms.txt. Browsers always get the normal page.', 'relve-agent-ready' ); ?></p>
249
+
250
+ <?php if ( is_array( $note ) ) : ?>
251
+ <div class="notice notice-<?php echo esc_attr( 'success' === $note['type'] ? 'success' : 'error' ); ?> is-dismissible">
252
+ <p><?php echo esc_html( $note['message'] ); ?></p>
253
+ <?php if ( ! empty( $note['reasons'] ) ) : ?>
254
+ <ul style="list-style:disc;margin-left:2em">
255
+ <?php foreach ( $note['reasons'] as $reason ) : ?>
256
+ <li><code><?php echo esc_html( $reason ); ?></code></li>
257
+ <?php endforeach; ?>
258
+ </ul>
259
+ <?php endif; ?>
260
+ </div>
261
+ <?php endif; ?>
262
+
263
+ <?php self::render_status( $s, $count ); ?>
264
+
265
+ <h2><?php esc_html_e( 'Settings', 'relve-agent-ready' ); ?></h2>
266
+ <form method="post" action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>">
267
+ <?php settings_fields( self::GROUP ); ?>
268
+ <table class="form-table" role="presentation">
269
+ <tr>
270
+ <th scope="row"><?php esc_html_e( 'Enabled', 'relve-agent-ready' ); ?></th>
271
+ <td>
272
+ <label>
273
+ <input type="checkbox" name="<?php echo esc_attr( $opt ); ?>[enabled]" value="1" <?php checked( $s['enabled'] ); ?> />
274
+ <?php esc_html_e( 'Serve Markdown and llms.txt. While unchecked, no request is changed.', 'relve-agent-ready' ); ?>
275
+ </label>
276
+ </td>
277
+ </tr>
278
+ <tr>
279
+ <th scope="row"><?php esc_html_e( 'Post types', 'relve-agent-ready' ); ?></th>
280
+ <td>
281
+ <?php foreach ( $types as $type ) : ?>
282
+ <?php
283
+ if ( 'attachment' === $type->name ) {
284
+ continue;
285
+ }
286
+ ?>
287
+ <label style="display:block">
288
+ <input type="checkbox" name="<?php echo esc_attr( $opt ); ?>[post_types][]" value="<?php echo esc_attr( $type->name ); ?>" <?php checked( in_array( $type->name, $s['post_types'], true ) ); ?> />
289
+ <?php echo esc_html( $type->labels->name ); ?> <code><?php echo esc_html( $type->name ); ?></code>
290
+ </label>
291
+ <?php endforeach; ?>
292
+ <p class="description"><?php esc_html_e( 'The front page and category, tag and author archives are served when Posts is enabled.', 'relve-agent-ready' ); ?></p>
293
+ </td>
294
+ </tr>
295
+ <tr>
296
+ <th scope="row"><label for="relve-ar-excluded"><?php esc_html_e( 'Excluded paths', 'relve-agent-ready' ); ?></label></th>
297
+ <td>
298
+ <textarea id="relve-ar-excluded" class="large-text code" rows="8" name="<?php echo esc_attr( $opt ); ?>[excluded_paths]"><?php echo esc_textarea( $s['excluded_paths'] ); ?></textarea>
299
+ <p class="description"><?php esc_html_e( 'One path per line. A path excludes itself and everything below it ("/cart" excludes /cart and /cart/x, not /cartoon). Matched against the full path and the path below your home URL.', 'relve-agent-ready' ); ?></p>
300
+ </td>
301
+ </tr>
302
+ <tr>
303
+ <th scope="row"><?php esc_html_e( 'llms.txt', 'relve-agent-ready' ); ?></th>
304
+ <td>
305
+ <?php
306
+ $modes = array(
307
+ 'auto' => __( 'Auto: serve it unless Yoast SEO (or a real llms.txt file) already does', 'relve-agent-ready' ),
308
+ 'on' => __( 'On: always serve it', 'relve-agent-ready' ),
309
+ 'off' => __( 'Off', 'relve-agent-ready' ),
310
+ );
311
+ foreach ( $modes as $value => $label ) :
312
+ ?>
313
+ <label style="display:block">
314
+ <input type="radio" name="<?php echo esc_attr( $opt ); ?>[llms_mode]" value="<?php echo esc_attr( $value ); ?>" <?php checked( $s['llms_mode'], $value ); ?> />
315
+ <?php echo esc_html( $label ); ?>
316
+ </label>
317
+ <?php endforeach; ?>
318
+ </td>
319
+ </tr>
320
+ <?php self::render_refine_fields( $s ); ?>
321
+ </table>
322
+ <?php submit_button(); ?>
323
+ </form>
324
+
325
+ <h2><?php esc_html_e( 'Better Markdown with your own OpenAI key', 'relve-agent-ready' ); ?></h2>
326
+ <p><?php esc_html_e( 'Each published post is sent to Relve\'s refine service, which rewrites it with Relve\'s prompt using your OpenAI key and returns clean Markdown. The result is stored as that post\'s refined file and served first. Your key passes through Relve\'s server for that single call only; it is not stored or logged there. You pay OpenAI for the tokens.', 'relve-agent-ready' ); ?></p>
327
+ <?php self::render_refine_status( $s, Relve_AR_Refiner::state(), Relve_AR_Refiner::queue(), Relve_AR_Refiner::count_rewritten() ); ?>
328
+ <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
329
+ <input type="hidden" name="action" value="relve_ar_refine_all" />
330
+ <?php wp_nonce_field( 'relve_ar_refine_all' ); ?>
331
+ <?php submit_button( __( 'Rewrite all now', 'relve-agent-ready' ), 'secondary', 'submit', false, '' === $s['openai_key'] ? array( 'disabled' => 'disabled' ) : array() ); ?>
332
+ <p class="description"><?php esc_html_e( 'Queues every published post of the enabled types; WP-Cron rewrites 5 per run. Pages that have not changed since their last rewrite cost nothing. To start again from scratch, use "Clear refined files" below.', 'relve-agent-ready' ); ?></p>
333
+ </form>
334
+
335
+ <h2><?php esc_html_e( 'Refined files from Relve', 'relve-agent-ready' ); ?></h2>
336
+ <p>
337
+ <?php
338
+ printf(
339
+ /* translators: %d: number of files. */
340
+ esc_html( _n( '%d refined file is stored. Refined files are served before the Markdown built from WordPress.', '%d refined files are stored. Refined files are served before the Markdown built from WordPress.', $count, 'relve-agent-ready' ) ),
341
+ (int) $count
342
+ );
343
+ ?>
344
+ </p>
345
+ <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
346
+ <input type="hidden" name="action" value="relve_ar_upload" />
347
+ <?php wp_nonce_field( 'relve_ar_upload' ); ?>
348
+ <input type="file" name="relve_ar_zip" accept=".zip,application/zip" required />
349
+ <?php submit_button( __( 'Upload zip', 'relve-agent-ready' ), 'secondary', 'submit', false ); ?>
350
+ <p class="description"><?php esc_html_e( 'The zip from Relve (refined-md.zip): a top-level md/ folder of .md files. Uploading replaces the stored set. Max 20 MB, 2 MB per file.', 'relve-agent-ready' ); ?></p>
351
+ </form>
352
+ <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-top:1em">
353
+ <input type="hidden" name="action" value="relve_ar_clear" />
354
+ <?php wp_nonce_field( 'relve_ar_clear' ); ?>
355
+ <?php submit_button( __( 'Clear refined files', 'relve-agent-ready' ), 'delete', 'submit', false, array( 'onclick' => "return confirm('" . esc_js( __( 'Delete all refined files?', 'relve-agent-ready' ) ) . "');" ) ); ?>
356
+ </form>
357
+ </div>
358
+ <?php
359
+ }
360
+
361
+ /**
362
+ * Settings rows: OpenAI key (masked, never echoed), model, rewrite on save.
363
+ *
364
+ * @param array $s Settings.
365
+ * @return void
366
+ */
367
+ public static function render_refine_fields( $s ) {
368
+ $opt = Relve_AR_Settings::OPTION;
369
+ $masked = Relve_AR_Settings::mask_key( $s['openai_key'] );
370
+ ?>
371
+ <tr>
372
+ <th scope="row"><label for="relve-ar-openai-key"><?php esc_html_e( 'OpenAI key', 'relve-agent-ready' ); ?></label></th>
373
+ <td>
374
+ <?php if ( '' !== $masked ) : ?>
375
+ <p><?php esc_html_e( 'Saved key:', 'relve-agent-ready' ); ?> <code><?php echo esc_html( $masked ); ?></code></p>
376
+ <label>
377
+ <input type="checkbox" name="<?php echo esc_attr( $opt ); ?>[openai_key_remove]" value="1" />
378
+ <?php esc_html_e( 'Remove the saved key', 'relve-agent-ready' ); ?>
379
+ </label>
380
+ <?php endif; ?>
381
+ <p>
382
+ <input type="password" id="relve-ar-openai-key" class="regular-text code" name="<?php echo esc_attr( $opt ); ?>[openai_key_new]" value="" autocomplete="new-password" spellcheck="false" placeholder="<?php echo esc_attr( '' !== $masked ? __( 'Replace key', 'relve-agent-ready' ) : 'sk-...' ); ?>" />
383
+ </p>
384
+ <p class="description"><?php esc_html_e( 'Stored in this site\'s own database. Sent only to Relve\'s refine endpoint over https, for one call at a time; Relve does not store or log it. Leave empty to keep the saved key.', 'relve-agent-ready' ); ?></p>
385
+ </td>
386
+ </tr>
387
+ <tr>
388
+ <th scope="row"><label for="relve-ar-model"><?php esc_html_e( 'Model', 'relve-agent-ready' ); ?></label></th>
389
+ <td>
390
+ <select id="relve-ar-model" name="<?php echo esc_attr( $opt ); ?>[refine_model]">
391
+ <?php foreach ( Relve_AR_Settings::MODELS as $model ) : ?>
392
+ <option value="<?php echo esc_attr( $model ); ?>" <?php selected( $s['refine_model'], $model ); ?>><?php echo esc_html( $model ); ?></option>
393
+ <?php endforeach; ?>
394
+ </select>
395
+ </td>
396
+ </tr>
397
+ <tr>
398
+ <th scope="row"><?php esc_html_e( 'Rewrite on publish', 'relve-agent-ready' ); ?></th>
399
+ <td>
400
+ <label>
401
+ <input type="checkbox" name="<?php echo esc_attr( $opt ); ?>[refine_on_save]" value="1" <?php checked( $s['refine_on_save'] ); ?> />
402
+ <?php esc_html_e( 'Rewrite a post in the background (WP-Cron, about 30 seconds later) when it is published or updated. Needs a saved key and "Enabled".', 'relve-agent-ready' ); ?>
403
+ </label>
404
+ </td>
405
+ </tr>
406
+ <?php
407
+ }
408
+
409
+ /**
410
+ * Rewrite status: key, posts rewritten, last run, last error, progress.
411
+ *
412
+ * @param array $s Settings.
413
+ * @param array $state Refiner state.
414
+ * @param array $q Refiner queue.
415
+ * @param int $rewritten Posts with a rewritten file.
416
+ * @return void
417
+ */
418
+ public static function render_refine_status( $s, $state, $q, $rewritten ) {
419
+ $format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
420
+ ?>
421
+ <table class="widefat striped" style="max-width:60em">
422
+ <tbody>
423
+ <tr>
424
+ <td><?php esc_html_e( 'OpenAI key saved', 'relve-agent-ready' ); ?></td>
425
+ <td><strong><?php echo '' !== $s['openai_key'] ? esc_html__( 'Yes', 'relve-agent-ready' ) : esc_html__( 'No', 'relve-agent-ready' ); ?></strong></td>
426
+ </tr>
427
+ <tr>
428
+ <td><?php esc_html_e( 'Posts rewritten', 'relve-agent-ready' ); ?></td>
429
+ <td><?php echo esc_html( (string) (int) $rewritten ); ?></td>
430
+ </tr>
431
+ <tr>
432
+ <td><?php esc_html_e( 'Last run', 'relve-agent-ready' ); ?></td>
433
+ <td><?php echo $state['last_run'] ? esc_html( wp_date( $format, (int) $state['last_run'] ) ) : esc_html__( 'Never', 'relve-agent-ready' ); ?></td>
434
+ </tr>
435
+ <tr>
436
+ <td><?php esc_html_e( 'Last error', 'relve-agent-ready' ); ?></td>
437
+ <td>
438
+ <?php if ( '' === $state['last_error'] ) : ?>
439
+ <?php esc_html_e( 'None', 'relve-agent-ready' ); ?>
440
+ <?php else : ?>
441
+ <code><?php echo esc_html( $state['last_error'] ); ?></code>
442
+ <?php echo esc_html( Relve_AR_Refiner::error_label( $state['last_error'] ) ); ?>
443
+ <?php if ( $state['last_error_at'] ) : ?>
444
+ (<?php echo esc_html( wp_date( $format, (int) $state['last_error_at'] ) ); ?><?php echo $state['last_error_post'] ? esc_html( ', post #' . (int) $state['last_error_post'] ) : ''; ?>)
445
+ <?php endif; ?>
446
+ <?php endif; ?>
447
+ </td>
448
+ </tr>
449
+ <tr>
450
+ <td><?php esc_html_e( 'Rewrite all', 'relve-agent-ready' ); ?></td>
451
+ <td>
452
+ <?php
453
+ if ( $q['ids'] ) {
454
+ /* translators: 1: posts done, 2: posts queued. */
455
+ echo esc_html( sprintf( __( 'In progress: %1$d / %2$d', 'relve-agent-ready' ), $q['done'], $q['total'] ) );
456
+ if ( '' !== $state['paused'] ) {
457
+ echo ' ';
458
+ /* translators: %s: error code. */
459
+ echo esc_html( sprintf( __( '(paused: %s; fix the key and it resumes)', 'relve-agent-ready' ), $state['paused'] ) );
460
+ }
461
+ } elseif ( is_array( $state['last_batch'] ) && isset( $state['last_batch']['done'], $state['last_batch']['total'] ) ) {
462
+ /* translators: 1: posts done, 2: posts queued. */
463
+ echo esc_html( sprintf( __( 'Finished: %1$d / %2$d', 'relve-agent-ready' ), (int) $state['last_batch']['done'], (int) $state['last_batch']['total'] ) );
464
+ } else {
465
+ esc_html_e( 'Not run yet', 'relve-agent-ready' );
466
+ }
467
+ ?>
468
+ </td>
469
+ </tr>
470
+ </tbody>
471
+ </table>
472
+ <?php
473
+ }
474
+
475
+ /**
476
+ * Status panel.
477
+ *
478
+ * @param array $s Settings.
479
+ * @param int $count Refined file count.
480
+ * @return void
481
+ */
482
+ private static function render_status( $s, $count ) {
483
+ $yoast = Relve_AR_Llms::yoast_serves();
484
+ $physical = Relve_AR_Llms::physical_file_exists();
485
+ $serves = Relve_AR_Llms::should_serve( $s['llms_mode'], $yoast, $physical );
486
+ $origin = Relve_AR_Router::home_origin();
487
+ $llms = array();
488
+ foreach ( Relve_AR_Router::llms_paths( $s ) as $path ) {
489
+ $llms[] = $origin . $path;
490
+ }
491
+
492
+ $sample = get_posts(
493
+ array(
494
+ 'post_type' => in_array( 'post', $s['post_types'], true ) ? 'post' : ( $s['post_types'] ? $s['post_types'] : 'post' ),
495
+ 'post_status' => 'publish',
496
+ 'has_password' => false,
497
+ 'numberposts' => 1,
498
+ 'orderby' => 'date',
499
+ 'order' => 'DESC',
500
+ 'suppress_filters' => false,
501
+ )
502
+ );
503
+ $page = $sample ? get_permalink( $sample[0] ) : home_url( '/' );
504
+ $md = Relve_AR_Llms::md_url( $page, home_url( '/' ) );
505
+ ?>
506
+ <div class="card" style="max-width:none">
507
+ <h2 class="title"><?php esc_html_e( 'Status', 'relve-agent-ready' ); ?></h2>
508
+ <table class="widefat striped" style="max-width:60em">
509
+ <tbody>
510
+ <tr>
511
+ <td><?php esc_html_e( 'Serving Markdown', 'relve-agent-ready' ); ?></td>
512
+ <td><strong><?php echo $s['enabled'] ? esc_html__( 'On', 'relve-agent-ready' ) : esc_html__( 'Off (tick Enabled below)', 'relve-agent-ready' ); ?></strong></td>
513
+ </tr>
514
+ <tr>
515
+ <td><?php esc_html_e( 'Refined files', 'relve-agent-ready' ); ?></td>
516
+ <td><?php echo esc_html( (string) (int) $count ); ?></td>
517
+ </tr>
518
+ <tr>
519
+ <td><?php esc_html_e( 'llms.txt', 'relve-agent-ready' ); ?></td>
520
+ <td>
521
+ <?php
522
+ if ( ! $s['enabled'] ) {
523
+ esc_html_e( 'Off while the plugin is disabled.', 'relve-agent-ready' );
524
+ } elseif ( $serves ) {
525
+ echo esc_html( implode( ' ', $llms ) );
526
+ } elseif ( 'off' === $s['llms_mode'] ) {
527
+ esc_html_e( 'Off (setting).', 'relve-agent-ready' );
528
+ } elseif ( $yoast ) {
529
+ esc_html_e( 'Served by Yoast SEO; this plugin stays out of the way (choose "On" to take over).', 'relve-agent-ready' );
530
+ } else {
531
+ esc_html_e( 'A real llms.txt file exists in the WordPress folder; the web server serves that one.', 'relve-agent-ready' );
532
+ }
533
+ ?>
534
+ </td>
535
+ </tr>
536
+ </tbody>
537
+ </table>
538
+ <p><?php esc_html_e( 'Test from a terminal (expect Content-Type: text/markdown and x-agent-ready-source):', 'relve-agent-ready' ); ?></p>
539
+ <pre style="white-space:pre-wrap;background:#f6f7f7;padding:1em;max-width:60em"><?php
540
+ echo esc_html(
541
+ 'curl -sI -H "Accept: text/markdown" "' . $page . "\"\n"
542
+ . 'curl -s "' . $md . "\"\n"
543
+ . 'curl -s "' . ( $llms ? $llms[0] : $origin . Relve_AR_Router::home_path() . '/llms.txt' ) . '"'
544
+ );
545
+ ?></pre>
546
+ <p class="description"><?php esc_html_e( 'A page cache or CDN in front of WordPress may still answer an Accept request with cached HTML. That is safe; the .md URL always works. Behind a proxy or Worker, make sure it forwards the Accept header, .md paths and llms.txt to WordPress.', 'relve-agent-ready' ); ?></p>
547
+ </div>
548
+ <?php
549
+ }
550
+ }