polite-crawler 0.1.3 → 0.1.4
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 +22 -0
- package/build/{cli-daemon--cTmq0Z5.mjs → cli-daemon-ZYRdy6EW.mjs} +3 -3
- package/build/{cli-daemon--cTmq0Z5.mjs.map → cli-daemon-ZYRdy6EW.mjs.map} +1 -1
- package/build/cli-recipes-Bci6HREA.mjs +59 -0
- package/build/cli-recipes-Bci6HREA.mjs.map +1 -0
- package/build/cli.mjs +397 -18
- package/build/cli.mjs.map +1 -1
- package/build/{daemon-fZTE24P5.mjs → daemon-DK6U51_v.mjs} +3 -3
- package/build/{daemon-fZTE24P5.mjs.map → daemon-DK6U51_v.mjs.map} +1 -1
- package/build/{daemon-DP4EqjFL.d.mts → daemon-wwIWsUmU.d.mts} +7 -2
- package/build/{diff-NQcQqKe9.mjs → diff-BUM91Rev.mjs} +2 -2
- package/build/{diff-NQcQqKe9.mjs.map → diff-BUM91Rev.mjs.map} +1 -1
- package/build/{extract-ByFfc8Zk.mjs → extract-CefB5Vfn.mjs} +2 -2
- package/build/{extract-ByFfc8Zk.mjs.map → extract-CefB5Vfn.mjs.map} +1 -1
- package/build/index.d.mts +2 -2
- package/build/index.mjs +10 -894
- package/build/mcp.d.mts +1 -1
- package/build/mcp.mjs +4 -4
- package/build/{open-C4fhXJgD.mjs → open-CbpSCEZc.mjs} +12 -284
- package/build/open-CbpSCEZc.mjs.map +1 -0
- package/build/recipe-GiFQojmo.mjs +279 -0
- package/build/recipe-GiFQojmo.mjs.map +1 -0
- package/build/recipe-product-Dellqudu.mjs +892 -0
- package/build/recipe-product-Dellqudu.mjs.map +1 -0
- package/build/{runtime-JToRO63o.mjs → runtime-D_r2SYge.mjs} +2 -2
- package/build/{runtime-JToRO63o.mjs.map → runtime-D_r2SYge.mjs.map} +1 -1
- package/build/{version-ntbY-6wh.mjs → version-BPem-XZd.mjs} +2 -2
- package/build/{version-ntbY-6wh.mjs.map → version-BPem-XZd.mjs.map} +1 -1
- package/build/{wacz-BFNG42Kt.mjs → wacz-QyzLp4mC.mjs} +3 -3
- package/build/{wacz-BFNG42Kt.mjs.map → wacz-QyzLp4mC.mjs.map} +1 -1
- package/build/{warc-CVS0t1fW.mjs → warc-BHcaRKHw.mjs} +2 -2
- package/build/{warc-CVS0t1fW.mjs.map → warc-BHcaRKHw.mjs.map} +1 -1
- package/build/webbotauth-CjJdTAfE.mjs.map +1 -1
- package/package.json +1 -1
- package/build/index.mjs.map +0 -1
- package/build/open-C4fhXJgD.mjs.map +0 -1
package/build/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as durationMs, r as VERSION } from "./version-
|
|
2
|
+
import { i as durationMs, r as VERSION } from "./version-BPem-XZd.mjs";
|
|
3
3
|
import { t as isLoopbackHost } from "./loopback-C_QYowSf.mjs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
@@ -103,6 +103,18 @@ const OPTIONS = {
|
|
|
103
103
|
"ollama-url": { type: "string" },
|
|
104
104
|
"allow-remote-ollama": { type: "boolean" },
|
|
105
105
|
"max-docs": { type: "string" },
|
|
106
|
+
name: { type: "string" },
|
|
107
|
+
"require-name": { type: "boolean" },
|
|
108
|
+
address: { type: "string" },
|
|
109
|
+
lat: { type: "string" },
|
|
110
|
+
lon: { type: "string" },
|
|
111
|
+
osm: { type: "string" },
|
|
112
|
+
pages: { type: "string" },
|
|
113
|
+
compare: { type: "string" },
|
|
114
|
+
"on-behalf-of": {
|
|
115
|
+
type: "string",
|
|
116
|
+
multiple: true
|
|
117
|
+
},
|
|
106
118
|
...EXPORT_OPTIONS
|
|
107
119
|
};
|
|
108
120
|
/** These options belong to `daemon`, and not to `daemon status`. */
|
|
@@ -120,23 +132,34 @@ const DAEMON_RUN_OPTIONS = [
|
|
|
120
132
|
"mcp-token-file",
|
|
121
133
|
"allow-remote"
|
|
122
134
|
];
|
|
135
|
+
/** These options belong to `venue` only. */
|
|
136
|
+
const VENUE_OPTIONS = [
|
|
137
|
+
"name",
|
|
138
|
+
"require-name",
|
|
139
|
+
"address",
|
|
140
|
+
"lat",
|
|
141
|
+
"lon",
|
|
142
|
+
"osm",
|
|
143
|
+
"pages",
|
|
144
|
+
"compare"
|
|
145
|
+
];
|
|
123
146
|
/** These options belong to one command, or to the commands that the text names. */
|
|
124
147
|
const OWNER = {
|
|
125
|
-
render: "fetch crawl watch",
|
|
126
|
-
"max-age": "fetch crawl discover",
|
|
148
|
+
render: "fetch crawl watch venue product",
|
|
149
|
+
"max-age": "fetch crawl discover venue product",
|
|
127
150
|
limit: "find",
|
|
128
151
|
kind: "find embed",
|
|
129
152
|
rank: "find",
|
|
130
153
|
from: "read diff",
|
|
131
154
|
length: "read",
|
|
132
|
-
"user-provided": "read extract",
|
|
155
|
+
"user-provided": "read extract venue product",
|
|
133
156
|
"allow-user-provided": "mcp daemon",
|
|
134
157
|
...Object.fromEntries(Object.keys(EXPORT_OPTIONS).map((option) => [option, "export"])),
|
|
135
158
|
"max-urls": "discover",
|
|
136
159
|
feed: "discover",
|
|
137
160
|
"max-pages": "crawl watch",
|
|
138
161
|
"max-depth": "crawl",
|
|
139
|
-
budget: "crawl watch embed",
|
|
162
|
+
budget: "crawl watch embed venue product",
|
|
140
163
|
include: "crawl",
|
|
141
164
|
exclude: "crawl",
|
|
142
165
|
sitemaps: "crawl",
|
|
@@ -147,10 +170,10 @@ const OWNER = {
|
|
|
147
170
|
confirm: "watch",
|
|
148
171
|
scope: "watch",
|
|
149
172
|
label: "watch",
|
|
150
|
-
"archive-fallback": "fetch",
|
|
151
|
-
"archive-despite-robots": "fetch",
|
|
152
|
-
"sign-key": "fetch discover crawl watch mcp key-directory daemon",
|
|
153
|
-
"signature-agent": "fetch discover crawl watch mcp key-directory daemon",
|
|
173
|
+
"archive-fallback": "fetch venue product",
|
|
174
|
+
"archive-despite-robots": "fetch venue product",
|
|
175
|
+
"sign-key": "fetch discover crawl watch mcp key-directory daemon venue product",
|
|
176
|
+
"signature-agent": "fetch discover crawl watch mcp key-directory daemon venue product",
|
|
154
177
|
...Object.fromEntries(DAEMON_RUN_OPTIONS.map((option) => [option, "daemon"])),
|
|
155
178
|
field: "extract",
|
|
156
179
|
capture: "extract",
|
|
@@ -158,7 +181,9 @@ const OWNER = {
|
|
|
158
181
|
"embed-model": "find embed mcp",
|
|
159
182
|
"ollama-url": "find embed mcp",
|
|
160
183
|
"allow-remote-ollama": "find embed mcp",
|
|
161
|
-
"max-docs": "embed"
|
|
184
|
+
"max-docs": "embed",
|
|
185
|
+
...Object.fromEntries(VENUE_OPTIONS.map((option) => [option, "venue"])),
|
|
186
|
+
"on-behalf-of": "venue product"
|
|
162
187
|
};
|
|
163
188
|
/** These are the commands, with the smallest and the largest count of arguments. */
|
|
164
189
|
const COMMANDS = {
|
|
@@ -236,6 +261,16 @@ const COMMANDS = {
|
|
|
236
261
|
min: 0,
|
|
237
262
|
max: 0,
|
|
238
263
|
wants: "embed takes no argument: give the model with --embedder"
|
|
264
|
+
},
|
|
265
|
+
venue: {
|
|
266
|
+
min: 1,
|
|
267
|
+
max: 1,
|
|
268
|
+
wants: "venue wants the URL of the site of one venue"
|
|
269
|
+
},
|
|
270
|
+
product: {
|
|
271
|
+
min: 1,
|
|
272
|
+
max: 1,
|
|
273
|
+
wants: "product wants the URL of one product page"
|
|
239
274
|
}
|
|
240
275
|
};
|
|
241
276
|
const parse = (args) => {
|
|
@@ -692,6 +727,311 @@ const extractCode = (result) => {
|
|
|
692
727
|
const done = result.summary.verified + result.summary.normalized;
|
|
693
728
|
return done === result.summary.fields && done > 0 ? 0 : 1;
|
|
694
729
|
};
|
|
730
|
+
/** This function reads a latitude or a longitude of the command line. */
|
|
731
|
+
const coordinateOf = (name, text, limit) => {
|
|
732
|
+
const value = /^\s*-?\d+(?:\.\d+)?\s*$/.test(text) ? Number(text) : NaN;
|
|
733
|
+
if (!Number.isFinite(value) || Math.abs(value) > limit) throw new UsageError(`--${name} wants a number from -${String(limit)} to ${String(limit)}, not "${text}"`);
|
|
734
|
+
return value;
|
|
735
|
+
};
|
|
736
|
+
/** This function checks the options of `venue` and `product`, and gives the options of the recipe. */
|
|
737
|
+
const recipeLineOf = (recipe, values, rest, page) => {
|
|
738
|
+
const url = rest[0] ?? "";
|
|
739
|
+
if (!/^https?:\/\//i.test(url) || !URL.canParse(url)) throw new UsageError(`${recipe} wants an http or https URL, not "${url}"`);
|
|
740
|
+
for (const agent of values["on-behalf-of"] ?? []) if (!/^[A-Za-z0-9_-]+$/.test(agent)) throw new UsageError(`--on-behalf-of wants the robots.txt token of an agent, such as Claude-User, not "${agent}"`);
|
|
741
|
+
const name = values.name;
|
|
742
|
+
if (name !== void 0 && name.trim() === "") throw new UsageError("--name wants the name of the venue");
|
|
743
|
+
if (values["require-name"] === true && name === void 0) throw new UsageError("--require-name goes with --name");
|
|
744
|
+
const address = values.address;
|
|
745
|
+
if (address !== void 0 && address.trim() === "") throw new UsageError("--address wants a street address");
|
|
746
|
+
if (values.lat === void 0 !== (values.lon === void 0)) throw new UsageError("--lat and --lon go together");
|
|
747
|
+
const lat = values.lat === void 0 ? void 0 : coordinateOf("lat", values.lat, 90);
|
|
748
|
+
const lon = values.lon === void 0 ? void 0 : coordinateOf("lon", values.lon, 180);
|
|
749
|
+
const osm = values.osm;
|
|
750
|
+
if (osm !== void 0 && osm.trim() === "") throw new UsageError("--osm wants an opening_hours value, such as \"Mo-Fr 08:00-18:00\"");
|
|
751
|
+
const pages = values.pages === void 0 ? void 0 : whole("pages", values.pages, 0, 0);
|
|
752
|
+
if (pages !== void 0 && pages > 8) throw new UsageError(`--pages wants a whole number from 0 to ${String(8)}`);
|
|
753
|
+
const compare = values.compare;
|
|
754
|
+
if (compare !== void 0 && compare.trim() === "") throw new UsageError("--compare wants a file");
|
|
755
|
+
return {
|
|
756
|
+
recipe,
|
|
757
|
+
url,
|
|
758
|
+
render: page.render,
|
|
759
|
+
maxAge: page.maxAge,
|
|
760
|
+
budget: page.budget,
|
|
761
|
+
onBehalfOf: values["on-behalf-of"],
|
|
762
|
+
userProvided: values["user-provided"] === true,
|
|
763
|
+
fallback: values["archive-fallback"] === true ? "archive" : void 0,
|
|
764
|
+
archiveDespiteRobots: values["archive-despite-robots"] === true,
|
|
765
|
+
name,
|
|
766
|
+
requireName: values["require-name"] === true,
|
|
767
|
+
location: address === void 0 && lat === void 0 ? void 0 : {
|
|
768
|
+
...address === void 0 ? {} : { address },
|
|
769
|
+
...lat === void 0 ? {} : {
|
|
770
|
+
lat,
|
|
771
|
+
lon
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
osm,
|
|
775
|
+
pages,
|
|
776
|
+
compare
|
|
777
|
+
};
|
|
778
|
+
};
|
|
779
|
+
/** The days of a week in their order, as `DAYS` of `hours.ts`. */
|
|
780
|
+
const WEEK_DAYS = [
|
|
781
|
+
"mon",
|
|
782
|
+
"tue",
|
|
783
|
+
"wed",
|
|
784
|
+
"thu",
|
|
785
|
+
"fri",
|
|
786
|
+
"sat",
|
|
787
|
+
"sun"
|
|
788
|
+
];
|
|
789
|
+
const DAY_LABELS = {
|
|
790
|
+
mon: "Mon",
|
|
791
|
+
tue: "Tue",
|
|
792
|
+
wed: "Wed",
|
|
793
|
+
thu: "Thu",
|
|
794
|
+
fri: "Fri",
|
|
795
|
+
sat: "Sat",
|
|
796
|
+
sun: "Sun"
|
|
797
|
+
};
|
|
798
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
799
|
+
/** This function tells if a value is a time of a kept week: `HH:MM` or `H:MM`, to `24:00`. */
|
|
800
|
+
const isClock = (value) => {
|
|
801
|
+
if (typeof value !== "string") return false;
|
|
802
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
|
803
|
+
return match !== null && Number(match[1]) <= 24 && Number(match[2]) < 60;
|
|
804
|
+
};
|
|
805
|
+
const intervalOf = (value) => Array.isArray(value) && value.length === 2 && isClock(value[0]) && isClock(value[1]) ? [value[0], value[1]] : void 0;
|
|
806
|
+
/**
|
|
807
|
+
* This function reads the kept week of `--compare`, a JSON file. The file is a week, or an object
|
|
808
|
+
* with the field `week` (a reading of `venue --json`) or `hours_week`. Each day gives a list of
|
|
809
|
+
* intervals (`[]` for closed), one interval, or null for closed. An error names the file.
|
|
810
|
+
*/
|
|
811
|
+
const keptWeekOf = (path) => {
|
|
812
|
+
const file = fileOf(path);
|
|
813
|
+
let text;
|
|
814
|
+
try {
|
|
815
|
+
text = readFileSync(file, "utf8");
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const code = error.code;
|
|
818
|
+
throw new UsageError(`--compare: the file ${file} does not open (${typeof code === "string" ? code : "error"})`, { cause: error });
|
|
819
|
+
}
|
|
820
|
+
let data;
|
|
821
|
+
try {
|
|
822
|
+
data = JSON.parse(text.charCodeAt(0) === 65279 ? text.slice(1) : text);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
throw new UsageError(`--compare: the file ${file} is not JSON`, { cause: error });
|
|
825
|
+
}
|
|
826
|
+
if (isRecord(data) && (isRecord(data["week"]) || isRecord(data["hours_week"]))) data = isRecord(data["week"]) ? data["week"] : data["hours_week"];
|
|
827
|
+
const wants = `--compare: the file ${file} is not a week. Write a list of intervals, one interval or null for each day, such as {"mon": [["09:00", "17:00"]], "sun": []}`;
|
|
828
|
+
if (!isRecord(data)) throw new UsageError(wants);
|
|
829
|
+
const week = {};
|
|
830
|
+
for (const [key, value] of Object.entries(data)) {
|
|
831
|
+
const day = WEEK_DAYS.find((one) => one === key);
|
|
832
|
+
if (day === void 0) throw new UsageError(`--compare: the file ${file} names "${key}", not a day: use mon, tue, wed, thu, fri, sat or sun`);
|
|
833
|
+
const one = intervalOf(value);
|
|
834
|
+
const intervals = value === null ? [] : one !== void 0 ? [one] : Array.isArray(value) ? value.map(intervalOf) : [void 0];
|
|
835
|
+
if (intervals.some((interval) => interval === void 0)) throw new UsageError(`${wants} (the value of ${key} is not)`);
|
|
836
|
+
week[day] = intervals;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
file,
|
|
840
|
+
week
|
|
841
|
+
};
|
|
842
|
+
};
|
|
843
|
+
/** This function writes the intervals of one day, as `describeWeek` of `hours.ts` does. */
|
|
844
|
+
const dayText = (intervals) => intervals === void 0 ? "not stated" : intervals.length === 0 ? "closed" : intervals.map(([open, close]) => open === "00:00" && close === "00:00" ? "open 24 hours" : `${open}–${close}`).join(", ");
|
|
845
|
+
/** This function gives the days of a comparison in their order. A day that no week states is not there. */
|
|
846
|
+
const compareDays = (kept, read, comparison) => {
|
|
847
|
+
const days = [];
|
|
848
|
+
for (const day of WEEK_DAYS) {
|
|
849
|
+
const one = {
|
|
850
|
+
day,
|
|
851
|
+
kept: kept[day],
|
|
852
|
+
read: read[day]
|
|
853
|
+
};
|
|
854
|
+
if (one.kept === void 0 && one.read === void 0) continue;
|
|
855
|
+
const status = comparison.status === "new" || comparison.added.includes(day) ? "new" : comparison.days.includes(day) ? "changed" : comparison.skipped.includes(day) ? "skipped" : one.read === void 0 ? "not read" : "same";
|
|
856
|
+
if (comparison.status === "new" && one.read === void 0) continue;
|
|
857
|
+
days.push({
|
|
858
|
+
...one,
|
|
859
|
+
status
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return days;
|
|
863
|
+
};
|
|
864
|
+
const QUOTE_CHARS = 200;
|
|
865
|
+
const shortText = (text) => text.length > QUOTE_CHARS ? `${text.slice(0, QUOTE_CHARS)}…` : text;
|
|
866
|
+
/** This function gives the lines of one reading: its rank, its source, its week and its quote. */
|
|
867
|
+
const readingLines = (one) => {
|
|
868
|
+
const marks = [
|
|
869
|
+
one.schedule === "other" ? "not the opening hours" : null,
|
|
870
|
+
one.inferred ? "am or pm guessed" : null,
|
|
871
|
+
one.partial ? "a rule left out" : null,
|
|
872
|
+
one.season === null ? null : `season ${one.season}`
|
|
873
|
+
].filter((mark) => mark !== null);
|
|
874
|
+
const where = [
|
|
875
|
+
one.url,
|
|
876
|
+
one.path,
|
|
877
|
+
one.capture === null ? null : `capture ${one.capture}`,
|
|
878
|
+
one.archived === void 0 ? null : `archived copy (${one.archived.source}, captured ${one.archived.timestamp})`
|
|
879
|
+
].filter((part) => part !== null);
|
|
880
|
+
return [
|
|
881
|
+
` rank ${String(one.rank)} ${one.from}${one.label === null ? "" : ` "${one.label}"`} ${one.hours === "" ? "no day" : one.hours}${marks.length > 0 ? ` (${marks.join(", ")})` : ""}`,
|
|
882
|
+
...where.length > 0 ? [` ${where.join(" · ")}`] : [],
|
|
883
|
+
one.from === "text" ? ` quote: ${JSON.stringify(shortText(one.raw))}` : ` value: ${shortText(one.raw.replace(/\s+/g, " "))}`
|
|
884
|
+
];
|
|
885
|
+
};
|
|
886
|
+
const pageLine = (page) => {
|
|
887
|
+
const facts = [
|
|
888
|
+
`${page.outcome} ${String(page.status)}`,
|
|
889
|
+
page.from_cache ? "from the store" : null,
|
|
890
|
+
page.rendered ? "rendered" : null,
|
|
891
|
+
page.held_back ? "held back" : null,
|
|
892
|
+
page.archived === void 0 ? null : `archived copy (${page.archived.source})`
|
|
893
|
+
].filter((fact) => fact !== null);
|
|
894
|
+
return ` ${page.outcome === "ok" || page.outcome === "not-modified" || page.archived !== void 0 ? "✓" : "✗"} ${page.url} ${facts.join(" · ")}`;
|
|
895
|
+
};
|
|
896
|
+
const WALL_OUTCOMES = /* @__PURE__ */ new Set([
|
|
897
|
+
"blocked",
|
|
898
|
+
"challenge",
|
|
899
|
+
"payment-required"
|
|
900
|
+
]);
|
|
901
|
+
/**
|
|
902
|
+
* This function tells if a site refused the crawler, with the rule of `wall` of `readProduct`. The
|
|
903
|
+
* causes are a 401, a 402 or a 403, a challenge, a bot check, robots.txt or a stop of the host.
|
|
904
|
+
*/
|
|
905
|
+
const refusedPage = (page) => WALL_OUTCOMES.has(page.outcome) || page.outcome === "skipped" && /^(?:robots\.txt disallows|host stopped)/.test(page.note ?? "") || page.doc === null && /\bbot check\b/.test(page.note ?? "");
|
|
906
|
+
/**
|
|
907
|
+
* This function gives the exit code of `venue`. It is 0 when the site gave a reading of the opening
|
|
908
|
+
* hours. It is `WALL` when the home page refused the crawler, and `NOTHING_READ` for a different
|
|
909
|
+
* result. An OpenStreetMap value alone does not count.
|
|
910
|
+
*/
|
|
911
|
+
const venueCode = (result) => {
|
|
912
|
+
if (result.readings.some((one) => one.from !== "osm" && one.schedule === "opening")) return 0;
|
|
913
|
+
const home = result.pages[0];
|
|
914
|
+
return home !== void 0 && refusedPage(home) ? 4 : 3;
|
|
915
|
+
};
|
|
916
|
+
/** This function gives the exit code of `product`: 0 for Product data, `WALL` for a refusal, else `NOTHING_READ`. */
|
|
917
|
+
const productCode = (result) => result.found ? 0 : result.wall ? 4 : 3;
|
|
918
|
+
/** This line tells how to read a held-back page. */
|
|
919
|
+
const HELD_BACK_LINE = "Add --user-provided if you gave this URL yourself. The crawler logs that read.";
|
|
920
|
+
/** This function gives the line of a page that needs a browser, when rendering is off. */
|
|
921
|
+
const renderLine = (notes, renderOff) => renderOff !== void 0 && notes.some((note) => note !== null && /no browser/i.test(note)) ? [`rendering is off: ${renderOff}`] : [];
|
|
922
|
+
/**
|
|
923
|
+
* This function gives the lines of `venue`: the week of the site, and each reading with its source.
|
|
924
|
+
* Then come the days where the readings disagree, the menus, the phone, the pages and the notes.
|
|
925
|
+
* With `--compare`, the lines give each day of the comparison.
|
|
926
|
+
*/
|
|
927
|
+
const describeVenue = (result, compare, renderOff) => {
|
|
928
|
+
const code = venueCode(result);
|
|
929
|
+
const lines = [`${code === 0 ? "✓" : "✗"} ${result.home ?? result.url}`];
|
|
930
|
+
const site = result.from_site;
|
|
931
|
+
const home = result.pages[0];
|
|
932
|
+
if (site !== null) {
|
|
933
|
+
const source = [
|
|
934
|
+
`${site.from}, rank ${String(site.rank)}`,
|
|
935
|
+
site.url,
|
|
936
|
+
site.capture === null ? null : `capture ${site.capture}`
|
|
937
|
+
].filter((part) => part !== null);
|
|
938
|
+
lines.push(` the site: ${site.hours}`, ` from ${source.join(", ")}`);
|
|
939
|
+
} else if (result.best !== null) lines.push(` no reading of the site with no guess. The best reading (${result.best.from}, rank ${String(result.best.rank)}): ${result.best.hours}`);
|
|
940
|
+
else if (code === 4 && home !== void 0) lines.push(` wall: the site refused the crawler (${home.outcome} ${String(home.status)}). The crawler does not try a different identity.`);
|
|
941
|
+
else lines.push(" no opening hours found");
|
|
942
|
+
if (result.readings.length > 0) {
|
|
943
|
+
lines.push("readings, the best first:");
|
|
944
|
+
for (const one of result.readings) lines.push(...readingLines(one));
|
|
945
|
+
}
|
|
946
|
+
for (const day of result.disagree) {
|
|
947
|
+
const said = result.readings.filter((one) => one.rank <= 2 && one.week[day] !== void 0).map((one) => `${dayText(one.week[day])} (rank ${String(one.rank)} ${one.from})`);
|
|
948
|
+
lines.push(`the readings disagree on ${DAY_LABELS[day]}: ${said.join(" · ")}`);
|
|
949
|
+
}
|
|
950
|
+
if (result.menus.length > 0) lines.push("menus:", ...result.menus.map((menu) => ` ${menu.url}${menu.text === "" ? "" : ` ${menu.text}`}`));
|
|
951
|
+
if (result.phone !== null) lines.push(`phone: ${result.phone}`);
|
|
952
|
+
if (result.moved !== null) lines.push(`moved: the site now goes to ${result.moved}`);
|
|
953
|
+
if (result.names_venue !== null) lines.push(result.names_venue ? "names the venue: yes" : "names the venue: no. The domain can belong to a different business now.");
|
|
954
|
+
if (result.held_back) lines.push(`held back: a page asked not to be AI input, so the recipe read nothing from it. ${HELD_BACK_LINE}`);
|
|
955
|
+
if (result.pages.length > 0) lines.push(`pages (${String(result.pages.length)}):`, ...result.pages.map(pageLine));
|
|
956
|
+
if (compare !== void 0) {
|
|
957
|
+
const comparison = compare.comparison;
|
|
958
|
+
if (comparison === null || site === null) lines.push(`compare with ${compare.file}: no reading of the site with no guess, so nothing to compare`);
|
|
959
|
+
else {
|
|
960
|
+
const changed = comparison.days.length > 0 ? ` (${comparison.days.map((day) => DAY_LABELS[day]).join(", ")})` : "";
|
|
961
|
+
lines.push(`compare with ${compare.file}: ${comparison.status}${changed}`);
|
|
962
|
+
for (const one of compareDays(compare.kept, site.week, comparison)) {
|
|
963
|
+
const text = one.status === "changed" || one.status === "skipped" ? `${dayText(one.kept)} → ${dayText(one.read)}` : one.status === "not read" ? `kept ${dayText(one.kept)}` : dayText(one.read);
|
|
964
|
+
lines.push(` ${DAY_LABELS[one.day]} ${one.status.padEnd(8)} ${text}`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (result.note !== null) lines.push(`note: ${result.note}`);
|
|
969
|
+
lines.push(...renderLine([result.note], renderOff));
|
|
970
|
+
return lines;
|
|
971
|
+
};
|
|
972
|
+
/** This function gives the number of digits after the decimal point of a currency: 2 for EUR, 0 for JPY. */
|
|
973
|
+
const fractionOf = (currency) => {
|
|
974
|
+
try {
|
|
975
|
+
return new Intl.NumberFormat("en", {
|
|
976
|
+
style: "currency",
|
|
977
|
+
currency
|
|
978
|
+
}).resolvedOptions().maximumFractionDigits ?? 2;
|
|
979
|
+
} catch {
|
|
980
|
+
return 2;
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
/** This function writes an amount of money with its ISO 4217 code: `1,299.00 EUR`, `980 JPY`. */
|
|
984
|
+
const moneyText = (money) => {
|
|
985
|
+
const digits = fractionOf(money.currency);
|
|
986
|
+
return `${new Intl.NumberFormat("en", {
|
|
987
|
+
minimumFractionDigits: digits,
|
|
988
|
+
maximumFractionDigits: Math.max(digits, 4)
|
|
989
|
+
}).format(money.amount)} ${money.currency}`;
|
|
990
|
+
};
|
|
991
|
+
/**
|
|
992
|
+
* This function gives the lines of `product`. They are the name, the brand and the codes, the price
|
|
993
|
+
* as money, the availability, the variant, the seller, the page and the notes. A wall and a
|
|
994
|
+
* held-back page say so.
|
|
995
|
+
*/
|
|
996
|
+
const describeProduct = (result, renderOff) => {
|
|
997
|
+
const code = productCode(result);
|
|
998
|
+
const lines = [`${code === 0 ? "✓" : "✗"} ${result.name ?? result.url}`];
|
|
999
|
+
if (result.name !== null) lines.push(` ${result.url}`);
|
|
1000
|
+
if (code === 4) lines.push(` wall: the shop refused the crawler (${result.outcome} ${String(result.status)}). The crawler does not try a different identity.`);
|
|
1001
|
+
if (result.held_back) lines.push(` held back: the page asked not to be AI input. ${HELD_BACK_LINE}`);
|
|
1002
|
+
const codes = [
|
|
1003
|
+
result.brand === null ? null : `brand ${result.brand}`,
|
|
1004
|
+
result.sku === null ? null : `sku ${result.sku}`,
|
|
1005
|
+
result.gtin === null ? null : `gtin ${result.gtin}`
|
|
1006
|
+
].filter((part) => part !== null);
|
|
1007
|
+
if (codes.length > 0) lines.push(` ${codes.join(" · ")}`);
|
|
1008
|
+
if (result.price !== null) lines.push(result.price_to === null ? ` price ${moneyText(result.price)}` : ` price ${moneyText(result.price)} to ${moneyText(result.price_to)}, the range of the offers`);
|
|
1009
|
+
if (result.availability !== null) lines.push(` availability ${result.availability}${result.availabilities.length > 1 ? ` (the offers: ${result.availabilities.join(", ")})` : ""}`);
|
|
1010
|
+
const variant = result.variant;
|
|
1011
|
+
if (variant !== null) {
|
|
1012
|
+
const parts = [
|
|
1013
|
+
variant.name,
|
|
1014
|
+
variant.sku === null ? null : `sku ${variant.sku}`,
|
|
1015
|
+
variant.price === null ? null : moneyText(variant.price),
|
|
1016
|
+
variant.availability
|
|
1017
|
+
].filter((part) => part !== null);
|
|
1018
|
+
lines.push(` variant ${parts.length > 0 ? parts.join(" · ") : "with no data"}`);
|
|
1019
|
+
}
|
|
1020
|
+
if (result.seller !== null) lines.push(` seller ${result.seller}`);
|
|
1021
|
+
if (result.image !== null) lines.push(` image ${result.image}`);
|
|
1022
|
+
if (result.archived !== void 0) lines.push(` archived copy (${result.archived.source}, captured ${result.archived.timestamp}): ${result.archived.url}`);
|
|
1023
|
+
const facts = [
|
|
1024
|
+
`${result.outcome} ${String(result.status)}`,
|
|
1025
|
+
result.doc === null ? null : `doc ${result.doc}`,
|
|
1026
|
+
result.capture === null ? null : `capture ${result.capture}`,
|
|
1027
|
+
result.rendered ? "rendered" : null,
|
|
1028
|
+
result.from_cache ? "from the store" : null
|
|
1029
|
+
].filter((fact) => fact !== null);
|
|
1030
|
+
lines.push(` ${facts.join(" · ")}`);
|
|
1031
|
+
if (result.note !== null) lines.push(` note: ${result.note}`);
|
|
1032
|
+
lines.push(...renderLine([result.note], renderOff).map((line) => ` ${line}`));
|
|
1033
|
+
return lines;
|
|
1034
|
+
};
|
|
695
1035
|
/** This function checks the options of `export`, and gives its format and its options. */
|
|
696
1036
|
const exportPlan = (values) => {
|
|
697
1037
|
const out = values.out;
|
|
@@ -780,6 +1120,11 @@ const readLine = (args, env) => {
|
|
|
780
1120
|
} catch (error) {
|
|
781
1121
|
throw new UsageError(`--max-age: ${error.message}`, { cause: error });
|
|
782
1122
|
}
|
|
1123
|
+
const recipeLine = command === "venue" || command === "product" ? recipeLineOf(command, values, rest, {
|
|
1124
|
+
render,
|
|
1125
|
+
maxAge,
|
|
1126
|
+
budget: values.budget === void 0 ? void 0 : crawlLimits.budgetMs
|
|
1127
|
+
}) : void 0;
|
|
783
1128
|
return {
|
|
784
1129
|
kind: "command",
|
|
785
1130
|
command,
|
|
@@ -806,7 +1151,8 @@ const readLine = (args, env) => {
|
|
|
806
1151
|
signKey,
|
|
807
1152
|
signatureAgent,
|
|
808
1153
|
embedder,
|
|
809
|
-
maxDocs
|
|
1154
|
+
maxDocs,
|
|
1155
|
+
recipeLine
|
|
810
1156
|
};
|
|
811
1157
|
};
|
|
812
1158
|
//#endregion
|
|
@@ -843,6 +1189,8 @@ Usage:
|
|
|
843
1189
|
polite-crawler extract <doc> --field name=value[:kind]...
|
|
844
1190
|
check proposed field values against a stored page
|
|
845
1191
|
polite-crawler embed --embedder <name> give vectors to the stored pages, for find --rank hybrid
|
|
1192
|
+
polite-crawler venue <url> read the opening hours of a venue from its own site
|
|
1193
|
+
polite-crawler product <url> read the schema.org Product data of a product page
|
|
846
1194
|
|
|
847
1195
|
Options:
|
|
848
1196
|
--dir <path> the store (default $POLITE_CRAWLER_DIR, or ~/.polite-crawler)
|
|
@@ -937,6 +1285,20 @@ Options:
|
|
|
937
1285
|
embed --budget <time> the longest time (default 10m); no embedding starts after it
|
|
938
1286
|
--max-docs <n> how many texts get vectors at most
|
|
939
1287
|
--kind <kind> only this kind of document
|
|
1288
|
+
venue --name <text> the name of the venue: the result tells if the home page names it
|
|
1289
|
+
--require-name with --name: read no hours from a site that does not name the venue
|
|
1290
|
+
--address <text> the street address, for a page that lists more than one location
|
|
1291
|
+
--lat <n> --lon <n> the position of the venue, for the same use (within 150 m);
|
|
1292
|
+
write a value below 0 after =, such as --lon=-20.5
|
|
1293
|
+
--osm <value> an OpenStreetMap opening_hours value, read as one more reading;
|
|
1294
|
+
no request goes to OpenStreetMap
|
|
1295
|
+
--pages <n> the pages after the home page: 0 to 8 (default 3)
|
|
1296
|
+
--compare <file> compare the week of the site with a kept week (JSON), day by day
|
|
1297
|
+
venue, product --render, --max-age, --archive-fallback, --archive-despite-robots as for fetch
|
|
1298
|
+
--budget <time> no request starts after this time, such as 2m (default: no limit)
|
|
1299
|
+
--on-behalf-of <agent> the robots.txt rules of this agent apply too, such as
|
|
1300
|
+
Claude-User (repeat it)
|
|
1301
|
+
--user-provided as for read
|
|
940
1302
|
|
|
941
1303
|
Exit codes:
|
|
942
1304
|
0 done
|
|
@@ -946,13 +1308,18 @@ Exit codes:
|
|
|
946
1308
|
or an error
|
|
947
1309
|
(fetch: a page with an archived copy counts as done)
|
|
948
1310
|
2 the command line is wrong
|
|
1311
|
+
3 venue, product: nothing read: no opening hours of the site or no Product data, a page
|
|
1312
|
+
held back, or a page that did not come
|
|
1313
|
+
4 venue, product: the site refused the crawler (a wall): a 401, a 402 or a 403, a
|
|
1314
|
+
challenge, a bot check, robots.txt or a stopped host. The crawler does not try a
|
|
1315
|
+
different identity
|
|
949
1316
|
`;
|
|
950
1317
|
/**
|
|
951
1318
|
* This function does the command `export`, and gives the exit code. `readLine` checked the whole
|
|
952
1319
|
* command line first. The export only reads the store: it makes no store, and it sends no request.
|
|
953
1320
|
*/
|
|
954
1321
|
const exportCommand = async (plan, dir, json) => {
|
|
955
|
-
const result = plan.format === "wacz" ? await (await import("./wacz-
|
|
1322
|
+
const result = plan.format === "wacz" ? await (await import("./wacz-QyzLp4mC.mjs").then((n) => n.n)).exportWacz(dir, plan.options) : await (await import("./warc-BHcaRKHw.mjs").then((n) => n.a)).exportWarc(dir, plan.options);
|
|
956
1323
|
process.stdout.write(`${json ? JSON.stringify(result, null, 2) : describeExport(result, plan.options.includeRefusals)}\n`);
|
|
957
1324
|
return 0;
|
|
958
1325
|
};
|
|
@@ -1074,7 +1441,7 @@ const runWatchCommand = async (crawler, line, values, limits, json, out) => {
|
|
|
1074
1441
|
};
|
|
1075
1442
|
/** This function gives the text of a diff for a person: the two captures, then the text of `formatDiff`. */
|
|
1076
1443
|
const describeDiff = async (diff) => {
|
|
1077
|
-
const { formatDiff } = await import("./diff-
|
|
1444
|
+
const { formatDiff } = await import("./diff-BUM91Rev.mjs").then((n) => n.i);
|
|
1078
1445
|
return [
|
|
1079
1446
|
`${diff.doc} ${diff.from.capture} (${diff.from.fetched_at}) -> ${diff.to.capture} (${diff.to.fetched_at})`,
|
|
1080
1447
|
formatDiff(diff),
|
|
@@ -1084,7 +1451,7 @@ const describeDiff = async (diff) => {
|
|
|
1084
1451
|
};
|
|
1085
1452
|
/** This function checks the fields against a stored document, and prints a line for each field. The exit code is 0 when each field is verified or normalized. */
|
|
1086
1453
|
const runExtract = async (crawler, doc, fields, options, json, out) => {
|
|
1087
|
-
const { extractDocument } = await import("./extract-
|
|
1454
|
+
const { extractDocument } = await import("./extract-CefB5Vfn.mjs").then((n) => n.i);
|
|
1088
1455
|
const claims = fields;
|
|
1089
1456
|
const result = crawler.extract === void 0 ? await extractDocument(crawler.runtime, doc, claims, options) : await crawler.extract(doc, claims, options);
|
|
1090
1457
|
if (json) out(JSON.stringify(result, null, 2));
|
|
@@ -1178,7 +1545,7 @@ const main = async (args) => {
|
|
|
1178
1545
|
return 0;
|
|
1179
1546
|
}
|
|
1180
1547
|
if (command === "daemon") {
|
|
1181
|
-
const { daemonCommand } = await import("./cli-daemon
|
|
1548
|
+
const { daemonCommand } = await import("./cli-daemon-ZYRdy6EW.mjs");
|
|
1182
1549
|
return daemonCommand(line.daemonLine, options, {
|
|
1183
1550
|
allowUserProvided: line.allowUserProvided,
|
|
1184
1551
|
json: values.json === true,
|
|
@@ -1186,10 +1553,13 @@ const main = async (args) => {
|
|
|
1186
1553
|
});
|
|
1187
1554
|
}
|
|
1188
1555
|
if (line.exportPlan !== void 0) return exportCommand(line.exportPlan, dir, values.json === true);
|
|
1189
|
-
const
|
|
1556
|
+
const recipeLine = line.recipeLine;
|
|
1557
|
+
const recipes = recipeLine === void 0 ? void 0 : await import("./cli-recipes-Bci6HREA.mjs");
|
|
1558
|
+
const recipeInput = recipes === void 0 || recipeLine === void 0 ? void 0 : recipes.prepareRecipe(recipeLine);
|
|
1559
|
+
const { openCrawler } = await import("./open-CbpSCEZc.mjs").then((n) => n.a);
|
|
1190
1560
|
let renderOff;
|
|
1191
1561
|
let renderer = options.renderer;
|
|
1192
|
-
if (!(command === "fetch" || command === "crawl" || watchLine?.action === "run") || render === "never") renderer = false;
|
|
1562
|
+
if (!(command === "fetch" || command === "crawl" || recipeLine !== void 0 || watchLine?.action === "run") || render === "never") renderer = false;
|
|
1193
1563
|
else if (renderer === void 0) {
|
|
1194
1564
|
const { browserMissing } = await import("./browser-CKRNbIDT.mjs").then((n) => n.n);
|
|
1195
1565
|
renderOff = await browserMissing();
|
|
@@ -1273,6 +1643,15 @@ const main = async (args) => {
|
|
|
1273
1643
|
maxDocs: line.maxDocs,
|
|
1274
1644
|
kind: values.kind
|
|
1275
1645
|
}, values.json === true, out);
|
|
1646
|
+
case "venue":
|
|
1647
|
+
case "product": {
|
|
1648
|
+
const io = {
|
|
1649
|
+
json: values.json === true,
|
|
1650
|
+
out,
|
|
1651
|
+
renderOff
|
|
1652
|
+
};
|
|
1653
|
+
return command === "venue" ? await recipes.venueCommand(crawler, recipeLine, recipeInput, io) : await recipes.productCommand(crawler, recipeLine, io);
|
|
1654
|
+
}
|
|
1276
1655
|
default: {
|
|
1277
1656
|
const counts = await crawler.reindex();
|
|
1278
1657
|
print(counts, () => describeReindex(counts));
|
|
@@ -1292,6 +1671,6 @@ main(process.argv.slice(2)).then((code) => {
|
|
|
1292
1671
|
process.exitCode = usage ? 2 : 1;
|
|
1293
1672
|
});
|
|
1294
1673
|
//#endregion
|
|
1295
|
-
export { describeDaemonStatus as t };
|
|
1674
|
+
export { keptWeekOf as a, describeVenue as i, describeDaemonStatus as n, productCode as o, describeProduct as r, venueCode as s, UsageError as t };
|
|
1296
1675
|
|
|
1297
1676
|
//# sourceMappingURL=cli.mjs.map
|