@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.
- package/README.md +51 -25
- package/SETUP.md +24 -7
- package/dist/bin/agent-ready.js +105 -11
- package/dist/refine/remote.d.ts +57 -0
- package/dist/refine/remote.js +238 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-admin.php +189 -2
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-markdown-builder.php +22 -14
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-refiner.php +894 -0
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-settings.php +89 -3
- package/dist/wp/relve-agent-ready/includes/class-relve-ar-store.php +40 -0
- package/dist/wp/relve-agent-ready/readme.txt +37 -4
- package/dist/wp/relve-agent-ready/relve-agent-ready.php +16 -5
- package/dist/wp/relve-agent-ready/uninstall.php +15 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ they got before.
|
|
|
8
8
|
- **Serve**: Markdown on `Accept: text/markdown`, on `<page>.md` and on
|
|
9
9
|
`?format=md`, plus a generated [`/llms.txt`](https://llmstxt.org). No AI
|
|
10
10
|
runs per request: it is plain code, in milliseconds.
|
|
11
|
-
- **Refine** (optional, with your own
|
|
12
|
-
better Markdown file for each page ahead of time
|
|
13
|
-
number and URL against the page,
|
|
14
|
-
|
|
11
|
+
- **Refine** (optional, with your own OpenAI key): one command writes a
|
|
12
|
+
better Markdown file for each page ahead of time with Relve's prompt, a
|
|
13
|
+
check compares every number and URL against the page, and unchanged pages
|
|
14
|
+
are skipped (free) on the next run.
|
|
15
15
|
- **Proof**: a free checker that scores any site out of 100, and a dashboard
|
|
16
16
|
of which AI agents read which pages.
|
|
17
17
|
|
|
@@ -168,28 +168,49 @@ Most CDNs (CloudFront included) don't vary their cache on `Accept`, so:
|
|
|
168
168
|
HTML. Safe, just not Markdown. `.md` URLs always work; reliable negotiation
|
|
169
169
|
needs `Accept` in the CDN cache key.
|
|
170
170
|
|
|
171
|
-
## Refine: better Markdown
|
|
171
|
+
## Refine: better Markdown with your own OpenAI key
|
|
172
|
+
|
|
173
|
+
One command, run from your project root with `OPENAI_API_KEY` in `.env.local`,
|
|
174
|
+
`.env` or the shell:
|
|
172
175
|
|
|
173
176
|
```
|
|
174
|
-
npx agent-ready refine --site https://your-site.com --
|
|
175
|
-
npx agent-ready refine --site https://your-site.com --
|
|
177
|
+
npx agent-ready refine --site https://your-site.com --sitemap --dry-run
|
|
178
|
+
npx agent-ready refine --site https://your-site.com --sitemap
|
|
176
179
|
```
|
|
177
180
|
|
|
178
|
-
For each page
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
and
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
For each page the command fetches the HTML itself and sends it to the Relve
|
|
182
|
+
API (`relvehq.com`) with your key in a header. Relve's prompt runs on Relve's
|
|
183
|
+
server: it extracts the page, calls OpenAI with your key, checks the result
|
|
184
|
+
(every number and URL must be on the page; one retry), and returns the
|
|
185
|
+
Markdown. The command saves `<out>/<path>.md` plus `<out>/<path>.json`
|
|
186
|
+
(default out: `.agent-ready/md`).
|
|
187
|
+
|
|
188
|
+
- **Cost:** your own OpenAI usage, one model call per changed page (roughly
|
|
189
|
+
a few thousand tokens each with the default `gpt-4o-mini`). Relve charges nothing.
|
|
190
|
+
- **Your key:** used for that call only. Relve does not store or log it, and
|
|
191
|
+
it is never written to the output files.
|
|
192
|
+
- **Re-run when content changes.** Each saved `.json` holds the page's
|
|
193
|
+
fingerprint; it is sent along, and an unchanged page comes back as
|
|
194
|
+
`unchanged` with no model call (free). `--force` rewrites everything.
|
|
195
|
+
- **Optional in CI or deploy:** run the command before `next build` (with
|
|
196
|
+
`OPENAI_API_KEY` as a secret) and commit or ship `.agent-ready/md`.
|
|
197
|
+
|
|
198
|
+
| Option | |
|
|
185
199
|
|---|---|
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
200
|
+
| `--sitemap` / `--paths /a,/b` | which pages (sitemap at `<site>/sitemap.xml`) |
|
|
201
|
+
| `--model` | `gpt-4o-mini` (default, fastest), `gpt-4o`, `gpt-4.1`, `gpt-4.1-mini` |
|
|
202
|
+
| `--site-name` | your brand name, used in the documents |
|
|
203
|
+
| `--out`, `--limit`, `--concurrency` (2), `--force`, `--dry-run` | `--dry-run` lists the pages: no API call, nothing written |
|
|
204
|
+
| `AGENT_READY_REFINE_ENDPOINT` | another API URL (testing) |
|
|
205
|
+
|
|
206
|
+
The run ends with `written / unchanged / rejected / errors` and the token
|
|
207
|
+
total. Rejected and failed pages are listed with their code (for example
|
|
208
|
+
`too_little_text`) and nothing is saved for them; the exit code is then 1.
|
|
209
|
+
If OpenAI rejects the key or your account is out of quota, the run stops at
|
|
210
|
+
the first page and says so.
|
|
189
211
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
which the Edge middleware imports):
|
|
212
|
+
Serve the files from the route file (`fileStore` is Node only, so keep it
|
|
213
|
+
out of `agent-ready.config.ts`, which the Edge middleware imports):
|
|
193
214
|
|
|
194
215
|
```ts
|
|
195
216
|
// app/%5F%5Fmd/[[...path]]/route.ts
|
|
@@ -209,10 +230,14 @@ required section is missing, it has more or fewer than one H1, HTML tags, a
|
|
|
209
230
|
wrong fingerprint, or it is over the token budget. It cannot catch a reworded
|
|
210
231
|
claim, so read a sample before you switch on.
|
|
211
232
|
|
|
212
|
-
**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
233
|
+
**Advanced: your own prompts.** The package ships no prompt. With
|
|
234
|
+
`--prompts <dir>` (several: `a,b`) or the `AGENT_READY_PROMPTS` variable
|
|
235
|
+
(`.env.local`, `.env` or the shell), `refine` runs your prompt files on your
|
|
236
|
+
machine instead of calling the Relve API, with `OPENAI_API_KEY` (default
|
|
237
|
+
model `gpt-4o`) or `ANTHROPIC_API_KEY` (default `claude-opus-5-5`);
|
|
238
|
+
`AGENT_READY_MODEL` picks another model. The steps are the same: fetch,
|
|
239
|
+
extract, skip unchanged, fill the prompt, one model call, check, one retry,
|
|
240
|
+
save. A prompt file is Markdown with `## SYSTEM`, `## USER`
|
|
216
241
|
(`{{PAGE_URL}}`, `{{PAGE_TEXT}}`, `{{PAGE_LINKS}}`, `{{JSON_LD}}`,
|
|
217
242
|
`{{SOURCE_HASH}}`) and a `## Settings` table (`page_type`, `version`,
|
|
218
243
|
`required_sections`, `token_budget`). A `page_type` of `page` takes every
|
|
@@ -327,8 +352,9 @@ if (d.markdown) {
|
|
|
327
352
|
```
|
|
328
353
|
agent-ready init [--stack next|node|wordpress] [--dry-run] [--force] [--cwd <dir>]
|
|
329
354
|
agent-ready check <url> [--json]
|
|
330
|
-
agent-ready refine --site <origin> (--paths /a,/b | --sitemap) --
|
|
355
|
+
agent-ready refine --site <origin> (--paths /a,/b | --sitemap) [--model <id>] [--site-name <name>]
|
|
331
356
|
[--out <dir>] [--limit <n>] [--force] [--concurrency <n>] [--dry-run]
|
|
357
|
+
[--prompts <dir>] (advanced: your own prompts, run locally)
|
|
332
358
|
agent-ready stats --log <hits.jsonl> [--days <n>] [--json]
|
|
333
359
|
```
|
|
334
360
|
|
package/SETUP.md
CHANGED
|
@@ -209,7 +209,7 @@ The plugin tells WP Rocket not to cache `.md` and `?format=md` responses, and ma
|
|
|
209
209
|
|
|
210
210
|
### Refined files on WordPress
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
With your own OpenAI key, the plugin can write a better Markdown file for each post itself: see the plugin's settings page (Settings > Agent Ready) and its readme. You can also upload a zip of `.md` files you made with `npx agent-ready refine` (zip the `.agent-ready/md` folder): under the refined files section, choose it and click "Upload zip" (up to 20 MB, 2 MB per file). Each upload replaces the whole stored set; "Clear refined files" removes them all. See "Add refined files" below for how to check it.
|
|
213
213
|
|
|
214
214
|
## Test it locally
|
|
215
215
|
|
|
@@ -265,12 +265,29 @@ npx @relvehq/agent-ready check https://your-site.com
|
|
|
265
265
|
|
|
266
266
|
Most CDNs do not put `Accept` in the cache key. If the CDN already holds the HTML for a page, an `Accept: text/markdown` request may get that HTML. That is safe, just not Markdown. `.md` URLs and `/llms.txt` always work, because they are their own URLs. For reliable `Accept` negotiation, add the `Accept` header to the CDN cache key.
|
|
267
267
|
|
|
268
|
-
## Add refined files
|
|
268
|
+
## Add refined files: better Markdown with your own OpenAI key (optional)
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
One command writes a better Markdown file for each page ahead of time, with Relve's prompt and your own OpenAI key (BYOK).
|
|
271
271
|
|
|
272
|
-
1.
|
|
273
|
-
|
|
272
|
+
1. Put your key in `.env.local`, `.env` or the shell as `OPENAI_API_KEY`, then run from your project root:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
npx agent-ready refine --site https://your-site.com --sitemap --dry-run
|
|
276
|
+
npx agent-ready refine --site https://your-site.com --sitemap
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`--dry-run` lists the pages and calls nothing. The real run fetches each page's HTML and sends it to the Relve API on relvehq.com with your key. Relve's prompt runs on Relve's server, calls OpenAI with your key, checks the result (every number and URL must be on the page) and returns the Markdown. The files land in `.agent-ready/md`: `index.md`, `pricing.md`, each with a `.json` next to it (fingerprint, model, tokens, date).
|
|
280
|
+
|
|
281
|
+
- What it costs: your own OpenAI usage, one model call per changed page. Relve charges nothing.
|
|
282
|
+
- Your key: it passes through Relve's server for that call only. Relve never stores or logs it, and it is never written to the output files.
|
|
283
|
+
- Options: `--paths /a,/b` instead of `--sitemap`, `--model gpt-4o` (also `gpt-4.1`, `gpt-4.1-mini`; default `gpt-4o-mini`, the fastest), `--site-name "Your Brand"`, `--limit 20`, `--out <dir>`, `--force`.
|
|
284
|
+
- The run ends with a summary (written, unchanged, rejected, errors, tokens). Rejected or failed pages are listed with a code (for example `too_little_text`) and nothing is saved for them; they keep the plain `convert` Markdown.
|
|
285
|
+
- Re-run it when your content changes. Unchanged pages are skipped with no model call, so they are free. You can add the command to CI or your deploy (before the build, with `OPENAI_API_KEY` as a secret).
|
|
286
|
+
|
|
287
|
+
Advanced: your own prompts. `--prompts <dir>` (or `AGENT_READY_PROMPTS`) runs your own prompt files on your machine instead of calling the Relve API. See the package README.
|
|
288
|
+
|
|
289
|
+
2. Commit the folder at `.agent-ready/md` in your project root (or generate it in your deploy).
|
|
290
|
+
3. Tell the package to read it. The store reads files with Node, so put it where only Node code runs.
|
|
274
291
|
|
|
275
292
|
Next.js: in the route file (`app/%5F%5Fmd/[[...path]]/route.ts`), not in `agent-ready.config.ts`. The config file is also loaded by the middleware, which runs on the Edge runtime and has no file system.
|
|
276
293
|
|
|
@@ -297,7 +314,7 @@ import { fileStore } from "@relvehq/agent-ready/refine/file-store";
|
|
|
297
314
|
app.use(agentReadyNode(createAgentReady({ site: "https://your-site.com", store: fileStore(".agent-ready/md") })));
|
|
298
315
|
```
|
|
299
316
|
|
|
300
|
-
|
|
317
|
+
4. Redeploy. Check a refined page:
|
|
301
318
|
|
|
302
319
|
```
|
|
303
320
|
curl -i https://your-site.com/pricing.md
|
|
@@ -305,7 +322,7 @@ curl -i https://your-site.com/pricing.md
|
|
|
305
322
|
|
|
306
323
|
The response should carry `x-agent-ready-source: stored`. Pages without a refined file keep working as before (`convert`).
|
|
307
324
|
|
|
308
|
-
WordPress (Path 5): no folder to commit.
|
|
325
|
+
WordPress (Path 5): no folder to commit. Use the plugin's OpenAI key setting or upload a zip (see "Refined files on WordPress"), then run the same `curl` on a refined post's `.md` URL. Pages without a refined file keep `x-agent-ready-source: native`.
|
|
309
326
|
|
|
310
327
|
## Turn it off
|
|
311
328
|
|
package/dist/bin/agent-ready.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// agent-ready init [--stack next|node|wordpress] [--dry-run] [--force] [--cwd <dir>] [--pkg <name>]
|
|
6
6
|
// agent-ready check <url> [--json]
|
|
7
7
|
// agent-ready stats --log <hits.jsonl> [--days <n>] [--json]
|
|
8
|
-
// agent-ready refine --site <origin> (--paths a,b | --sitemap) --
|
|
8
|
+
// agent-ready refine --site <origin> (--paths a,b | --sitemap) [--model <id>]
|
|
9
9
|
// [--site-name <name>] [--out <dir>] [--limit <n>] [--force]
|
|
10
|
-
// [--concurrency <n>] [--dry-run]
|
|
10
|
+
// [--concurrency <n>] [--dry-run] [--prompts <dir>[,<dir>]]
|
|
11
11
|
//
|
|
12
|
-
// The package ships no prompt. refine
|
|
13
|
-
//
|
|
12
|
+
// The package ships no prompt. By default refine sends each page to the Relve
|
|
13
|
+
// API (refine/remote.ts) with the caller's OPENAI_API_KEY; Relve's prompt runs
|
|
14
|
+
// there. With --prompts or AGENT_READY_PROMPTS it runs those prompt files
|
|
15
|
+
// locally instead (the original mode).
|
|
14
16
|
//
|
|
15
17
|
// Thin: argument parsing and the real file system; the work is in cli/init.ts
|
|
16
18
|
// and refine/*. NODE ONLY.
|
|
@@ -62,16 +64,19 @@ const USAGE = [
|
|
|
62
64
|
" detects Next.js, Node servers (Express, Fastify, Nest, Nitro...) and WordPress;",
|
|
63
65
|
" --stack skips detection, --force replaces an installed WordPress plugin",
|
|
64
66
|
` guide: ${init_1.ONBOARDING_URL}`,
|
|
65
|
-
" agent-ready refine --site <origin> (--paths /a,/b | --sitemap) --
|
|
67
|
+
" agent-ready refine --site <origin> (--paths /a,/b | --sitemap) [--model gpt-4o|gpt-4o-mini|gpt-4.1|gpt-4.1-mini]",
|
|
66
68
|
" [--site-name <name>] [--out <dir>] [--limit <n>] [--force] [--concurrency <n>] [--dry-run]",
|
|
69
|
+
" better Markdown files in .agent-ready/md, written with Relve's prompt and your own OpenAI key",
|
|
67
70
|
"",
|
|
68
71
|
" agent-ready check <url> [--json] how agent-ready is a site? (read-only)",
|
|
69
72
|
" agent-ready stats --log <hits.jsonl> [--days <n>] [--json] agent visits summary",
|
|
70
73
|
"",
|
|
71
|
-
"refine needs
|
|
74
|
+
"refine needs OPENAI_API_KEY (read from .env.local / .env or the shell) unless --dry-run. Each page goes to",
|
|
75
|
+
"relvehq.com with the key; the key is used for that call only and never stored. Unchanged pages are free.",
|
|
76
|
+
"AGENT_READY_REFINE_ENDPOINT overrides the API URL.",
|
|
72
77
|
"",
|
|
73
|
-
"
|
|
74
|
-
"AGENT_READY_MODEL picks the model (default gpt-4o
|
|
78
|
+
"Advanced: --prompts <dir>[,<dir>] (or AGENT_READY_PROMPTS) runs your own prompt files locally instead, with",
|
|
79
|
+
"OPENAI_API_KEY or ANTHROPIC_API_KEY; AGENT_READY_MODEL picks the model (default gpt-4o / claude-opus-5-5).",
|
|
75
80
|
].join("\n");
|
|
76
81
|
function nodeFs(root) {
|
|
77
82
|
const abs = (p) => (0, node_path_1.resolve)(root, p);
|
|
@@ -215,11 +220,13 @@ async function refineCommand(rest, flag, d) {
|
|
|
215
220
|
// Several folders may be given ("a,b"). Checked before any page is fetched.
|
|
216
221
|
const promptSource = flag("--prompts") ?? env.AGENT_READY_PROMPTS;
|
|
217
222
|
const promptDirs = (promptSource ?? "").split(",").map((p) => p.trim()).filter(Boolean);
|
|
218
|
-
if (!promptDirs.length) {
|
|
219
|
-
log("agent-ready refine:
|
|
220
|
-
" or AGENT_READY_PROMPTS (in .env.local, .env or the shell). Each file needs ## SYSTEM, ## USER and ## Settings.");
|
|
223
|
+
if (rest.includes("--prompts") && !promptDirs.length) {
|
|
224
|
+
log("agent-ready refine: --prompts needs a folder of prompt files (--prompts <dir>[,<dir>]).");
|
|
221
225
|
return 1;
|
|
222
226
|
}
|
|
227
|
+
// No prompt folder: Relve's prompt, run on relvehq.com with the caller's key.
|
|
228
|
+
if (!promptDirs.length)
|
|
229
|
+
return remoteRefineCommand(rest, flag, site, env, f, log);
|
|
223
230
|
if (!flag("--prompts"))
|
|
224
231
|
log(`agent-ready refine: prompts from AGENT_READY_PROMPTS (${promptDirs.join(", ")})`);
|
|
225
232
|
const siteName = flag("--site-name");
|
|
@@ -319,6 +326,93 @@ async function refineCommand(rest, flag, d) {
|
|
|
319
326
|
log(`model usage: ${report.usage.inputTokens} input + ${report.usage.outputTokens} output tokens`);
|
|
320
327
|
return c.error || c.rejected ? 2 : 0;
|
|
321
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* `refine` without --prompts: each page's HTML goes to the Relve API with the
|
|
331
|
+
* caller's OPENAI_API_KEY; Relve's prompt runs there. Checks happen before any
|
|
332
|
+
* fetch; the key is never printed.
|
|
333
|
+
*/
|
|
334
|
+
async function remoteRefineCommand(rest, flag, site, env, f, log) {
|
|
335
|
+
const { refineRemote, stopMessage, savedFingerprint, DEFAULT_REFINE_ENDPOINT, REFINE_MODELS } = await Promise.resolve().then(() => __importStar(require("../refine/remote")));
|
|
336
|
+
const dryRun = rest.includes("--dry-run");
|
|
337
|
+
const model = flag("--model");
|
|
338
|
+
if (model !== undefined && !REFINE_MODELS.includes(model)) {
|
|
339
|
+
log(`agent-ready refine: --model must be one of ${REFINE_MODELS.join(", ")}`);
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
const endpoint = env.AGENT_READY_REFINE_ENDPOINT?.trim() || DEFAULT_REFINE_ENDPOINT;
|
|
343
|
+
if (!/^https?:\/\//.test(endpoint)) {
|
|
344
|
+
log("agent-ready refine: AGENT_READY_REFINE_ENDPOINT must be an http(s) URL.");
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
const apiKey = env.OPENAI_API_KEY?.trim();
|
|
348
|
+
if (!apiKey && !dryRun) {
|
|
349
|
+
log("agent-ready refine: set OPENAI_API_KEY (in .env.local, .env or the shell) to your OpenAI key, or use --dry-run." +
|
|
350
|
+
" Advanced: --prompts <dir> runs your own prompt files locally.");
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
let paths = [];
|
|
354
|
+
if (flag("--paths"))
|
|
355
|
+
paths = flag("--paths").split(",").map((p) => p.trim()).filter(Boolean);
|
|
356
|
+
else if (rest.includes("--sitemap")) {
|
|
357
|
+
const { sitemapEntries } = await Promise.resolve().then(() => __importStar(require("../core/sitemap")));
|
|
358
|
+
const entries = await sitemapEntries({ fetch: f, limit: 5000 })({ request: new Request(site + "/llms.txt") });
|
|
359
|
+
paths = entries.map((e) => e.url);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
log(USAGE);
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
paths = paths.filter((p) => p.startsWith("/"));
|
|
366
|
+
const limit = Number(flag("--limit") ?? "0");
|
|
367
|
+
if (limit > 0)
|
|
368
|
+
paths = paths.slice(0, limit);
|
|
369
|
+
if (!paths.length) {
|
|
370
|
+
log("agent-ready refine: no pages (check --paths, or that the site has a /sitemap.xml).");
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
const outDir = (0, node_path_1.resolve)(flag("--out") ?? ".agent-ready/md");
|
|
374
|
+
const force = rest.includes("--force");
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
// No API call, no page fetch, nothing written.
|
|
377
|
+
log(`agent-ready refine --dry-run: ${paths.length} page(s) would go to ${endpoint}${model ? ` (model ${model})` : ""}`);
|
|
378
|
+
for (const path of paths) {
|
|
379
|
+
const saved = force ? null : await savedFingerprint(outDir, path);
|
|
380
|
+
log(` ${(saved ? "would check" : "would send").padEnd(11)} ${path}${saved ? " (saved file; free if unchanged)" : ""}`);
|
|
381
|
+
}
|
|
382
|
+
if (!apiKey)
|
|
383
|
+
log("Set OPENAI_API_KEY to run it for real.");
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
log("Rewriting with Relve's prompt via relvehq.com using your OpenAI key (the key is used for each call only; Relve does not store it).");
|
|
387
|
+
const report = await refineRemote({
|
|
388
|
+
site,
|
|
389
|
+
paths,
|
|
390
|
+
apiKey: apiKey,
|
|
391
|
+
outDir,
|
|
392
|
+
endpoint,
|
|
393
|
+
model,
|
|
394
|
+
siteName: flag("--site-name"),
|
|
395
|
+
force,
|
|
396
|
+
concurrency: Number(flag("--concurrency") ?? "2") || 2,
|
|
397
|
+
fetch: f,
|
|
398
|
+
onResult: (it) => {
|
|
399
|
+
const extra = it.status === "ok"
|
|
400
|
+
? ` ${it.tokens ?? 0} tokens${it.kind ? ` kind: ${it.kind}` : ""}`
|
|
401
|
+
: it.status === "rejected"
|
|
402
|
+
? ` ${(it.issues ?? []).slice(0, 5).join("; ")}`
|
|
403
|
+
: it.status === "error"
|
|
404
|
+
? ` ${it.code}${it.message ? `: ${it.message}` : ""}`
|
|
405
|
+
: "";
|
|
406
|
+
log(` ${it.status.padEnd(10)} ${it.path}${extra}`);
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const c = report.counts;
|
|
410
|
+
if (report.stopped)
|
|
411
|
+
log(`agent-ready refine: stopped early. ${stopMessage(report.stopped)}`);
|
|
412
|
+
log(`done: ${c.ok} written, ${c.unchanged} unchanged, ${c.rejected} rejected, ${c.error} errors${c.skipped ? `, ${c.skipped} not run` : ""} -> ${report.dir}`);
|
|
413
|
+
log(`tokens: ${report.tokens} in the written files (OpenAI bills its usage for each call to your account)`);
|
|
414
|
+
return c.error || c.rejected || report.stopped ? 1 : 0;
|
|
415
|
+
}
|
|
322
416
|
/**
|
|
323
417
|
* Read `.env.local` then `.env` from the working directory, like Next.js, so
|
|
324
418
|
* the keys a project already has (OPENAI_API_KEY, ANTHROPIC_API_KEY) just
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.3.0";
|
|
2
|
+
export declare const DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
|
|
3
|
+
export declare const REFINE_MODELS: readonly ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
|
|
4
|
+
export declare const USER_AGENT = "agent-ready/0.3.0 (+https://relvehq.com/agent-ready)";
|
|
5
|
+
export type RemoteStatus = "ok" | "unchanged" | "rejected" | "error" | "skipped";
|
|
6
|
+
export interface RemoteItem {
|
|
7
|
+
path: string;
|
|
8
|
+
status: RemoteStatus;
|
|
9
|
+
/** Error code from the contract (e.g. "openai_auth", "too_little_text"), or a local one ("page_fetch", "network", "timeout", "bad_response"). */
|
|
10
|
+
code?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
/** Check messages on "rejected". */
|
|
13
|
+
issues?: string[];
|
|
14
|
+
tokens?: number;
|
|
15
|
+
kind?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
saved: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface RemoteReport {
|
|
20
|
+
items: RemoteItem[];
|
|
21
|
+
counts: Record<RemoteStatus, number>;
|
|
22
|
+
tokens: number;
|
|
23
|
+
/** The error code that stopped the run early, or null. */
|
|
24
|
+
stopped: string | null;
|
|
25
|
+
dir: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RemoteOptions {
|
|
28
|
+
/** Public origin, e.g. "https://your-site.com". */
|
|
29
|
+
site: string;
|
|
30
|
+
paths: string[];
|
|
31
|
+
/** The caller's OpenAI key. Sent in X-OpenAI-Key, nothing else. */
|
|
32
|
+
apiKey: string;
|
|
33
|
+
/** Output folder. Default ".agent-ready/md". */
|
|
34
|
+
outDir?: string;
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
siteName?: string;
|
|
38
|
+
/** Ignore saved fingerprints. */
|
|
39
|
+
force?: boolean;
|
|
40
|
+
/** Pages at a time. Default 2. */
|
|
41
|
+
concurrency?: number;
|
|
42
|
+
fetch?: typeof fetch;
|
|
43
|
+
/** Relve API timeout. Default 90 s. */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
/** Page fetch timeout. Default 30 s. */
|
|
46
|
+
pageTimeoutMs?: number;
|
|
47
|
+
/** Wait before retrying a 502/504. Default 2 s. */
|
|
48
|
+
retryDelayMs?: number;
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
sleep?: (ms: number) => Promise<void>;
|
|
51
|
+
onResult?: (item: RemoteItem) => void;
|
|
52
|
+
}
|
|
53
|
+
/** The saved JSON's fingerprint, else one built from a local run's sourceHash, else null. */
|
|
54
|
+
export declare function savedFingerprint(outDir: string, path: string): Promise<string | null>;
|
|
55
|
+
export declare function refineRemote(opts: RemoteOptions): Promise<RemoteReport>;
|
|
56
|
+
/** What a stop code means, for the CLI. */
|
|
57
|
+
export declare function stopMessage(code: string): string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Refine through the Relve API, with the caller's own OpenAI key (BYOK).
|
|
3
|
+
// changes/agent-ready-byok-refine/contract.md is the wire format.
|
|
4
|
+
//
|
|
5
|
+
// await refineRemote({ site, paths, apiKey, outDir })
|
|
6
|
+
//
|
|
7
|
+
// For each page: fetch its HTML here, read the saved fingerprint from
|
|
8
|
+
// `<out>/<path>.json`, POST { url, html, siteName?, model?, previousFingerprint? }
|
|
9
|
+
// to the endpoint with the key in `X-OpenAI-Key`, and save `ok` results as
|
|
10
|
+
// `<out>/<path>.md` + `<out>/<path>.json` through fileStore, so the files are
|
|
11
|
+
// served exactly like the ones local refine writes. Relve's prompt runs on
|
|
12
|
+
// Relve's server; it never reaches this machine. The key is sent with each
|
|
13
|
+
// call and is never written, logged or put in an error message here.
|
|
14
|
+
//
|
|
15
|
+
// NODE ONLY (fileStore, node:fs).
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.USER_AGENT = exports.REFINE_MODELS = exports.DEFAULT_REFINE_ENDPOINT = exports.PACKAGE_VERSION = void 0;
|
|
51
|
+
exports.savedFingerprint = savedFingerprint;
|
|
52
|
+
exports.refineRemote = refineRemote;
|
|
53
|
+
exports.stopMessage = stopMessage;
|
|
54
|
+
const file_store_1 = require("./file-store");
|
|
55
|
+
exports.PACKAGE_VERSION = "0.3.0";
|
|
56
|
+
exports.DEFAULT_REFINE_ENDPOINT = "https://relvehq.com/api/agent-ready/v1/refine";
|
|
57
|
+
exports.REFINE_MODELS = ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini"];
|
|
58
|
+
exports.USER_AGENT = `agent-ready/${exports.PACKAGE_VERSION} (+https://relvehq.com/agent-ready)`;
|
|
59
|
+
/** Errors that mean every later call would fail the same way: the run stops. */
|
|
60
|
+
const FATAL = new Set(["openai_auth", "openai_quota", "missing_key"]);
|
|
61
|
+
/** The saved JSON's fingerprint, else one built from a local run's sourceHash, else null. */
|
|
62
|
+
async function savedFingerprint(outDir, path) {
|
|
63
|
+
try {
|
|
64
|
+
const { readFile } = await Promise.resolve().then(() => __importStar(require("node:fs/promises")));
|
|
65
|
+
const meta = JSON.parse(await readFile(metaFile(outDir, path), "utf8"));
|
|
66
|
+
if (typeof meta.fingerprint === "string" && meta.fingerprint)
|
|
67
|
+
return meta.fingerprint;
|
|
68
|
+
if (typeof meta.sourceHash === "string" && meta.sourceHash)
|
|
69
|
+
return meta.sourceHash.startsWith("sha256:") ? meta.sourceHash : `sha256:${meta.sourceHash}`;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** `<out>/<path>.json`, the same name fileStore uses. */
|
|
77
|
+
function metaFile(outDir, path) {
|
|
78
|
+
const clean = path.replace(/^\/+|\/+$/g, "");
|
|
79
|
+
return `${outDir.replace(/[\\/]+$/, "")}/${clean || "index"}.json`;
|
|
80
|
+
}
|
|
81
|
+
async function refineRemote(opts) {
|
|
82
|
+
const site = opts.site.replace(/\/+$/, "");
|
|
83
|
+
const dir = opts.outDir ?? ".agent-ready/md";
|
|
84
|
+
const store = (0, file_store_1.fileStore)(dir);
|
|
85
|
+
const f = opts.fetch ?? fetch;
|
|
86
|
+
const endpoint = opts.endpoint ?? exports.DEFAULT_REFINE_ENDPOINT;
|
|
87
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
88
|
+
const now = opts.now ?? (() => new Date());
|
|
89
|
+
const items = opts.paths.map((path) => ({ path, status: "skipped", saved: false }));
|
|
90
|
+
let next = 0;
|
|
91
|
+
let stopped = null;
|
|
92
|
+
const one = async (path) => {
|
|
93
|
+
const url = site + path;
|
|
94
|
+
let html;
|
|
95
|
+
try {
|
|
96
|
+
html = await fetchPage(f, url, opts.pageTimeoutMs ?? 30000);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return { path, status: "error", code: "page_fetch", message: err.message, saved: false };
|
|
100
|
+
}
|
|
101
|
+
const previousFingerprint = opts.force ? null : await savedFingerprint(dir, path);
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
url,
|
|
104
|
+
html,
|
|
105
|
+
...(opts.siteName ? { siteName: opts.siteName } : {}),
|
|
106
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
107
|
+
...(previousFingerprint ? { previousFingerprint } : {}),
|
|
108
|
+
});
|
|
109
|
+
let reply = null;
|
|
110
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
111
|
+
reply = await post(f, endpoint, opts.apiKey, body, opts.timeoutMs ?? 90000);
|
|
112
|
+
if (attempt > 0)
|
|
113
|
+
break;
|
|
114
|
+
if (reply.httpStatus === 429 && reply.json?.error === "rate_limited") {
|
|
115
|
+
const after = Number(reply.json.retryAfter ?? reply.retryAfterHeader ?? 10);
|
|
116
|
+
await sleep(Math.min(Number.isFinite(after) && after >= 0 ? after : 10, 60) * 1000);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (reply.httpStatus === 502 || reply.httpStatus === 504) {
|
|
120
|
+
await sleep(opts.retryDelayMs ?? 2000);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const r = reply;
|
|
126
|
+
if (r.local)
|
|
127
|
+
return { path, status: "error", code: r.local.code, message: r.local.message, saved: false };
|
|
128
|
+
const j = r.json ?? {};
|
|
129
|
+
if (r.httpStatus === 200 && j.status === "ok" && typeof j.markdown === "string" && typeof j.fingerprint === "string") {
|
|
130
|
+
const stamp = now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
131
|
+
const tokens = Number(j.tokens) || 0;
|
|
132
|
+
const doc = {
|
|
133
|
+
path,
|
|
134
|
+
url,
|
|
135
|
+
markdown: j.markdown,
|
|
136
|
+
// sourceHash keeps local refine's freshness check working on these files.
|
|
137
|
+
sourceHash: j.fingerprint.replace(/^sha256:/, ""),
|
|
138
|
+
promptId: String(j.prompt ?? "page-v1"),
|
|
139
|
+
generatedAt: stamp,
|
|
140
|
+
tokens,
|
|
141
|
+
fingerprint: j.fingerprint,
|
|
142
|
+
model: j.model ?? opts.model ?? null,
|
|
143
|
+
prompt: j.prompt ?? null,
|
|
144
|
+
kind: j.kind ?? null,
|
|
145
|
+
refinedAt: stamp,
|
|
146
|
+
source: "relve-api",
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
await store.save(doc);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return { path, status: "error", code: "write", message: err.message, saved: false };
|
|
153
|
+
}
|
|
154
|
+
return { path, status: "ok", tokens, kind: j.kind ?? undefined, model: j.model ?? undefined, saved: true };
|
|
155
|
+
}
|
|
156
|
+
if (r.httpStatus === 200 && j.status === "unchanged")
|
|
157
|
+
return { path, status: "unchanged", saved: false };
|
|
158
|
+
if (j.status === "rejected") {
|
|
159
|
+
return { path, status: "rejected", code: "rejected", issues: Array.isArray(j.issues) ? j.issues.map(String) : [], saved: false };
|
|
160
|
+
}
|
|
161
|
+
const code = typeof j.error === "string" ? j.error : r.httpStatus === 200 ? "bad_response" : `http_${r.httpStatus}`;
|
|
162
|
+
return { path, status: "error", code, message: typeof j.message === "string" ? j.message : undefined, saved: false };
|
|
163
|
+
};
|
|
164
|
+
const worker = async () => {
|
|
165
|
+
while (!stopped && next < opts.paths.length) {
|
|
166
|
+
const i = next++;
|
|
167
|
+
const item = await one(opts.paths[i]);
|
|
168
|
+
items[i] = item;
|
|
169
|
+
if (item.status === "error" && item.code && FATAL.has(item.code))
|
|
170
|
+
stopped = item.code;
|
|
171
|
+
opts.onResult?.(item);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(opts.concurrency ?? 2, opts.paths.length || 1)) }, worker));
|
|
175
|
+
const counts = { ok: 0, unchanged: 0, rejected: 0, error: 0, skipped: 0 };
|
|
176
|
+
let tokens = 0;
|
|
177
|
+
for (const it of items) {
|
|
178
|
+
counts[it.status]++;
|
|
179
|
+
tokens += it.tokens ?? 0;
|
|
180
|
+
}
|
|
181
|
+
return { items, counts, tokens, stopped, dir: store.dir };
|
|
182
|
+
}
|
|
183
|
+
/** What a stop code means, for the CLI. */
|
|
184
|
+
function stopMessage(code) {
|
|
185
|
+
if (code === "openai_auth")
|
|
186
|
+
return "OpenAI rejected the key in OPENAI_API_KEY. Check it at https://platform.openai.com/api-keys and run again.";
|
|
187
|
+
if (code === "openai_quota")
|
|
188
|
+
return "Your OpenAI account is out of quota or has no billing set up. Check https://platform.openai.com/settings/organization/billing and run again.";
|
|
189
|
+
if (code === "missing_key")
|
|
190
|
+
return "The Relve API got no key. Set OPENAI_API_KEY and run again.";
|
|
191
|
+
return code;
|
|
192
|
+
}
|
|
193
|
+
async function post(f, endpoint, apiKey, body, timeoutMs) {
|
|
194
|
+
const ctl = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
196
|
+
try {
|
|
197
|
+
const res = await f(endpoint, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "content-type": "application/json", accept: "application/json", "x-openai-key": apiKey, "user-agent": exports.USER_AGENT },
|
|
200
|
+
body,
|
|
201
|
+
signal: ctl.signal,
|
|
202
|
+
});
|
|
203
|
+
const text = await res.text();
|
|
204
|
+
let json = null;
|
|
205
|
+
try {
|
|
206
|
+
json = JSON.parse(text);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { httpStatus: res.status, json: null, retryAfterHeader: null, local: { code: res.ok ? "bad_response" : `http_${res.status}`, message: "the Relve API did not answer with JSON" } };
|
|
210
|
+
}
|
|
211
|
+
return { httpStatus: res.status, json, retryAfterHeader: res.headers.get("retry-after") };
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
const aborted = err?.name === "AbortError" || ctl.signal.aborted;
|
|
215
|
+
return {
|
|
216
|
+
httpStatus: 0,
|
|
217
|
+
json: null,
|
|
218
|
+
retryAfterHeader: null,
|
|
219
|
+
local: aborted ? { code: "timeout", message: `no answer from the Relve API in ${Math.round(timeoutMs / 1000)} s` } : { code: "network", message: err?.message ?? "request failed" },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function fetchPage(f, url, timeoutMs) {
|
|
227
|
+
const ctl = new AbortController();
|
|
228
|
+
const timer = setTimeout(() => ctl.abort(), timeoutMs);
|
|
229
|
+
try {
|
|
230
|
+
const res = await f(url, { headers: { accept: "text/html", "user-agent": exports.USER_AGENT }, redirect: "follow", signal: ctl.signal });
|
|
231
|
+
if (!res.ok)
|
|
232
|
+
throw new Error(`HTTP ${res.status}`);
|
|
233
|
+
return await res.text();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
}
|