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/README.md
CHANGED
|
@@ -103,6 +103,22 @@ npx polite-crawler watch list
|
|
|
103
103
|
- `watch run` takes `--budget` (default `10m`) and `--max-pages` (default 100). It prints each check, then a summary. A scheduler of the operator starts it.
|
|
104
104
|
- `daemon` checks the watch list on a schedule, until you stop it. With `--mcp-http`, it also serves the MCP tools over local HTTP. [The daemon](#the-daemon) tells how.
|
|
105
105
|
|
|
106
|
+
To read the hours of a venue or a product page, use `venue` and `product`. They use the recipes of the package:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npx polite-crawler venue https://forno-esempio.example/ --name "Forno Esempio" --pages 2
|
|
110
|
+
npx polite-crawler venue https://forno-esempio.example/ --compare kept-week.json
|
|
111
|
+
npx polite-crawler product "https://bottega-campione.example/products/olio-esempio?variant=222"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `venue` prints the week of the site and each reading with its URL, its rank and its quote. It also prints the days where the readings disagree, the menus, the phone, the pages and the notes.
|
|
115
|
+
- `venue` takes `--name`, `--require-name`, `--address`, `--lat` and `--lon`, `--osm <value>` and `--pages <0-8>`. [Read the hours of a venue](#read-the-hours-of-a-venue) tells what each one does.
|
|
116
|
+
- Write a number below zero after `=`, for example `--lon=-20.5`. Else the command line reads it as an option.
|
|
117
|
+
- `venue --compare <file>` compares the week of the site with a week that you keep, as JSON. It prints `same`, `changed`, `new` or `not read` for each day. The file is a week, for example `{"mon": [["07:00", "15:00"]], "sun": []}`, or the `from_site` of `venue --json`.
|
|
118
|
+
- `product` prints the name, the brand, the SKU and the GTIN, and the price as money with the range of the offers. It also prints the availability, the variant, the seller, and a wall or a held-back page.
|
|
119
|
+
- The two commands take `--render`, `--max-age`, `--budget`, `--user-provided`, `--archive-fallback` and `--archive-despite-robots`. `--on-behalf-of <agent>` applies the robots.txt rules of an agent too, for example `Claude-User`.
|
|
120
|
+
- `--json` prints the `VenueResult` or the `ProductResult` of the library.
|
|
121
|
+
|
|
106
122
|
The command gives these exit codes:
|
|
107
123
|
|
|
108
124
|
| Code | Meaning |
|
|
@@ -110,6 +126,8 @@ The command gives these exit codes:
|
|
|
110
126
|
| `0` | The command did its work. |
|
|
111
127
|
| `1` | The command did not do its work: a page that the crawler did not fetch, no search result, a quote that is not in the document, a document that is not stored or that is held back, or an error. `discover` gives 1 when it finds no URL, and `crawl` when it gets no page. `read --at` and `verify --at` give 1 for a time before the first capture. `watch run` gives 1 when a check is `refused` or an `error`, or when entries stay due. `watch remove` gives 1 for a URL that is not on the list. With `--archive-fallback`, a page with an archived copy counts as done. `extract` gives 1 when a field is not `verified` or `normalized`. `embed` gives 1 when its budget stops it before each page has vectors. |
|
|
112
128
|
| `2` | The command line is wrong. The command opens no store. |
|
|
129
|
+
| `3` | `venue` and `product` read nothing: no opening hours of the site or no Product data, a page that is held back, or a page that did not come. |
|
|
130
|
+
| `4` | `venue` and `product`: the site refused the crawler (a wall). The causes are a 401, a 402 or a 403, a challenge, a bot check, robots.txt or a stop of the host. The crawler does not try a different identity. |
|
|
113
131
|
|
|
114
132
|
## Use it from Claude (MCP)
|
|
115
133
|
|
|
@@ -935,6 +953,8 @@ const then = venueReadingsOf(version!);
|
|
|
935
953
|
- With `skipSplit`, it does not compare a day with two or more intervals, for a store that keeps one interval for each day.
|
|
936
954
|
- One reading can be an error of the site. Confirm a change with a second reading, as a watch does.
|
|
937
955
|
|
|
956
|
+
The command line has `polite-crawler venue`, and `venue --compare <file>` does this comparison. [Use it from the command line](#use-it-from-the-command-line) gives the options.
|
|
957
|
+
|
|
938
958
|
## Read a product page
|
|
939
959
|
|
|
940
960
|
`readProduct` reads the schema.org Product data of a product page. Most shop systems write this data on each product page.
|
|
@@ -977,6 +997,8 @@ const check = await crawler.extract(product.doc!, {
|
|
|
977
997
|
});
|
|
978
998
|
```
|
|
979
999
|
|
|
1000
|
+
The command line has `polite-crawler product`. It exits with 4 at a wall, and with 3 when it reads no Product data.
|
|
1001
|
+
|
|
980
1002
|
## Writing a recipe
|
|
981
1003
|
|
|
982
1004
|
A recipe tells the crawler what to fetch and how to read it into documents. The venue and product recipes read pages with the page reader of the package. A recipe of your own reads a source that the page reader does not know, for example an API.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as describeDaemonStatus } from "./cli.mjs";
|
|
2
2
|
//#region src/cli-daemon.ts
|
|
3
3
|
/**
|
|
4
4
|
* This function does the command `daemon`, and gives the exit code. `daemon status` gives 0 when a
|
|
@@ -6,7 +6,7 @@ import { t as describeDaemonStatus } from "./cli.mjs";
|
|
|
6
6
|
* error at start rejects, so the command exits with 1.
|
|
7
7
|
*/
|
|
8
8
|
const daemonCommand = async (line, options, io) => {
|
|
9
|
-
const { changeCommand, readDaemonState, startDaemon } = await import("./daemon-
|
|
9
|
+
const { changeCommand, readDaemonState, startDaemon } = await import("./daemon-DK6U51_v.mjs").then((n) => n.a);
|
|
10
10
|
if (line.action === "status") {
|
|
11
11
|
const state = readDaemonState(options.dir);
|
|
12
12
|
io.out(io.json ? JSON.stringify(state ?? null, null, 2) : describeDaemonStatus(state, options.dir));
|
|
@@ -35,4 +35,4 @@ const daemonCommand = async (line, options, io) => {
|
|
|
35
35
|
//#endregion
|
|
36
36
|
export { daemonCommand };
|
|
37
37
|
|
|
38
|
-
//# sourceMappingURL=cli-daemon
|
|
38
|
+
//# sourceMappingURL=cli-daemon-ZYRdy6EW.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-daemon
|
|
1
|
+
{"version":3,"file":"cli-daemon-ZYRdy6EW.mjs","names":[],"sources":["../src/cli-daemon.ts"],"sourcesContent":["/*\n * The command `daemon` of the command line: `daemon` runs the daemon until `SIGINT` or `SIGTERM`, and\n * `daemon status` prints the state file. `cli.ts` loads this file only for the command `daemon`.\n */\nimport type { DaemonLine } from \"./cli-line.ts\";\nimport { describeDaemonStatus } from \"./cli-line.ts\";\nimport type { OpenOptions } from \"./open.ts\";\n\n/** These are the parts of the command line that the command `daemon` uses. */\nexport interface DaemonIo {\n /** The value true lets `user_provided` work when the MCP client cannot ask the user. */\n readonly allowUserProvided: boolean;\n /** The value true prints the status as JSON. */\n readonly json: boolean;\n /** This function prints a line on the standard output. */\n readonly out: (text: string) => void;\n}\n\n/**\n * This function does the command `daemon`, and gives the exit code. `daemon status` gives 0 when a\n * daemon operates on the store, and 1 when none operates. `daemon` gives 0 after a clean stop. An\n * error at start rejects, so the command exits with 1.\n */\nexport const daemonCommand = async (\n line: DaemonLine,\n options: OpenOptions,\n io: DaemonIo,\n): Promise<number> => {\n const { changeCommand, readDaemonState, startDaemon } = await import(\"./daemon.ts\");\n if (line.action === \"status\") {\n const state = readDaemonState(options.dir);\n io.out(\n io.json ? JSON.stringify(state ?? null, null, 2) : describeDaemonStatus(state, options.dir),\n );\n return state?.alive === true ? 0 : 1;\n }\n const daemon = await startDaemon({\n ...options,\n tick: line.tickMs,\n jitter: line.jitterMs,\n budget: line.budgetMs,\n maxPages: line.maxPages,\n grace: line.graceMs,\n events: line.events,\n onChange: line.onChange === undefined ? undefined : changeCommand(line.onChange),\n mcp:\n line.mcp === undefined\n ? undefined\n : {\n port: line.mcp.port,\n host: line.mcp.host,\n allowRemote: line.mcp.allowRemote,\n tokenFile: line.mcp.tokenFile,\n allowUserProvided: io.allowUserProvided,\n },\n signals: true,\n });\n await daemon.done;\n return 0;\n};\n"],"mappings":";;;;;;;AAuBA,MAAa,gBAAgB,OAC3B,MACA,SACA,OACoB;CACpB,MAAM,EAAE,eAAe,iBAAiB,gBAAgB,MAAM,OAAO,wBAAc,CAAA,MAAA,MAAA,EAAA,CAAA;CACnF,IAAI,KAAK,WAAW,UAAU;EAC5B,MAAM,QAAQ,gBAAgB,QAAQ,GAAG;EACzC,GAAG,IACD,GAAG,OAAO,KAAK,UAAU,SAAS,MAAM,MAAM,CAAC,IAAI,qBAAqB,OAAO,QAAQ,GAAG,CAC5F;EACA,OAAO,OAAO,UAAU,OAAO,IAAI;CACrC;CAsBA,OAAM,MArBe,YAAY;EAC/B,GAAG;EACH,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,OAAO,KAAK;EACZ,QAAQ,KAAK;EACb,UAAU,KAAK,aAAa,KAAA,IAAY,KAAA,IAAY,cAAc,KAAK,QAAQ;EAC/E,KACE,KAAK,QAAQ,KAAA,IACT,KAAA,IACA;GACE,MAAM,KAAK,IAAI;GACf,MAAM,KAAK,IAAI;GACf,aAAa,KAAK,IAAI;GACtB,WAAW,KAAK,IAAI;GACpB,mBAAmB,GAAG;EACxB;EACN,SAAS;CACX,CAAC,EAAA,CACY;CACb,OAAO;AACT"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { compareHours, fromCompact } from "./hours.mjs";
|
|
2
|
+
import { d as readVenue, o as readProduct } from "./recipe-product-Dellqudu.mjs";
|
|
3
|
+
import { a as keptWeekOf, i as describeVenue, o as productCode, r as describeProduct, s as venueCode, t as UsageError } from "./cli.mjs";
|
|
4
|
+
//#region src/cli-recipes.ts
|
|
5
|
+
/**
|
|
6
|
+
* This function checks the parts of `venue` that need a file or a reader, before the store opens.
|
|
7
|
+
* They are the kept week of `--compare` and the value of `--osm`. An error is a usage error.
|
|
8
|
+
*/
|
|
9
|
+
const prepareRecipe = (line) => {
|
|
10
|
+
if (line.osm !== void 0 && fromCompact(line.osm, "osm") === void 0) throw new UsageError(`--osm: the hours reader cannot read "${line.osm}". Write an opening_hours value, such as "Mo-Fr 08:00-18:00; Sa 09:00-13:00"`);
|
|
11
|
+
return { kept: line.compare === void 0 ? void 0 : keptWeekOf(line.compare) };
|
|
12
|
+
};
|
|
13
|
+
/** This function gives the options of the recipe that `venue` and `product` share. */
|
|
14
|
+
const sharedOptions = (line) => ({
|
|
15
|
+
render: line.render,
|
|
16
|
+
maxAge: line.maxAge,
|
|
17
|
+
budget: line.budget,
|
|
18
|
+
onBehalfOf: line.onBehalfOf,
|
|
19
|
+
userProvided: line.userProvided,
|
|
20
|
+
fallback: line.fallback,
|
|
21
|
+
archiveDespiteRobots: line.archiveDespiteRobots
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* This function does the command `venue`, and gives the exit code. It reads the hours of the venue
|
|
25
|
+
* with `readVenue`. With `--compare`, it compares the week of the site (`from_site`) with the kept
|
|
26
|
+
* week through `compareHours`.
|
|
27
|
+
*/
|
|
28
|
+
const venueCommand = async (target, line, input, io) => {
|
|
29
|
+
const options = {
|
|
30
|
+
...sharedOptions(line),
|
|
31
|
+
name: line.name,
|
|
32
|
+
requireName: line.requireName,
|
|
33
|
+
location: line.location,
|
|
34
|
+
osm: line.osm,
|
|
35
|
+
pages: line.pages
|
|
36
|
+
};
|
|
37
|
+
const result = await readVenue(target, line.url, options);
|
|
38
|
+
const site = result.from_site;
|
|
39
|
+
const compare = input.kept === void 0 ? void 0 : {
|
|
40
|
+
file: input.kept.file,
|
|
41
|
+
kept: input.kept.week,
|
|
42
|
+
comparison: site === null ? null : compareHours(input.kept.week, site.week)
|
|
43
|
+
};
|
|
44
|
+
io.out(io.json ? JSON.stringify(compare === void 0 ? result : {
|
|
45
|
+
...result,
|
|
46
|
+
compare
|
|
47
|
+
}, null, 2) : describeVenue(result, compare, io.renderOff).join("\n"));
|
|
48
|
+
return venueCode(result);
|
|
49
|
+
};
|
|
50
|
+
/** This function does the command `product`, and gives the exit code. It reads the page with `readProduct`. */
|
|
51
|
+
const productCommand = async (target, line, io) => {
|
|
52
|
+
const result = await readProduct(target, line.url, sharedOptions(line));
|
|
53
|
+
io.out(io.json ? JSON.stringify(result, null, 2) : describeProduct(result, io.renderOff).join("\n"));
|
|
54
|
+
return productCode(result);
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { prepareRecipe, productCommand, venueCommand };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=cli-recipes-Bci6HREA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-recipes-Bci6HREA.mjs","names":[],"sources":["../src/cli-recipes.ts"],"sourcesContent":["/*\n * The commands `venue` and `product` of the command line. They read a venue or a product page with\n * the recipes of the package (`readVenue` and `readProduct`). `cli.ts` loads this file only for these\n * two commands, so the other commands do not load the recipes.\n */\nimport type { KeptWeek, RecipeLine, VenueCompare } from \"./cli-line.ts\";\nimport {\n UsageError,\n describeProduct,\n describeVenue,\n keptWeekOf,\n productCode,\n venueCode,\n} from \"./cli-line.ts\";\nimport { compareHours, fromCompact } from \"./hours.ts\";\nimport type { RecipeTarget } from \"./recipe.ts\";\nimport type { ProductOptions } from \"./recipe-product.ts\";\nimport { readProduct } from \"./recipe-product.ts\";\nimport type { VenueOptions } from \"./recipe-venue.ts\";\nimport { readVenue } from \"./recipe-venue.ts\";\n\n/** These are the parts of the command line that `venue` and `product` use. */\nexport interface RecipeIo {\n /** The value true prints the result as JSON. */\n readonly json: boolean;\n /** This function prints a line on the standard output. */\n readonly out: (text: string) => void;\n /** This text tells why rendering is off, when the computer has no browser. */\n readonly renderOff?: string | undefined;\n}\n\n/** These are the inputs of `venue` that the command reads before the store opens. */\nexport interface RecipeInput {\n /** This value is the kept week of `--compare`, or undefined with no `--compare`. */\n readonly kept: KeptWeek | undefined;\n}\n\n/**\n * This function checks the parts of `venue` that need a file or a reader, before the store opens.\n * They are the kept week of `--compare` and the value of `--osm`. An error is a usage error.\n */\nexport const prepareRecipe = (line: RecipeLine): RecipeInput => {\n if (line.osm !== undefined && fromCompact(line.osm, \"osm\") === undefined) {\n throw new UsageError(\n `--osm: the hours reader cannot read \"${line.osm}\". Write an opening_hours value, such as \"Mo-Fr 08:00-18:00; Sa 09:00-13:00\"`,\n );\n }\n return { kept: line.compare === undefined ? undefined : keptWeekOf(line.compare) };\n};\n\n/** This function gives the options of the recipe that `venue` and `product` share. */\nconst sharedOptions = (line: RecipeLine): ProductOptions => ({\n render: line.render,\n maxAge: line.maxAge,\n budget: line.budget,\n onBehalfOf: line.onBehalfOf,\n userProvided: line.userProvided,\n fallback: line.fallback,\n archiveDespiteRobots: line.archiveDespiteRobots,\n});\n\n/**\n * This function does the command `venue`, and gives the exit code. It reads the hours of the venue\n * with `readVenue`. With `--compare`, it compares the week of the site (`from_site`) with the kept\n * week through `compareHours`.\n */\nexport const venueCommand = async (\n target: RecipeTarget,\n line: RecipeLine,\n input: RecipeInput,\n io: RecipeIo,\n): Promise<number> => {\n const options: VenueOptions = {\n ...sharedOptions(line),\n name: line.name,\n requireName: line.requireName,\n location: line.location,\n osm: line.osm,\n pages: line.pages,\n };\n const result = await readVenue(target, line.url, options);\n const site = result.from_site;\n const compare: VenueCompare | undefined =\n input.kept === undefined\n ? undefined\n : {\n file: input.kept.file,\n kept: input.kept.week,\n comparison: site === null ? null : compareHours(input.kept.week, site.week),\n };\n io.out(\n io.json\n ? JSON.stringify(compare === undefined ? result : { ...result, compare }, null, 2)\n : describeVenue(result, compare, io.renderOff).join(\"\\n\"),\n );\n return venueCode(result);\n};\n\n/** This function does the command `product`, and gives the exit code. It reads the page with `readProduct`. */\nexport const productCommand = async (\n target: RecipeTarget,\n line: RecipeLine,\n io: RecipeIo,\n): Promise<number> => {\n const result = await readProduct(target, line.url, sharedOptions(line));\n io.out(\n io.json ? JSON.stringify(result, null, 2) : describeProduct(result, io.renderOff).join(\"\\n\"),\n );\n return productCode(result);\n};\n"],"mappings":";;;;;;;;AAyCA,MAAa,iBAAiB,SAAkC;CAC9D,IAAI,KAAK,QAAQ,KAAA,KAAa,YAAY,KAAK,KAAK,KAAK,MAAM,KAAA,GAC7D,MAAM,IAAI,WACR,wCAAwC,KAAK,IAAI,6EACnD;CAEF,OAAO,EAAE,MAAM,KAAK,YAAY,KAAA,IAAY,KAAA,IAAY,WAAW,KAAK,OAAO,EAAE;AACnF;;AAGA,MAAM,iBAAiB,UAAsC;CAC3D,QAAQ,KAAK;CACb,QAAQ,KAAK;CACb,QAAQ,KAAK;CACb,YAAY,KAAK;CACjB,cAAc,KAAK;CACnB,UAAU,KAAK;CACf,sBAAsB,KAAK;AAC7B;;;;;;AAOA,MAAa,eAAe,OAC1B,QACA,MACA,OACA,OACoB;CACpB,MAAM,UAAwB;EAC5B,GAAG,cAAc,IAAI;EACrB,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,UAAU,KAAK;EACf,KAAK,KAAK;EACV,OAAO,KAAK;CACd;CACA,MAAM,SAAS,MAAM,UAAU,QAAQ,KAAK,KAAK,OAAO;CACxD,MAAM,OAAO,OAAO;CACpB,MAAM,UACJ,MAAM,SAAS,KAAA,IACX,KAAA,IACA;EACE,MAAM,MAAM,KAAK;EACjB,MAAM,MAAM,KAAK;EACjB,YAAY,SAAS,OAAO,OAAO,aAAa,MAAM,KAAK,MAAM,KAAK,IAAI;CAC5E;CACN,GAAG,IACD,GAAG,OACC,KAAK,UAAU,YAAY,KAAA,IAAY,SAAS;EAAE,GAAG;EAAQ;CAAQ,GAAG,MAAM,CAAC,IAC/E,cAAc,QAAQ,SAAS,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,CAC5D;CACA,OAAO,UAAU,MAAM;AACzB;;AAGA,MAAa,iBAAiB,OAC5B,QACA,MACA,OACoB;CACpB,MAAM,SAAS,MAAM,YAAY,QAAQ,KAAK,KAAK,cAAc,IAAI,CAAC;CACtE,GAAG,IACD,GAAG,OAAO,KAAK,UAAU,QAAQ,MAAM,CAAC,IAAI,gBAAgB,QAAQ,GAAG,SAAS,CAAC,CAAC,KAAK,IAAI,CAC7F;CACA,OAAO,YAAY,MAAM;AAC3B"}
|