polite-crawler 0.1.2 → 0.1.3
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 +533 -8
- package/build/cli-daemon--cTmq0Z5.mjs +38 -0
- package/build/cli-daemon--cTmq0Z5.mjs.map +1 -0
- package/build/cli.mjs +288 -20
- package/build/cli.mjs.map +1 -1
- package/build/{open-Ca1lcWR3.d.mts → daemon-DP4EqjFL.d.mts} +913 -16
- package/build/daemon-fZTE24P5.mjs +515 -0
- package/build/daemon-fZTE24P5.mjs.map +1 -0
- package/build/{diff-Dk2eSvi-.mjs → diff-NQcQqKe9.mjs} +2 -2
- package/build/{diff-Dk2eSvi-.mjs.map → diff-NQcQqKe9.mjs.map} +1 -1
- package/build/{extract-DcwNrTK7.mjs → extract-ByFfc8Zk.mjs} +2 -2
- package/build/{extract-DcwNrTK7.mjs.map → extract-ByFfc8Zk.mjs.map} +1 -1
- package/build/hours.d.mts +26 -0
- package/build/hours.mjs +48 -1
- package/build/hours.mjs.map +1 -1
- package/build/index.d.mts +491 -11
- package/build/index.mjs +898 -9
- package/build/index.mjs.map +1 -0
- package/build/loopback-C_QYowSf.mjs +22 -0
- package/build/loopback-C_QYowSf.mjs.map +1 -0
- package/build/mcp.d.mts +158 -2
- package/build/mcp.mjs +567 -18
- package/build/mcp.mjs.map +1 -1
- package/build/ollama-BJxkjny4.mjs +160 -0
- package/build/ollama-BJxkjny4.mjs.map +1 -0
- package/build/{open-DnrWgkC0.mjs → open-C4fhXJgD.mjs} +1404 -21
- package/build/open-C4fhXJgD.mjs.map +1 -0
- package/build/{runtime-k8mo1_7U.mjs → runtime-JToRO63o.mjs} +3 -3
- package/build/{runtime-k8mo1_7U.mjs.map → runtime-JToRO63o.mjs.map} +1 -1
- package/build/transformers-BQMlaGj4.mjs +133 -0
- package/build/transformers-BQMlaGj4.mjs.map +1 -0
- package/build/{duration-iIuMMofi.mjs → version-ntbY-6wh.mjs} +10 -10
- package/build/version-ntbY-6wh.mjs.map +1 -0
- package/build/{wacz-sfOLT8e9.mjs → wacz-BFNG42Kt.mjs} +3 -3
- package/build/{wacz-sfOLT8e9.mjs.map → wacz-BFNG42Kt.mjs.map} +1 -1
- package/build/{warc-VIPscWxY.mjs → warc-CVS0t1fW.mjs} +2 -2
- package/build/{warc-VIPscWxY.mjs.map → warc-CVS0t1fW.mjs.map} +1 -1
- package/package.json +10 -2
- package/build/duration-iIuMMofi.mjs.map +0 -1
- package/build/open-DnrWgkC0.mjs.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { t as describeDaemonStatus } from "./cli.mjs";
|
|
2
|
+
//#region src/cli-daemon.ts
|
|
3
|
+
/**
|
|
4
|
+
* This function does the command `daemon`, and gives the exit code. `daemon status` gives 0 when a
|
|
5
|
+
* daemon operates on the store, and 1 when none operates. `daemon` gives 0 after a clean stop. An
|
|
6
|
+
* error at start rejects, so the command exits with 1.
|
|
7
|
+
*/
|
|
8
|
+
const daemonCommand = async (line, options, io) => {
|
|
9
|
+
const { changeCommand, readDaemonState, startDaemon } = await import("./daemon-fZTE24P5.mjs").then((n) => n.a);
|
|
10
|
+
if (line.action === "status") {
|
|
11
|
+
const state = readDaemonState(options.dir);
|
|
12
|
+
io.out(io.json ? JSON.stringify(state ?? null, null, 2) : describeDaemonStatus(state, options.dir));
|
|
13
|
+
return state?.alive === true ? 0 : 1;
|
|
14
|
+
}
|
|
15
|
+
await (await startDaemon({
|
|
16
|
+
...options,
|
|
17
|
+
tick: line.tickMs,
|
|
18
|
+
jitter: line.jitterMs,
|
|
19
|
+
budget: line.budgetMs,
|
|
20
|
+
maxPages: line.maxPages,
|
|
21
|
+
grace: line.graceMs,
|
|
22
|
+
events: line.events,
|
|
23
|
+
onChange: line.onChange === void 0 ? void 0 : changeCommand(line.onChange),
|
|
24
|
+
mcp: line.mcp === void 0 ? void 0 : {
|
|
25
|
+
port: line.mcp.port,
|
|
26
|
+
host: line.mcp.host,
|
|
27
|
+
allowRemote: line.mcp.allowRemote,
|
|
28
|
+
tokenFile: line.mcp.tokenFile,
|
|
29
|
+
allowUserProvided: io.allowUserProvided
|
|
30
|
+
},
|
|
31
|
+
signals: true
|
|
32
|
+
})).done;
|
|
33
|
+
return 0;
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { daemonCommand };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=cli-daemon--cTmq0Z5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-daemon--cTmq0Z5.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"}
|
package/build/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as
|
|
2
|
+
import { i as durationMs, r as VERSION } from "./version-ntbY-6wh.mjs";
|
|
3
|
+
import { t as isLoopbackHost } from "./loopback-C_QYowSf.mjs";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
@@ -80,30 +81,62 @@ const OPTIONS = {
|
|
|
80
81
|
"archive-despite-robots": { type: "boolean" },
|
|
81
82
|
"sign-key": { type: "string" },
|
|
82
83
|
"signature-agent": { type: "string" },
|
|
84
|
+
tick: { type: "string" },
|
|
85
|
+
jitter: { type: "string" },
|
|
86
|
+
"tick-budget": { type: "string" },
|
|
87
|
+
"tick-pages": { type: "string" },
|
|
88
|
+
grace: { type: "string" },
|
|
89
|
+
events: { type: "string" },
|
|
90
|
+
"on-change": { type: "string" },
|
|
91
|
+
"mcp-http": { type: "boolean" },
|
|
92
|
+
"mcp-port": { type: "string" },
|
|
93
|
+
"mcp-host": { type: "string" },
|
|
94
|
+
"mcp-token-file": { type: "string" },
|
|
95
|
+
"allow-remote": { type: "boolean" },
|
|
83
96
|
field: {
|
|
84
97
|
type: "string",
|
|
85
98
|
multiple: true
|
|
86
99
|
},
|
|
87
100
|
capture: { type: "string" },
|
|
101
|
+
embedder: { type: "string" },
|
|
102
|
+
"embed-model": { type: "string" },
|
|
103
|
+
"ollama-url": { type: "string" },
|
|
104
|
+
"allow-remote-ollama": { type: "boolean" },
|
|
105
|
+
"max-docs": { type: "string" },
|
|
88
106
|
...EXPORT_OPTIONS
|
|
89
107
|
};
|
|
108
|
+
/** These options belong to `daemon`, and not to `daemon status`. */
|
|
109
|
+
const DAEMON_RUN_OPTIONS = [
|
|
110
|
+
"tick",
|
|
111
|
+
"jitter",
|
|
112
|
+
"tick-budget",
|
|
113
|
+
"tick-pages",
|
|
114
|
+
"grace",
|
|
115
|
+
"events",
|
|
116
|
+
"on-change",
|
|
117
|
+
"mcp-http",
|
|
118
|
+
"mcp-port",
|
|
119
|
+
"mcp-host",
|
|
120
|
+
"mcp-token-file",
|
|
121
|
+
"allow-remote"
|
|
122
|
+
];
|
|
90
123
|
/** These options belong to one command, or to the commands that the text names. */
|
|
91
124
|
const OWNER = {
|
|
92
125
|
render: "fetch crawl watch",
|
|
93
126
|
"max-age": "fetch crawl discover",
|
|
94
127
|
limit: "find",
|
|
95
|
-
kind: "find",
|
|
128
|
+
kind: "find embed",
|
|
96
129
|
rank: "find",
|
|
97
130
|
from: "read diff",
|
|
98
131
|
length: "read",
|
|
99
132
|
"user-provided": "read extract",
|
|
100
|
-
"allow-user-provided": "mcp",
|
|
133
|
+
"allow-user-provided": "mcp daemon",
|
|
101
134
|
...Object.fromEntries(Object.keys(EXPORT_OPTIONS).map((option) => [option, "export"])),
|
|
102
135
|
"max-urls": "discover",
|
|
103
136
|
feed: "discover",
|
|
104
137
|
"max-pages": "crawl watch",
|
|
105
138
|
"max-depth": "crawl",
|
|
106
|
-
budget: "crawl watch",
|
|
139
|
+
budget: "crawl watch embed",
|
|
107
140
|
include: "crawl",
|
|
108
141
|
exclude: "crawl",
|
|
109
142
|
sitemaps: "crawl",
|
|
@@ -116,10 +149,16 @@ const OWNER = {
|
|
|
116
149
|
label: "watch",
|
|
117
150
|
"archive-fallback": "fetch",
|
|
118
151
|
"archive-despite-robots": "fetch",
|
|
119
|
-
"sign-key": "fetch discover crawl watch mcp key-directory",
|
|
120
|
-
"signature-agent": "fetch discover crawl watch mcp key-directory",
|
|
152
|
+
"sign-key": "fetch discover crawl watch mcp key-directory daemon",
|
|
153
|
+
"signature-agent": "fetch discover crawl watch mcp key-directory daemon",
|
|
154
|
+
...Object.fromEntries(DAEMON_RUN_OPTIONS.map((option) => [option, "daemon"])),
|
|
121
155
|
field: "extract",
|
|
122
|
-
capture: "extract"
|
|
156
|
+
capture: "extract",
|
|
157
|
+
embedder: "find embed mcp",
|
|
158
|
+
"embed-model": "find embed mcp",
|
|
159
|
+
"ollama-url": "find embed mcp",
|
|
160
|
+
"allow-remote-ollama": "find embed mcp",
|
|
161
|
+
"max-docs": "embed"
|
|
123
162
|
};
|
|
124
163
|
/** These are the commands, with the smallest and the largest count of arguments. */
|
|
125
164
|
const COMMANDS = {
|
|
@@ -178,6 +217,11 @@ const COMMANDS = {
|
|
|
178
217
|
max: Infinity,
|
|
179
218
|
wants: "watch wants add, remove, list or run"
|
|
180
219
|
},
|
|
220
|
+
daemon: {
|
|
221
|
+
min: 0,
|
|
222
|
+
max: 1,
|
|
223
|
+
wants: "daemon takes no argument, or status"
|
|
224
|
+
},
|
|
181
225
|
"key-directory": {
|
|
182
226
|
min: 0,
|
|
183
227
|
max: 0,
|
|
@@ -187,6 +231,11 @@ const COMMANDS = {
|
|
|
187
231
|
min: 1,
|
|
188
232
|
max: 1,
|
|
189
233
|
wants: "extract wants one document id and --field name=value"
|
|
234
|
+
},
|
|
235
|
+
embed: {
|
|
236
|
+
min: 0,
|
|
237
|
+
max: 0,
|
|
238
|
+
wants: "embed takes no argument: give the model with --embedder"
|
|
190
239
|
}
|
|
191
240
|
};
|
|
192
241
|
const parse = (args) => {
|
|
@@ -424,6 +473,102 @@ const describeEvent = (event) => {
|
|
|
424
473
|
].join("\n");
|
|
425
474
|
};
|
|
426
475
|
const describeWatchSummary = (summary) => `${String(summary.checked)} checked: ${String(summary.new)} new, ${String(summary.changed)} changed, ${String(summary.unchanged)} unchanged, ${String(summary.gone)} gone, ${String(summary.refused)} refused, ${String(summary.error)} errors. ${String(summary.suspected)} suspected, ${String(summary.confirmed)} confirmed, ${String(summary.due)} still due. Stopped (${summary.stopped}): ${summary.reason}.`;
|
|
476
|
+
/** This function reads a file name of the command line: a leading `~` is the home folder. */
|
|
477
|
+
const fileOf = (text) => text === "~" ? homedir() : /^~[\\/]/.test(text) ? resolve(homedir(), text.slice(2)) : resolve(text);
|
|
478
|
+
/** This function reads a duration of `daemon`, with a floor in milliseconds. */
|
|
479
|
+
const daemonDuration = (name, text, floor) => {
|
|
480
|
+
if (text === void 0) return void 0;
|
|
481
|
+
let ms;
|
|
482
|
+
try {
|
|
483
|
+
ms = durationMs(text);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
throw new UsageError(`--${name}: ${error.message}`, { cause: error });
|
|
486
|
+
}
|
|
487
|
+
if (ms < floor) throw new UsageError(floor === 0 ? `--${name} wants 0 or more` : `--${name} wants ${String(floor / 1e3)}s or more`);
|
|
488
|
+
return ms;
|
|
489
|
+
};
|
|
490
|
+
/** These options of `daemon` belong to its MCP server, so they need `--mcp-http`. */
|
|
491
|
+
const MCP_OPTIONS = [
|
|
492
|
+
"mcp-port",
|
|
493
|
+
"mcp-host",
|
|
494
|
+
"mcp-token-file",
|
|
495
|
+
"allow-remote"
|
|
496
|
+
];
|
|
497
|
+
/** This function checks the options of `daemon` and of `daemon status`. */
|
|
498
|
+
const daemonLineOf = (values, rest) => {
|
|
499
|
+
const [action] = rest;
|
|
500
|
+
if (action !== void 0 && action !== "status") throw new UsageError(`daemon takes no argument, or status, not "${action}"`);
|
|
501
|
+
if (action === "status") {
|
|
502
|
+
const run = [
|
|
503
|
+
...DAEMON_RUN_OPTIONS,
|
|
504
|
+
"allow-user-provided",
|
|
505
|
+
"sign-key",
|
|
506
|
+
"signature-agent"
|
|
507
|
+
];
|
|
508
|
+
for (const option of run) if (values[option] !== void 0) throw new UsageError(`--${option} is an option of daemon, not of daemon status`);
|
|
509
|
+
}
|
|
510
|
+
if (values["mcp-http"] !== true) {
|
|
511
|
+
for (const option of MCP_OPTIONS) if (values[option] !== void 0) throw new UsageError(`--${option} goes with --mcp-http`);
|
|
512
|
+
}
|
|
513
|
+
const host = (values["mcp-host"] ?? "127.0.0.1").trim();
|
|
514
|
+
if (host === "") throw new UsageError("--mcp-host wants a host");
|
|
515
|
+
const allowRemote = values["allow-remote"] === true;
|
|
516
|
+
if (values["mcp-http"] === true && !isLoopbackHost(host) && !allowRemote) throw new UsageError(`--mcp-host ${host} is not a loopback address: add --allow-remote to serve other computers, after you read the warning in the README`);
|
|
517
|
+
const port = whole("mcp-port", values["mcp-port"], 9307, 0);
|
|
518
|
+
if (port > 65535) throw new UsageError("--mcp-port wants a port from 0 to 65535");
|
|
519
|
+
const onChange = values["on-change"];
|
|
520
|
+
if (onChange !== void 0 && onChange.trim() === "") throw new UsageError("--on-change wants a command");
|
|
521
|
+
const events = values.events;
|
|
522
|
+
if (events !== void 0 && events.trim() === "") throw new UsageError("--events wants a file");
|
|
523
|
+
const tokenFile = values["mcp-token-file"];
|
|
524
|
+
if (tokenFile !== void 0 && tokenFile.trim() === "") throw new UsageError("--mcp-token-file wants a file");
|
|
525
|
+
return {
|
|
526
|
+
action: action === "status" ? "status" : "run",
|
|
527
|
+
tickMs: daemonDuration("tick", values.tick, 1e3),
|
|
528
|
+
jitterMs: daemonDuration("jitter", values.jitter, 0),
|
|
529
|
+
budgetMs: daemonDuration("tick-budget", values["tick-budget"], 1e3),
|
|
530
|
+
maxPages: values["tick-pages"] === void 0 ? void 0 : whole("tick-pages", values["tick-pages"], 1, 1),
|
|
531
|
+
graceMs: daemonDuration("grace", values.grace, 0),
|
|
532
|
+
events: events === void 0 ? void 0 : fileOf(events),
|
|
533
|
+
onChange,
|
|
534
|
+
mcp: values["mcp-http"] === true ? {
|
|
535
|
+
port,
|
|
536
|
+
host,
|
|
537
|
+
allowRemote,
|
|
538
|
+
tokenFile: tokenFile === void 0 ? void 0 : fileOf(tokenFile)
|
|
539
|
+
} : void 0
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
/** This function gives the first line of `daemon status`: the daemon runs, stopped, or stopped with no clean stop. */
|
|
543
|
+
const daemonHead = (state) => state.alive ? `the daemon runs (process ${String(state.pid)}) since ${state.started_at}, on the store ${state.store}` : state.running ? `the daemon of process ${String(state.pid)} does not run: it stopped with no clean stop. Its state is of ${state.updated_at}` : `the daemon stopped at ${state.stopped_at ?? state.updated_at}`;
|
|
544
|
+
/** This function gives the line of the last tick of `daemon status`. */
|
|
545
|
+
const lastTick = (state) => {
|
|
546
|
+
const tick = state.last_tick;
|
|
547
|
+
if (tick === null) return "last tick: none yet";
|
|
548
|
+
if (tick.summary === null) return `last tick: ${tick.ended_at}, failed: ${tick.error ?? "error"}`;
|
|
549
|
+
const summary = tick.summary;
|
|
550
|
+
return `last tick: ${tick.ended_at}, ${String(summary.checked)} checked, ${String(summary.confirmed)} changes, ${String(summary.refused)} refused, ${String(summary.error)} errors (${summary.stopped})`;
|
|
551
|
+
};
|
|
552
|
+
/** This function gives the text of `daemon status`. */
|
|
553
|
+
const describeDaemonStatus = (state, dir) => {
|
|
554
|
+
if (state === void 0) return `no daemon has run on the store ${dir}`;
|
|
555
|
+
const lines = [daemonHead(state), lastTick(state)];
|
|
556
|
+
if (state.alive && state.next_tick_at !== null) lines.push(`next tick: ${state.next_tick_at}`);
|
|
557
|
+
lines.push(`${String(state.ticks)} ticks, ${String(state.checked)} checks, ${String(state.changes)} changes`);
|
|
558
|
+
if (state.next_due.length === 0) lines.push("next due: no watch");
|
|
559
|
+
else {
|
|
560
|
+
lines.push("next due:");
|
|
561
|
+
for (const due of state.next_due) lines.push(` ${due.next_at} ${due.url} (every ${everyText(due.every_ms)}, last ${due.last_kind ?? "none"})`);
|
|
562
|
+
}
|
|
563
|
+
if (state.mcp !== null) {
|
|
564
|
+
const where = state.mcp.token_file === null ? "" : ` (the token is in ${state.mcp.token_file})`;
|
|
565
|
+
lines.push(`MCP: ${state.mcp.url}${where}`);
|
|
566
|
+
lines.push(state.tasks.length === 0 ? "tasks: none running" : `tasks: ${String(state.tasks.length)} running`);
|
|
567
|
+
for (const task of state.tasks) lines.push(` ${task.id} ${task.tool} ${task.status}${task.message === null ? "" : `: ${task.message}`}`);
|
|
568
|
+
}
|
|
569
|
+
lines.push(state.last_error === null ? "last error: none" : `last error: ${state.last_error.at}: ${state.last_error.message}`);
|
|
570
|
+
return lines.join("\n");
|
|
571
|
+
};
|
|
427
572
|
/** These are the kinds of a field (`FIELD_KINDS` of `extract.ts`). The command line knows them before it loads the crawler. */
|
|
428
573
|
const KINDS = [
|
|
429
574
|
"text",
|
|
@@ -463,6 +608,33 @@ const fieldsOf = (given) => {
|
|
|
463
608
|
}
|
|
464
609
|
return fields;
|
|
465
610
|
};
|
|
611
|
+
/**
|
|
612
|
+
* This function reads the embedder of the command line, or of the environment for the commands
|
|
613
|
+
* that use one. `--embed-model` and `--ollama-url` need `--embedder`.
|
|
614
|
+
*/
|
|
615
|
+
const embedderOf = (values, env, command) => {
|
|
616
|
+
const uses = OWNER["embedder"].split(" ").includes(command);
|
|
617
|
+
const fromEnv = (name) => uses ? env[name] || void 0 : void 0;
|
|
618
|
+
const name = values.embedder ?? fromEnv("POLITE_CRAWLER_EMBEDDER");
|
|
619
|
+
const model = values["embed-model"] ?? fromEnv("POLITE_CRAWLER_EMBED_MODEL");
|
|
620
|
+
const url = values["ollama-url"] ?? fromEnv("POLITE_CRAWLER_OLLAMA_URL");
|
|
621
|
+
const allowRemote = values["allow-remote-ollama"] === true || uses && envFlag(env, "POLITE_CRAWLER_ALLOW_REMOTE_OLLAMA");
|
|
622
|
+
if (name === void 0) {
|
|
623
|
+
if (values["embed-model"] !== void 0 || values["ollama-url"] !== void 0) throw new UsageError("--embed-model and --ollama-url go with --embedder transformers or ollama");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (name !== "transformers" && name !== "ollama") throw new UsageError(`--embedder is transformers or ollama, not "${name}"`);
|
|
627
|
+
if (model !== void 0 && model.trim() === "") throw new UsageError("--embed-model wants a model");
|
|
628
|
+
if (values["ollama-url"] !== void 0 && name !== "ollama") throw new UsageError("--ollama-url goes with --embedder ollama");
|
|
629
|
+
return {
|
|
630
|
+
kind: name,
|
|
631
|
+
model,
|
|
632
|
+
url: name === "ollama" ? url : void 0,
|
|
633
|
+
allowRemote
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
/** This function gives the text of `embed`: the counts and the reason of the stop. */
|
|
637
|
+
const describeEmbed = (result) => [`${String(result.embedded)} texts got vectors (${String(result.chunks)} chunks); ${String(result.reused)} documents had vectors; ${String(result.held_back)} held back, so not sent to the model`, `${String(result.docs)} documents examined in ${String(result.ms)} ms with ${result.embedder}. Stopped (${result.stopped})${result.stopped === "done" ? "" : ": start embed again for the rest"}.`].join("\n");
|
|
466
638
|
/** This function gives the pages of `fetch`: a rejected fetch is an error of its URL only. A page that needs a browser names the reason when rendering is off. */
|
|
467
639
|
const fetchedPages = (urls, settled, renderOff) => settled.map((result, at) => {
|
|
468
640
|
const url = urls[at];
|
|
@@ -477,7 +649,7 @@ const fetchedPages = (urls, settled, renderOff) => settled.map((result, at) => {
|
|
|
477
649
|
} : page;
|
|
478
650
|
});
|
|
479
651
|
/** This function gives the text of the results of `find`. An archived copy says so. */
|
|
480
|
-
const describeFound = (found) => found.length === 0 ? "nothing stored matches" : found.map((hit) => `${hit.doc} ${hit.title}${hit.archived === void 0 ? "" : ` [archived copy, captured ${hit.archived.timestamp}]`}\n ${hit.snippet.replace(/\s+/g, " ")}`).join("\n");
|
|
652
|
+
const describeFound = (found) => found.length === 0 ? "nothing stored matches" : found.map((hit) => `${hit.doc} ${hit.title}${hit.archived === void 0 ? "" : ` [archived copy, captured ${hit.archived.timestamp}]`}${hit.passage === void 0 ? "" : ` [passage ${String(hit.passage.start)}–${String(hit.passage.end)}]`}\n ${hit.snippet.replace(/\s+/g, " ")}`).join("\n");
|
|
481
653
|
/** This function gives the text of `read`. With `--at`, the head names the capture and its fetch time. */
|
|
482
654
|
const describeRead = (read, at) => {
|
|
483
655
|
const version = at === void 0 ? "" : ` ${read.capture} (${read.fetched_at})`;
|
|
@@ -577,7 +749,11 @@ const readLine = (args, env) => {
|
|
|
577
749
|
const render = values.render ?? "auto";
|
|
578
750
|
if (render !== "auto" && render !== "never" && render !== "always") throw new UsageError(`--render is auto, never or always, not "${render}"`);
|
|
579
751
|
const rank = values.rank ?? "exact";
|
|
580
|
-
if (rank !== "exact" && rank !== "fast") throw new UsageError(`--rank is exact or
|
|
752
|
+
if (rank !== "exact" && rank !== "fast" && rank !== "hybrid") throw new UsageError(`--rank is exact, fast or hybrid, not "${rank}"`);
|
|
753
|
+
const embedder = embedderOf(values, env, command);
|
|
754
|
+
if (rank === "hybrid" && embedder === void 0) throw new UsageError("--rank hybrid wants --embedder transformers or ollama");
|
|
755
|
+
if (command === "embed" && embedder === void 0) throw new UsageError("embed wants --embedder transformers or ollama");
|
|
756
|
+
const maxDocs = values["max-docs"] === void 0 ? void 0 : whole("max-docs", values["max-docs"], 1, 1);
|
|
581
757
|
const limit = whole("limit", values.limit, 10, 1);
|
|
582
758
|
const from = command === "read" ? whole("from", values.from, 0, 0) : 0;
|
|
583
759
|
const length = whole("length", values.length, 4e3, 1);
|
|
@@ -587,11 +763,12 @@ const readLine = (args, env) => {
|
|
|
587
763
|
const diffTo = versionOf("to", values.to);
|
|
588
764
|
const watchLine = command === "watch" ? watchLineOf(values, rest) : void 0;
|
|
589
765
|
const extractFields = command === "extract" ? fieldsOf(values.field) : void 0;
|
|
766
|
+
const daemonLine = command === "daemon" ? daemonLineOf(values, rest) : void 0;
|
|
590
767
|
const dir = storeDir(values.dir ?? (env["POLITE_CRAWLER_DIR"] || void 0));
|
|
591
768
|
const allowPrivate = values["allow-private"] === true || envFlag(env, "POLITE_CRAWLER_ALLOW_PRIVATE");
|
|
592
|
-
const allowUserProvided = values["allow-user-provided"] === true || command === "mcp" && envFlag(env, "POLITE_CRAWLER_ALLOW_USER_PROVIDED");
|
|
769
|
+
const allowUserProvided = values["allow-user-provided"] === true || (command === "mcp" || daemonLine?.action === "run") && envFlag(env, "POLITE_CRAWLER_ALLOW_USER_PROVIDED");
|
|
593
770
|
if (values["archive-despite-robots"] === true && values["archive-fallback"] !== true) throw new UsageError("--archive-despite-robots works only with --archive-fallback");
|
|
594
|
-
const signs = OWNER["sign-key"].split(" ").includes(command) && (watchLine === void 0 || watchLine.action === "run");
|
|
771
|
+
const signs = OWNER["sign-key"].split(" ").includes(command) && (watchLine === void 0 || watchLine.action === "run") && (daemonLine === void 0 || daemonLine.action === "run");
|
|
595
772
|
const signKey = values["sign-key"] ?? (signs ? env["POLITE_CRAWLER_SIGN_KEY"] || void 0 : void 0);
|
|
596
773
|
const signatureAgent = values["signature-agent"] ?? (signs ? env["POLITE_CRAWLER_SIGNATURE_AGENT"] || void 0 : void 0);
|
|
597
774
|
if (command === "key-directory") {
|
|
@@ -619,6 +796,7 @@ const readLine = (args, env) => {
|
|
|
619
796
|
diffFrom,
|
|
620
797
|
diffTo,
|
|
621
798
|
watchLine,
|
|
799
|
+
daemonLine,
|
|
622
800
|
extractFields,
|
|
623
801
|
exportPlan: command === "export" ? exportPlan(values) : void 0,
|
|
624
802
|
dir,
|
|
@@ -626,7 +804,9 @@ const readLine = (args, env) => {
|
|
|
626
804
|
allowPrivate,
|
|
627
805
|
allowUserProvided,
|
|
628
806
|
signKey,
|
|
629
|
-
signatureAgent
|
|
807
|
+
signatureAgent,
|
|
808
|
+
embedder,
|
|
809
|
+
maxDocs
|
|
630
810
|
};
|
|
631
811
|
};
|
|
632
812
|
//#endregion
|
|
@@ -656,9 +836,13 @@ Usage:
|
|
|
656
836
|
polite-crawler watch remove <url...> stop the watch of these pages
|
|
657
837
|
polite-crawler watch list print the watch list
|
|
658
838
|
polite-crawler watch run check the pages that are due, within a budget
|
|
839
|
+
polite-crawler daemon check the watch list on a schedule, until SIGINT or SIGTERM;
|
|
840
|
+
with --mcp-http, serve the MCP tools over local HTTP too
|
|
841
|
+
polite-crawler daemon status print the state of the daemon of the store
|
|
659
842
|
polite-crawler key-directory print the key directory (JWKS) of --sign-key, to host
|
|
660
843
|
polite-crawler extract <doc> --field name=value[:kind]...
|
|
661
844
|
check proposed field values against a stored page
|
|
845
|
+
polite-crawler embed --embedder <name> give vectors to the stored pages, for find --rank hybrid
|
|
662
846
|
|
|
663
847
|
Options:
|
|
664
848
|
--dir <path> the store (default $POLITE_CRAWLER_DIR, or ~/.polite-crawler)
|
|
@@ -670,6 +854,13 @@ Options:
|
|
|
670
854
|
($POLITE_CRAWLER_SIGN_KEY)
|
|
671
855
|
--signature-agent <url> the https origin of your key directory, or the URL of a
|
|
672
856
|
JSON Web Key Set on a path, for --sign-key ($POLITE_CRAWLER_SIGNATURE_AGENT)
|
|
857
|
+
--embedder <name> the local model of hybrid search, for find, embed and mcp: transformers
|
|
858
|
+
(transformers.js in this process) or ollama ($POLITE_CRAWLER_EMBEDDER)
|
|
859
|
+
--embed-model <m> the model: Xenova/bge-small-en-v1.5 (transformers) or nomic-embed-text
|
|
860
|
+
(ollama) by default ($POLITE_CRAWLER_EMBED_MODEL)
|
|
861
|
+
--ollama-url <url> the Ollama server, http://127.0.0.1:11434 by default ($POLITE_CRAWLER_OLLAMA_URL)
|
|
862
|
+
--allow-remote-ollama permit an Ollama server on a different computer: the page text goes
|
|
863
|
+
to it ($POLITE_CRAWLER_ALLOW_REMOTE_OLLAMA=1)
|
|
673
864
|
--json print JSON
|
|
674
865
|
-v, --version print the version
|
|
675
866
|
-h, --help print this help
|
|
@@ -683,7 +874,8 @@ Options:
|
|
|
683
874
|
find --limit <n> how many results (default 10)
|
|
684
875
|
--kind <kind> only this kind of document, such as page or pdf
|
|
685
876
|
--rank <mode> exact (default): BM25 over each match; fast: leave common words
|
|
686
|
-
out of the one-word part, faster for a long question
|
|
877
|
+
out of the one-word part, faster for a long question; hybrid: BM25
|
|
878
|
+
and the meaning of the text, fused (needs --embedder)
|
|
687
879
|
read --from <n> where the range starts (default 0)
|
|
688
880
|
--length <n> how many characters (default 4000)
|
|
689
881
|
--user-provided you gave this URL yourself: read a page that refuses AI input (logged)
|
|
@@ -720,17 +912,38 @@ Options:
|
|
|
720
912
|
--render <mode> as for fetch
|
|
721
913
|
watch run --budget <time> the longest time (default 10m); no request starts after it
|
|
722
914
|
--max-pages <n> how many pages to check (default 100)
|
|
915
|
+
daemon --tick <time> the longest wait between two ticks: 1s or more (default 1m)
|
|
916
|
+
--jitter <time> a random wait from 0 to this after each tick (default 10s)
|
|
917
|
+
--tick-budget <time> the longest time of the checks of one tick (default 10m)
|
|
918
|
+
--tick-pages <n> how many pages one tick checks at most (default 100)
|
|
919
|
+
--grace <time> at a stop, the checks in progress get this time to end (default 10s)
|
|
920
|
+
--events <file> write each change as one JSON line to this file (default: the
|
|
921
|
+
standard output)
|
|
922
|
+
--on-change <command> start this command of yours for each change: it gets the
|
|
923
|
+
event as one JSON line on its standard input
|
|
924
|
+
--mcp-http serve the MCP tools over HTTP; each request needs the bearer token
|
|
925
|
+
--mcp-port <n> the port (default 9307)
|
|
926
|
+
--mcp-host <host> the host to bind (default 127.0.0.1)
|
|
927
|
+
--mcp-token-file <file> read the token from this file (default: a new token at each
|
|
928
|
+
start, in <store>/daemon/mcp-token)
|
|
929
|
+
--allow-remote permit a --mcp-host that is not loopback: read the README first
|
|
930
|
+
--allow-user-provided as for mcp
|
|
931
|
+
daemon status exit code 0 when a daemon runs on the store, 1 when none runs
|
|
723
932
|
extract --field <name=value[:kind]> a value to check, never made up by the crawler (repeat it);
|
|
724
933
|
kind: text (default), number, money, date, url, email, phone,
|
|
725
934
|
or json-ld:<path> such as json-ld:Product.offers.price
|
|
726
935
|
--capture <id> check the text of this capture, not the current text
|
|
727
936
|
--user-provided as for read
|
|
937
|
+
embed --budget <time> the longest time (default 10m); no embedding starts after it
|
|
938
|
+
--max-docs <n> how many texts get vectors at most
|
|
939
|
+
--kind <kind> only this kind of document
|
|
728
940
|
|
|
729
941
|
Exit codes:
|
|
730
942
|
0 done
|
|
731
943
|
1 not done: a page not fetched, no match, a quote not found, a document unknown or
|
|
732
944
|
held back, a watch check that failed or that is still due, a field value that extract
|
|
733
|
-
did not find or that is not clear,
|
|
945
|
+
did not find or that is not clear, embed that stopped before each page had vectors,
|
|
946
|
+
or an error
|
|
734
947
|
(fetch: a page with an archived copy counts as done)
|
|
735
948
|
2 the command line is wrong
|
|
736
949
|
`;
|
|
@@ -739,7 +952,7 @@ Exit codes:
|
|
|
739
952
|
* command line first. The export only reads the store: it makes no store, and it sends no request.
|
|
740
953
|
*/
|
|
741
954
|
const exportCommand = async (plan, dir, json) => {
|
|
742
|
-
const result = plan.format === "wacz" ? await (await import("./wacz-
|
|
955
|
+
const result = plan.format === "wacz" ? await (await import("./wacz-BFNG42Kt.mjs").then((n) => n.n)).exportWacz(dir, plan.options) : await (await import("./warc-CVS0t1fW.mjs").then((n) => n.a)).exportWarc(dir, plan.options);
|
|
743
956
|
process.stdout.write(`${json ? JSON.stringify(result, null, 2) : describeExport(result, plan.options.includeRefusals)}\n`);
|
|
744
957
|
return 0;
|
|
745
958
|
};
|
|
@@ -861,7 +1074,7 @@ const runWatchCommand = async (crawler, line, values, limits, json, out) => {
|
|
|
861
1074
|
};
|
|
862
1075
|
/** This function gives the text of a diff for a person: the two captures, then the text of `formatDiff`. */
|
|
863
1076
|
const describeDiff = async (diff) => {
|
|
864
|
-
const { formatDiff } = await import("./diff-
|
|
1077
|
+
const { formatDiff } = await import("./diff-NQcQqKe9.mjs").then((n) => n.i);
|
|
865
1078
|
return [
|
|
866
1079
|
`${diff.doc} ${diff.from.capture} (${diff.from.fetched_at}) -> ${diff.to.capture} (${diff.to.fetched_at})`,
|
|
867
1080
|
formatDiff(diff),
|
|
@@ -871,13 +1084,54 @@ const describeDiff = async (diff) => {
|
|
|
871
1084
|
};
|
|
872
1085
|
/** 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. */
|
|
873
1086
|
const runExtract = async (crawler, doc, fields, options, json, out) => {
|
|
874
|
-
const { extractDocument } = await import("./extract-
|
|
1087
|
+
const { extractDocument } = await import("./extract-ByFfc8Zk.mjs").then((n) => n.i);
|
|
875
1088
|
const claims = fields;
|
|
876
1089
|
const result = crawler.extract === void 0 ? await extractDocument(crawler.runtime, doc, claims, options) : await crawler.extract(doc, claims, options);
|
|
877
1090
|
if (json) out(JSON.stringify(result, null, 2));
|
|
878
1091
|
else for (const line of describeExtract(result)) out(line);
|
|
879
1092
|
return extractCode(result);
|
|
880
1093
|
};
|
|
1094
|
+
/**
|
|
1095
|
+
* This function makes the embedder of the command line. It loads no model: the model loads at the
|
|
1096
|
+
* first text. The transformers embedder keeps the files of its model in `<store>/models`.
|
|
1097
|
+
*/
|
|
1098
|
+
const embedderFor = async (choice, dir) => {
|
|
1099
|
+
try {
|
|
1100
|
+
if (choice.kind === "ollama") {
|
|
1101
|
+
const { ollamaEmbedder } = await import("./ollama-BJxkjny4.mjs").then((n) => n.a);
|
|
1102
|
+
return ollamaEmbedder({
|
|
1103
|
+
...choice.model === void 0 ? {} : { model: choice.model },
|
|
1104
|
+
...choice.url === void 0 ? {} : { baseUrl: choice.url },
|
|
1105
|
+
allowRemote: choice.allowRemote
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
const { transformersEmbedder } = await import("./transformers-BQMlaGj4.mjs").then((n) => n.i);
|
|
1109
|
+
return transformersEmbedder({
|
|
1110
|
+
...choice.model === void 0 ? {} : { model: choice.model },
|
|
1111
|
+
cacheDir: join(dir, "models")
|
|
1112
|
+
});
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
throw new UsageError(`--embedder ${choice.kind}: ${error.message}`, { cause: error });
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
/** This function gives vectors to the stored pages within the budget. The exit code is 0 when each page has vectors. */
|
|
1118
|
+
const runEmbed = async (crawler, limits, options, json, out) => {
|
|
1119
|
+
const controller = new AbortController();
|
|
1120
|
+
const stop = () => controller.abort(/* @__PURE__ */ new Error("interrupted"));
|
|
1121
|
+
process.once("SIGINT", stop);
|
|
1122
|
+
try {
|
|
1123
|
+
const result = await crawler.embed({
|
|
1124
|
+
budget: limits.budgetMs,
|
|
1125
|
+
signal: controller.signal,
|
|
1126
|
+
...options.maxDocs === void 0 ? {} : { maxDocs: options.maxDocs },
|
|
1127
|
+
...options.kind === void 0 ? {} : { kind: options.kind }
|
|
1128
|
+
});
|
|
1129
|
+
out(json ? JSON.stringify(result, null, 2) : describeEmbed(result));
|
|
1130
|
+
return result.stopped === "done" ? 0 : 1;
|
|
1131
|
+
} finally {
|
|
1132
|
+
process.removeListener("SIGINT", stop);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
881
1135
|
/** This function does the command of the command line, and gives the exit code. */
|
|
882
1136
|
const main = async (args) => {
|
|
883
1137
|
const out = (text) => void process.stdout.write(`${text}\n`);
|
|
@@ -891,6 +1145,7 @@ const main = async (args) => {
|
|
|
891
1145
|
return 0;
|
|
892
1146
|
}
|
|
893
1147
|
const { command, rest, values, render, rank, limit, from, length, maxAge, crawlLimits, at, diffFrom, diffTo, watchLine, extractFields, dir, signKey, signatureAgent } = line;
|
|
1148
|
+
const embedder = line.embedder === void 0 ? void 0 : await embedderFor(line.embedder, dir);
|
|
894
1149
|
if (command === "key-directory") return keyDirectoryCommand(keyFile(signKey), signatureAgent);
|
|
895
1150
|
let signer;
|
|
896
1151
|
if (signKey !== void 0 && signatureAgent !== void 0) {
|
|
@@ -911,7 +1166,8 @@ const main = async (args) => {
|
|
|
911
1166
|
contact: line.contact,
|
|
912
1167
|
...values["no-browser"] === true ? { renderer: false } : {},
|
|
913
1168
|
...line.allowPrivate ? { egress: { allowPrivate: true } } : {},
|
|
914
|
-
...signer === void 0 ? {} : { signer }
|
|
1169
|
+
...signer === void 0 ? {} : { signer },
|
|
1170
|
+
...embedder === void 0 ? {} : { embedder }
|
|
915
1171
|
};
|
|
916
1172
|
if (command === "mcp") {
|
|
917
1173
|
const { startMcpServer } = await import("./mcp.mjs");
|
|
@@ -921,8 +1177,16 @@ const main = async (args) => {
|
|
|
921
1177
|
});
|
|
922
1178
|
return 0;
|
|
923
1179
|
}
|
|
1180
|
+
if (command === "daemon") {
|
|
1181
|
+
const { daemonCommand } = await import("./cli-daemon--cTmq0Z5.mjs");
|
|
1182
|
+
return daemonCommand(line.daemonLine, options, {
|
|
1183
|
+
allowUserProvided: line.allowUserProvided,
|
|
1184
|
+
json: values.json === true,
|
|
1185
|
+
out
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
924
1188
|
if (line.exportPlan !== void 0) return exportCommand(line.exportPlan, dir, values.json === true);
|
|
925
|
-
const { openCrawler } = await import("./open-
|
|
1189
|
+
const { openCrawler } = await import("./open-C4fhXJgD.mjs").then((n) => n.a);
|
|
926
1190
|
let renderOff;
|
|
927
1191
|
let renderer = options.renderer;
|
|
928
1192
|
if (!(command === "fetch" || command === "crawl" || watchLine?.action === "run") || render === "never") renderer = false;
|
|
@@ -1005,6 +1269,10 @@ const main = async (args) => {
|
|
|
1005
1269
|
userProvided: values["user-provided"] === true,
|
|
1006
1270
|
at
|
|
1007
1271
|
}, values.json === true, out);
|
|
1272
|
+
case "embed": return await runEmbed(crawler, crawlLimits, {
|
|
1273
|
+
maxDocs: line.maxDocs,
|
|
1274
|
+
kind: values.kind
|
|
1275
|
+
}, values.json === true, out);
|
|
1008
1276
|
default: {
|
|
1009
1277
|
const counts = await crawler.reindex();
|
|
1010
1278
|
print(counts, () => describeReindex(counts));
|
|
@@ -1024,6 +1292,6 @@ main(process.argv.slice(2)).then((code) => {
|
|
|
1024
1292
|
process.exitCode = usage ? 2 : 1;
|
|
1025
1293
|
});
|
|
1026
1294
|
//#endregion
|
|
1027
|
-
export {};
|
|
1295
|
+
export { describeDaemonStatus as t };
|
|
1028
1296
|
|
|
1029
1297
|
//# sourceMappingURL=cli.mjs.map
|