@relvehq/agent-ready 0.2.1 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  import type { PageInput } from "./types";
2
2
  import type { PromptSettings } from "./prompts";
3
- export type IssueKind = "number" | "url" | "frontmatter" | "hash" | "section" | "h1" | "html" | "placeholder" | "tokens" | "empty";
3
+ export type IssueKind = "number" | "url" | "frontmatter" | "hash" | "section" | "h1" | "html" | "placeholder" | "tokens" | "empty" | "plan";
4
4
  export interface CheckIssue {
5
5
  kind: IssueKind;
6
6
  detail: string;
@@ -15,3 +15,5 @@ export interface CheckOptions {
15
15
  countTokens?: (text: string) => number;
16
16
  }
17
17
  export declare function checkDoc(doc: string, input: PageInput, settings: PromptSettings, opts?: CheckOptions): CheckResult;
18
+ /** First-column names of the tables under "## Pricing" / "## Plans" headings. */
19
+ export declare function planNames(prose: string): string[];
@@ -12,6 +12,9 @@
12
12
  // h1 not exactly one H1
13
13
  // html HTML tags in the document
14
14
  // placeholder a {{PLACEHOLDER}} or <Template field> left in
15
+ // plan a plan named in a "## Pricing" / "## Plans" table that the
16
+ // input never names (e.g. a made-up "Basic" built from the
17
+ // prices of other products shown on the page; 2026-10-29)
15
18
  // tokens over the prompt's token budget
16
19
  // empty no real body
17
20
  //
@@ -20,6 +23,7 @@
20
23
  // EDGE-SAFE (pure). It cannot catch a reworded claim; people still read.
21
24
  Object.defineProperty(exports, "__esModule", { value: true });
22
25
  exports.checkDoc = checkDoc;
26
+ exports.planNames = planNames;
23
27
  const enrich_1 = require("../core/enrich");
24
28
  const URL_RE = /https?:\/\/[^\s<>()[\]|"'`]+/g;
25
29
  const NUM_RE = /\d+(?:[.,]\d+)*/g;
@@ -100,8 +104,47 @@ function checkDoc(doc, input, settings, opts = {}) {
100
104
  seen.add(c);
101
105
  add("number", n);
102
106
  }
107
+ // Plans: every plan a Pricing / Plans table names must be named in the
108
+ // input. The number check alone passes a plan stitched together from
109
+ // prices that are on the page but belong to something else.
110
+ for (const name of planNames(prose)) {
111
+ if (!namedIn(haystack, name))
112
+ add("plan", name);
113
+ }
103
114
  return { ok: issues.length === 0, issues, tokens };
104
115
  }
116
+ /** First-column names of the tables under "## Pricing" / "## Plans" headings. */
117
+ function planNames(prose) {
118
+ const names = [];
119
+ for (const section of prose.split(/^(?=##\s)/m)) {
120
+ const heading = /^##\s+(.+)$/m.exec(section);
121
+ if (!heading || !/^(pricing|plans)\b/i.test(heading[1].trim()))
122
+ continue;
123
+ let header = true;
124
+ for (const line of section.split("\n")) {
125
+ if (!line.trim().startsWith("|")) {
126
+ header = true; // a new table starts with its own header row
127
+ continue;
128
+ }
129
+ if (/^\|[\s:|-]+\|?\s*$/.test(line.trim()))
130
+ continue; // separator row
131
+ if (header) {
132
+ header = false;
133
+ continue;
134
+ }
135
+ const cell = line.trim().replace(/^\|/, "").split("|")[0] ?? "";
136
+ const name = cell.replace(/[*_`]/g, "").replace(/\s+/g, " ").trim();
137
+ if (name)
138
+ names.push(name);
139
+ }
140
+ }
141
+ return names;
142
+ }
143
+ /** The name appears in the text as a whole word or phrase, ignoring case and spacing. */
144
+ function namedIn(text, name) {
145
+ const words = name.split(" ").map(escapeRe).join("\\s+");
146
+ return new RegExp(`(?<![A-Za-z0-9])${words}(?![A-Za-z0-9])`, "i").test(text);
147
+ }
105
148
  /** "1,400" = "1400", "0.10" = "0.1", "05" = "5". */
106
149
  function canonNum(s) {
107
150
  const plain = s.replace(/,(?=\d{3}\b)/g, "");
@@ -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
+ }