@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 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 prompt files): an AI model writes a
12
- better Markdown file for each page ahead of time, a script checks every
13
- number and URL against the page,
14
- and unchanged pages are skipped on the next run.
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, written ahead of time
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 --prompts ./prompts --sitemap --limit 20 --dry-run
175
- npx agent-ready refine --site https://your-site.com --prompts ./prompts --site-name "Your Name" --sitemap --limit 20
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: fetch it, extract its text, links, JSON-LD and a fingerprint;
179
- skip it if the fingerprint matches the saved file; otherwise fill the prompt,
180
- call the model once, check the result, retry once with the problems listed,
181
- and save `<out>/<path>.md` plus `<out>/<path>.json` (default out:
182
- `.agent-ready/md`). `--dry-run` only reads pages: no model call, nothing written.
183
-
184
- | Setting | |
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
- | `OPENAI_API_KEY` | OpenAI, default model `gpt-4o` |
187
- | `ANTHROPIC_API_KEY` | Claude, default model `claude-opus-5-5` |
188
- | `AGENT_READY_MODEL` | any other model id, e.g. `gpt-4o-mini` |
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
- Keys are read from `.env.local` and `.env`. Serve the files from the route
191
- file (`fileStore` is Node only, so keep it out of `agent-ready.config.ts`,
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
- **The prompt.** The package ships none. Point `refine` at a folder of prompt
213
- files with `--prompts <dir>` (several: `a,b`) or the `AGENT_READY_PROMPTS`
214
- variable (`.env.local`, `.env` or the shell); without one it stops before
215
- fetching anything. A prompt file is Markdown with `## SYSTEM`, `## USER`
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) --prompts <dir> [--site-name <name>]
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
- Relve sends a zip of refined files (`refined-md.zip`). On Settings > Agent Ready, under "Refined files from Relve", choose it and click "Upload zip" (up to 20 MB, 2 MB per file). The page shows how many files are stored. Each upload replaces the whole stored set, so always upload the latest zip Relve sends; "Clear refined files" removes them all. See "Add refined files from Relve" below for how to check it.
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 from Relve (optional)
268
+ ## Add refined files: better Markdown with your own OpenAI key (optional)
269
269
 
270
- Relve can write a better Markdown file for each page ahead of time and send you a folder of `.md` and `.json` files.
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. Commit the folder at `.agent-ready/md` in your project root. Example: `.agent-ready/md/index.md`, `.agent-ready/md/pricing.md`.
273
- 2. Tell the package to read it. The store reads files with Node, so put it where only Node code runs.
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
- 3. Redeploy. Check a refined page:
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. Upload `refined-md.zip` on Settings > Agent Ready (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`.
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
 
@@ -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) --prompts <dir>[,<dir>]
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 reads the folders in --prompts, else in
13
- // AGENT_READY_PROMPTS, and stops before fetching anything when neither is set.
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) --prompts <dir>[,<dir>]",
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 a folder of prompt files: --prompts, or AGENT_READY_PROMPTS. The package ships none.",
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
- "refine needs OPENAI_API_KEY or ANTHROPIC_API_KEY (read from .env.local / .env) unless --dry-run.",
74
- "AGENT_READY_MODEL picks the model (default gpt-4o with OpenAI, claude-opus-5-5 with Claude).",
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: no prompts. The package ships none: name a folder of prompt files with --prompts <dir>[,<dir>]" +
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
+ }