@relvehq/agent-ready 0.3.0 → 0.3.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.
@@ -1,192 +1,192 @@
1
- <?php
2
- /**
3
- * llms.txt (https://llmstxt.org): "# Site name", "> tagline", then sections
4
- * of links to the `.md` URLs of recent posts and pages (up to 200).
5
- *
6
- * Port of @relvehq/agent-ready core/llms.ts (buildLlmsTxt) plus the WordPress
7
- * side: which posts to list and whether this plugin should answer at all
8
- * (mode auto|on|off; auto stays off when Yoast serves its own llms.txt or a
9
- * physical llms.txt file exists).
10
- *
11
- * @package Relve_Agent_Ready
12
- */
13
-
14
- if ( ! defined( 'ABSPATH' ) ) {
15
- exit;
16
- }
17
-
18
- /**
19
- * llms.txt builder and mode decision.
20
- */
21
- class Relve_AR_Llms {
22
-
23
- /** Max links listed. */
24
- const MAX_ENTRIES = 200;
25
-
26
- // ── pure helpers (unit-tested) ────────────────────────────────────────
27
-
28
- /**
29
- * Should the plugin answer llms.txt?
30
- *
31
- * @param string $mode auto|on|off.
32
- * @param bool $yoast_serves Yoast's llms.txt feature is on.
33
- * @param bool $physical_file A real llms.txt file sits in the web root.
34
- * @return bool
35
- */
36
- public static function should_serve( $mode, $yoast_serves, $physical_file = false ) {
37
- if ( 'on' === $mode ) {
38
- return true;
39
- }
40
- if ( 'off' === $mode ) {
41
- return false;
42
- }
43
- return ! $yoast_serves && ! $physical_file;
44
- }
45
-
46
- /**
47
- * Build the file.
48
- *
49
- * @param string $title Site name.
50
- * @param string $summary Tagline.
51
- * @param array $sections Section name => list of { title, url, description }.
52
- * @return string
53
- */
54
- public static function build( $title, $summary, $sections ) {
55
- $title = self::one_line( $title );
56
- $out = array( '# ' . ( '' === $title ? 'Site' : $title ) );
57
- $sum = self::one_line( $summary );
58
- if ( '' !== $sum ) {
59
- $out[] = '> ' . $sum;
60
- }
61
- foreach ( $sections as $name => $entries ) {
62
- $lines = array();
63
- foreach ( $entries as $e ) {
64
- if ( empty( $e['title'] ) || empty( $e['url'] ) ) {
65
- continue;
66
- }
67
- $t = str_replace( array( '[', ']' ), '', self::one_line( $e['title'] ) );
68
- $desc = isset( $e['description'] ) ? self::one_line( $e['description'] ) : '';
69
- $lines[] = '- [' . $t . '](' . Relve_AR_Converter::url_for_markdown( $e['url'] ) . ')' . ( '' !== $desc ? ': ' . $desc : '' );
70
- }
71
- if ( $lines ) {
72
- $out[] = '## ' . self::one_line( $name );
73
- $out[] = implode( "\n", $lines );
74
- }
75
- }
76
- return implode( "\n\n", $out ) . "\n";
77
- }
78
-
79
- /**
80
- * The `.md` URL of a permalink: "https://s/a/b/" -> "https://s/a/b.md";
81
- * the home URL -> ".../index.md"; a permalink with a query string (plain
82
- * permalinks) gets "format=md" instead.
83
- *
84
- * @param string $permalink Permalink.
85
- * @param string $home_url Home URL.
86
- * @return string
87
- */
88
- public static function md_url( $permalink, $home_url ) {
89
- $permalink = explode( '#', (string) $permalink, 2 )[0];
90
- if ( false !== strpos( $permalink, '?' ) ) {
91
- return $permalink . '&format=md';
92
- }
93
- if ( rtrim( $permalink, '/' ) === rtrim( (string) $home_url, '/' ) ) {
94
- return rtrim( $permalink, '/' ) . '/index.md';
95
- }
96
- return rtrim( $permalink, '/' ) . '.md';
97
- }
98
-
99
- /**
100
- * Collapse whitespace.
101
- *
102
- * @param string $s Text.
103
- * @return string
104
- */
105
- private static function one_line( $s ) {
106
- return Relve_AR_Markdown_Builder::one_line( (string) $s );
107
- }
108
-
109
- // ── WordPress-bound ───────────────────────────────────────────────────
110
-
111
- /**
112
- * Does Yoast SEO serve llms.txt on this site? Checked defensively: Yoast
113
- * active and its "enable_llms_txt" option on. Unsure means no.
114
- *
115
- * @return bool
116
- */
117
- public static function yoast_serves() {
118
- $serves = false;
119
- if ( defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Options' ) ) {
120
- $opt = get_option( 'wpseo' );
121
- $serves = is_array( $opt ) && ! empty( $opt['enable_llms_txt'] );
122
- }
123
- return (bool) apply_filters( 'relve_agent_ready_yoast_serves_llms', $serves );
124
- }
125
-
126
- /**
127
- * Is there a physical llms.txt in the WordPress root (the web server
128
- * would answer it before WordPress anyway)?
129
- *
130
- * @return bool
131
- */
132
- public static function physical_file_exists() {
133
- return defined( 'ABSPATH' ) && file_exists( ABSPATH . 'llms.txt' );
134
- }
135
-
136
- /**
137
- * Build this site's llms.txt.
138
- *
139
- * @param array $settings Plugin settings.
140
- * @return string
141
- */
142
- public static function generate( $settings ) {
143
- $home = home_url( '/' );
144
- $types = array_values( array_intersect( (array) $settings['post_types'], get_post_types( array( 'public' => true ) ) ) );
145
- $sections = array();
146
- $left = self::MAX_ENTRIES;
147
- $home_path = Relve_AR_Router::home_path();
148
- foreach ( $types as $type ) {
149
- if ( $left <= 0 ) {
150
- break;
151
- }
152
- $obj = get_post_type_object( $type );
153
- $name = $obj && isset( $obj->labels->name ) ? $obj->labels->name : $type;
154
- $query = new WP_Query(
155
- array(
156
- 'post_type' => $type,
157
- 'post_status' => 'publish',
158
- 'has_password' => false,
159
- 'posts_per_page' => $left,
160
- 'orderby' => 'post' === $type ? 'date' : 'modified',
161
- 'order' => 'DESC',
162
- 'ignore_sticky_posts' => true,
163
- 'no_found_rows' => true,
164
- )
165
- );
166
- $entries = array();
167
- foreach ( $query->posts as $p ) {
168
- $permalink = get_permalink( $p );
169
- if ( ! $permalink ) {
170
- continue;
171
- }
172
- $path = Relve_AR_Negotiation::normalize_path( (string) wp_parse_url( $permalink, PHP_URL_PATH ) );
173
- if ( null !== $path && Relve_AR_Negotiation::is_excluded( $path, $settings['excluded'], $home_path ) ) {
174
- continue;
175
- }
176
- $entries[] = array(
177
- 'title' => html_entity_decode( wp_strip_all_tags( get_the_title( $p ) ), ENT_QUOTES, 'UTF-8' ),
178
- 'url' => self::md_url( $permalink, $home ),
179
- 'description' => Relve_AR_Markdown_Builder::shorten( Relve_AR_Markdown_Builder::description_for( $p ), 200 ),
180
- );
181
- }
182
- $left -= count( $entries );
183
- $sections[ $name ] = $entries;
184
- }
185
- $sections = apply_filters( 'relve_agent_ready_llms_sections', $sections );
186
- return self::build(
187
- html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ),
188
- html_entity_decode( get_bloginfo( 'description' ), ENT_QUOTES, 'UTF-8' ),
189
- $sections
190
- );
191
- }
192
- }
1
+ <?php
2
+ /**
3
+ * llms.txt (https://llmstxt.org): "# Site name", "> tagline", then sections
4
+ * of links to the `.md` URLs of recent posts and pages (up to 200).
5
+ *
6
+ * Port of @relvehq/agent-ready core/llms.ts (buildLlmsTxt) plus the WordPress
7
+ * side: which posts to list and whether this plugin should answer at all
8
+ * (mode auto|on|off; auto stays off when Yoast serves its own llms.txt or a
9
+ * physical llms.txt file exists).
10
+ *
11
+ * @package Relve_Agent_Ready
12
+ */
13
+
14
+ if ( ! defined( 'ABSPATH' ) ) {
15
+ exit;
16
+ }
17
+
18
+ /**
19
+ * llms.txt builder and mode decision.
20
+ */
21
+ class Relve_AR_Llms {
22
+
23
+ /** Max links listed. */
24
+ const MAX_ENTRIES = 200;
25
+
26
+ // ── pure helpers (unit-tested) ────────────────────────────────────────
27
+
28
+ /**
29
+ * Should the plugin answer llms.txt?
30
+ *
31
+ * @param string $mode auto|on|off.
32
+ * @param bool $yoast_serves Yoast's llms.txt feature is on.
33
+ * @param bool $physical_file A real llms.txt file sits in the web root.
34
+ * @return bool
35
+ */
36
+ public static function should_serve( $mode, $yoast_serves, $physical_file = false ) {
37
+ if ( 'on' === $mode ) {
38
+ return true;
39
+ }
40
+ if ( 'off' === $mode ) {
41
+ return false;
42
+ }
43
+ return ! $yoast_serves && ! $physical_file;
44
+ }
45
+
46
+ /**
47
+ * Build the file.
48
+ *
49
+ * @param string $title Site name.
50
+ * @param string $summary Tagline.
51
+ * @param array $sections Section name => list of { title, url, description }.
52
+ * @return string
53
+ */
54
+ public static function build( $title, $summary, $sections ) {
55
+ $title = self::one_line( $title );
56
+ $out = array( '# ' . ( '' === $title ? 'Site' : $title ) );
57
+ $sum = self::one_line( $summary );
58
+ if ( '' !== $sum ) {
59
+ $out[] = '> ' . $sum;
60
+ }
61
+ foreach ( $sections as $name => $entries ) {
62
+ $lines = array();
63
+ foreach ( $entries as $e ) {
64
+ if ( empty( $e['title'] ) || empty( $e['url'] ) ) {
65
+ continue;
66
+ }
67
+ $t = str_replace( array( '[', ']' ), '', self::one_line( $e['title'] ) );
68
+ $desc = isset( $e['description'] ) ? self::one_line( $e['description'] ) : '';
69
+ $lines[] = '- [' . $t . '](' . Relve_AR_Converter::url_for_markdown( $e['url'] ) . ')' . ( '' !== $desc ? ': ' . $desc : '' );
70
+ }
71
+ if ( $lines ) {
72
+ $out[] = '## ' . self::one_line( $name );
73
+ $out[] = implode( "\n", $lines );
74
+ }
75
+ }
76
+ return implode( "\n\n", $out ) . "\n";
77
+ }
78
+
79
+ /**
80
+ * The `.md` URL of a permalink: "https://s/a/b/" -> "https://s/a/b.md";
81
+ * the home URL -> ".../index.md"; a permalink with a query string (plain
82
+ * permalinks) gets "format=md" instead.
83
+ *
84
+ * @param string $permalink Permalink.
85
+ * @param string $home_url Home URL.
86
+ * @return string
87
+ */
88
+ public static function md_url( $permalink, $home_url ) {
89
+ $permalink = explode( '#', (string) $permalink, 2 )[0];
90
+ if ( false !== strpos( $permalink, '?' ) ) {
91
+ return $permalink . '&format=md';
92
+ }
93
+ if ( rtrim( $permalink, '/' ) === rtrim( (string) $home_url, '/' ) ) {
94
+ return rtrim( $permalink, '/' ) . '/index.md';
95
+ }
96
+ return rtrim( $permalink, '/' ) . '.md';
97
+ }
98
+
99
+ /**
100
+ * Collapse whitespace.
101
+ *
102
+ * @param string $s Text.
103
+ * @return string
104
+ */
105
+ private static function one_line( $s ) {
106
+ return Relve_AR_Markdown_Builder::one_line( (string) $s );
107
+ }
108
+
109
+ // ── WordPress-bound ───────────────────────────────────────────────────
110
+
111
+ /**
112
+ * Does Yoast SEO serve llms.txt on this site? Checked defensively: Yoast
113
+ * active and its "enable_llms_txt" option on. Unsure means no.
114
+ *
115
+ * @return bool
116
+ */
117
+ public static function yoast_serves() {
118
+ $serves = false;
119
+ if ( defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Options' ) ) {
120
+ $opt = get_option( 'wpseo' );
121
+ $serves = is_array( $opt ) && ! empty( $opt['enable_llms_txt'] );
122
+ }
123
+ return (bool) apply_filters( 'relve_agent_ready_yoast_serves_llms', $serves );
124
+ }
125
+
126
+ /**
127
+ * Is there a physical llms.txt in the WordPress root (the web server
128
+ * would answer it before WordPress anyway)?
129
+ *
130
+ * @return bool
131
+ */
132
+ public static function physical_file_exists() {
133
+ return defined( 'ABSPATH' ) && file_exists( ABSPATH . 'llms.txt' );
134
+ }
135
+
136
+ /**
137
+ * Build this site's llms.txt.
138
+ *
139
+ * @param array $settings Plugin settings.
140
+ * @return string
141
+ */
142
+ public static function generate( $settings ) {
143
+ $home = home_url( '/' );
144
+ $types = array_values( array_intersect( (array) $settings['post_types'], get_post_types( array( 'public' => true ) ) ) );
145
+ $sections = array();
146
+ $left = self::MAX_ENTRIES;
147
+ $home_path = Relve_AR_Router::home_path();
148
+ foreach ( $types as $type ) {
149
+ if ( $left <= 0 ) {
150
+ break;
151
+ }
152
+ $obj = get_post_type_object( $type );
153
+ $name = $obj && isset( $obj->labels->name ) ? $obj->labels->name : $type;
154
+ $query = new WP_Query(
155
+ array(
156
+ 'post_type' => $type,
157
+ 'post_status' => 'publish',
158
+ 'has_password' => false,
159
+ 'posts_per_page' => $left,
160
+ 'orderby' => 'post' === $type ? 'date' : 'modified',
161
+ 'order' => 'DESC',
162
+ 'ignore_sticky_posts' => true,
163
+ 'no_found_rows' => true,
164
+ )
165
+ );
166
+ $entries = array();
167
+ foreach ( $query->posts as $p ) {
168
+ $permalink = get_permalink( $p );
169
+ if ( ! $permalink ) {
170
+ continue;
171
+ }
172
+ $path = Relve_AR_Negotiation::normalize_path( (string) wp_parse_url( $permalink, PHP_URL_PATH ) );
173
+ if ( null !== $path && Relve_AR_Negotiation::is_excluded( $path, $settings['excluded'], $home_path ) ) {
174
+ continue;
175
+ }
176
+ $entries[] = array(
177
+ 'title' => html_entity_decode( wp_strip_all_tags( get_the_title( $p ) ), ENT_QUOTES, 'UTF-8' ),
178
+ 'url' => self::md_url( $permalink, $home ),
179
+ 'description' => Relve_AR_Markdown_Builder::shorten( Relve_AR_Markdown_Builder::description_for( $p ), 200 ),
180
+ );
181
+ }
182
+ $left -= count( $entries );
183
+ $sections[ $name ] = $entries;
184
+ }
185
+ $sections = apply_filters( 'relve_agent_ready_llms_sections', $sections );
186
+ return self::build(
187
+ html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ),
188
+ html_entity_decode( get_bloginfo( 'description' ), ENT_QUOTES, 'UTF-8' ),
189
+ $sections
190
+ );
191
+ }
192
+ }