@wrenautomation/birdwatch 0.0.0-stage → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE.md +61 -0
  2. package/README.md +337 -2
  3. package/dist/app/cli.d.ts +4 -0
  4. package/dist/app/cli.js +149 -0
  5. package/dist/app/cli.js.map +1 -0
  6. package/dist/app/engine-cli.d.ts +50 -0
  7. package/dist/app/engine-cli.js +738 -0
  8. package/dist/app/engine-cli.js.map +1 -0
  9. package/dist/app/index.d.ts +6 -0
  10. package/dist/app/index.js +7 -0
  11. package/dist/app/index.js.map +1 -0
  12. package/dist/app/serve.d.ts +14 -0
  13. package/dist/app/serve.js +102 -0
  14. package/dist/app/serve.js.map +1 -0
  15. package/dist/document/body.d.ts +6 -0
  16. package/dist/document/body.js +18 -0
  17. package/dist/document/body.js.map +1 -0
  18. package/dist/document/check.d.ts +35 -0
  19. package/dist/document/check.js +68 -0
  20. package/dist/document/check.js.map +1 -0
  21. package/dist/document/extract.d.ts +81 -0
  22. package/dist/document/extract.js +226 -0
  23. package/dist/document/extract.js.map +1 -0
  24. package/dist/document/index.d.ts +11 -0
  25. package/dist/document/index.js +12 -0
  26. package/dist/document/index.js.map +1 -0
  27. package/dist/document/model.d.ts +10 -0
  28. package/dist/document/model.js +2 -0
  29. package/dist/document/model.js.map +1 -0
  30. package/dist/document/readers.d.ts +24 -0
  31. package/dist/document/readers.js +87 -0
  32. package/dist/document/readers.js.map +1 -0
  33. package/dist/document/types.d.ts +99 -0
  34. package/dist/document/types.js +93 -0
  35. package/dist/document/types.js.map +1 -0
  36. package/dist/engine/agent.d.ts +473 -0
  37. package/dist/engine/agent.js +989 -0
  38. package/dist/engine/agent.js.map +1 -0
  39. package/dist/engine/canonical.d.ts +4 -0
  40. package/dist/engine/canonical.js +55 -0
  41. package/dist/engine/canonical.js.map +1 -0
  42. package/dist/engine/cost.d.ts +13 -0
  43. package/dist/engine/cost.js +31 -0
  44. package/dist/engine/cost.js.map +1 -0
  45. package/dist/engine/index.d.ts +16 -0
  46. package/dist/engine/index.js +17 -0
  47. package/dist/engine/index.js.map +1 -0
  48. package/dist/engine/judge.d.ts +158 -0
  49. package/dist/engine/judge.js +323 -0
  50. package/dist/engine/judge.js.map +1 -0
  51. package/dist/engine/people.d.ts +34 -0
  52. package/dist/engine/people.js +125 -0
  53. package/dist/engine/people.js.map +1 -0
  54. package/dist/engine/plan.d.ts +6 -0
  55. package/dist/engine/plan.js +7 -0
  56. package/dist/engine/plan.js.map +1 -0
  57. package/dist/engine/ports.d.ts +124 -0
  58. package/dist/engine/ports.js +28 -0
  59. package/dist/engine/ports.js.map +1 -0
  60. package/dist/engine/recall.d.ts +42 -0
  61. package/dist/engine/recall.js +71 -0
  62. package/dist/engine/recall.js.map +1 -0
  63. package/dist/engine/runner.d.ts +30 -0
  64. package/dist/engine/runner.js +209 -0
  65. package/dist/engine/runner.js.map +1 -0
  66. package/dist/engine/state.d.ts +99 -0
  67. package/dist/engine/state.js +186 -0
  68. package/dist/engine/state.js.map +1 -0
  69. package/dist/engine/steps.d.ts +98 -0
  70. package/dist/engine/steps.js +560 -0
  71. package/dist/engine/steps.js.map +1 -0
  72. package/dist/engine/tree.d.ts +76 -0
  73. package/dist/engine/tree.js +148 -0
  74. package/dist/engine/tree.js.map +1 -0
  75. package/dist/engine/view.d.ts +138 -0
  76. package/dist/engine/view.js +188 -0
  77. package/dist/engine/view.js.map +1 -0
  78. package/dist/index.d.ts +15 -0
  79. package/dist/index.js +16 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/ledger/files.d.ts +39 -0
  82. package/dist/ledger/files.js +113 -0
  83. package/dist/ledger/files.js.map +1 -0
  84. package/dist/ledger/ids.d.ts +4 -0
  85. package/dist/ledger/ids.js +9 -0
  86. package/dist/ledger/ids.js.map +1 -0
  87. package/dist/ledger/index.d.ts +11 -0
  88. package/dist/ledger/index.js +12 -0
  89. package/dist/ledger/index.js.map +1 -0
  90. package/dist/ledger/ledger.d.ts +46 -0
  91. package/dist/ledger/ledger.js +105 -0
  92. package/dist/ledger/ledger.js.map +1 -0
  93. package/dist/ledger/memory.d.ts +19 -0
  94. package/dist/ledger/memory.js +19 -0
  95. package/dist/ledger/memory.js.map +1 -0
  96. package/dist/ledger/records.d.ts +1424 -0
  97. package/dist/ledger/records.js +292 -0
  98. package/dist/ledger/records.js.map +1 -0
  99. package/dist/plan/index.d.ts +7 -0
  100. package/dist/plan/index.js +8 -0
  101. package/dist/plan/index.js.map +1 -0
  102. package/dist/plan/model.d.ts +6 -0
  103. package/dist/plan/model.js +7 -0
  104. package/dist/plan/model.js.map +1 -0
  105. package/dist/plan/plan.d.ts +31 -0
  106. package/dist/plan/plan.js +87 -0
  107. package/dist/plan/plan.js.map +1 -0
  108. package/dist/plan/plans/index.d.ts +17 -0
  109. package/dist/plan/plans/index.js +236 -0
  110. package/dist/plan/plans/index.js.map +1 -0
  111. package/dist/plan/render.d.ts +41 -0
  112. package/dist/plan/render.js +75 -0
  113. package/dist/plan/render.js.map +1 -0
  114. package/dist/plan/review.d.ts +11 -0
  115. package/dist/plan/review.js +39 -0
  116. package/dist/plan/review.js.map +1 -0
  117. package/dist/plan/triage.d.ts +18 -0
  118. package/dist/plan/triage.js +65 -0
  119. package/dist/plan/triage.js.map +1 -0
  120. package/dist/plan/types.d.ts +401 -0
  121. package/dist/plan/types.js +223 -0
  122. package/dist/plan/types.js.map +1 -0
  123. package/dist/sources/arxiv.d.ts +105 -0
  124. package/dist/sources/arxiv.js +321 -0
  125. package/dist/sources/arxiv.js.map +1 -0
  126. package/dist/sources/exa.d.ts +19 -0
  127. package/dist/sources/exa.js +59 -0
  128. package/dist/sources/exa.js.map +1 -0
  129. package/dist/sources/fetcher.d.ts +35 -0
  130. package/dist/sources/fetcher.js +67 -0
  131. package/dist/sources/fetcher.js.map +1 -0
  132. package/dist/sources/google.d.ts +16 -0
  133. package/dist/sources/google.js +64 -0
  134. package/dist/sources/google.js.map +1 -0
  135. package/dist/sources/index.d.ts +17 -0
  136. package/dist/sources/index.js +18 -0
  137. package/dist/sources/index.js.map +1 -0
  138. package/dist/sources/junk.d.ts +10 -0
  139. package/dist/sources/junk.js +16 -0
  140. package/dist/sources/junk.js.map +1 -0
  141. package/dist/sources/ladder.d.ts +47 -0
  142. package/dist/sources/ladder.js +158 -0
  143. package/dist/sources/ladder.js.map +1 -0
  144. package/dist/sources/ledger.d.ts +7 -0
  145. package/dist/sources/ledger.js +35 -0
  146. package/dist/sources/ledger.js.map +1 -0
  147. package/dist/sources/paste.d.ts +8 -0
  148. package/dist/sources/paste.js +22 -0
  149. package/dist/sources/paste.js.map +1 -0
  150. package/dist/sources/searxng.d.ts +11 -0
  151. package/dist/sources/searxng.js +38 -0
  152. package/dist/sources/searxng.js.map +1 -0
  153. package/dist/sources/sites.d.ts +53 -0
  154. package/dist/sources/sites.js +285 -0
  155. package/dist/sources/sites.js.map +1 -0
  156. package/dist/sources/ytdlp.d.ts +43 -0
  157. package/dist/sources/ytdlp.js +117 -0
  158. package/dist/sources/ytdlp.js.map +1 -0
  159. package/package.json +65 -3
@@ -0,0 +1,738 @@
1
+ /**
2
+ * The engine from a shell: `run` an ask, `trace` a run, `keep` or `drop` a finding by hand, `steer`
3
+ * a run, say a `gap`, send it `back` to a step,
4
+ * `yield` per source, `sources` on this machine. Every command prints JSON, so Claude Code
5
+ * can drive it. The ledger is a folder (`--ledger`, BIRDWATCH_LEDGER, default `./ledger`); a
6
+ * writer names this terminal's file in it (`--writer`, BIRDWATCH_WRITER, default the user name).
7
+ * The model is a `@wrenautomation/models` spec: `anthropic` by default (Claude Haiku direct,
8
+ * ANTHROPIC_API_KEY), `gateway[:alias]`, `gemini`, `claude-code`, `command:<cmd>` and the rest
9
+ * of its grammar. A host with its own ledger, sources or
10
+ * model passes them in `EngineCliDeps`; wren's `research` command is these commands over
11
+ * Postgres. A run that stops on a transient failure (a 429, a 5xx, a dropped connection) is
12
+ * resumed by `run` itself after a pause, up to RESUMES times.
13
+ */
14
+ import { mkdir, readFile } from "node:fs/promises";
15
+ import { userInfo } from "node:os";
16
+ import { serve } from "@hono/node-server";
17
+ import { isTransient } from "@wrenautomation/models";
18
+ import { judgeFromSpec } from "@wrenautomation/models/judge";
19
+ import { modelFromSpec } from "@wrenautomation/models/spec";
20
+ import { PoliteFetcher, userAgent } from "@wrenautomation/read/fetch";
21
+ import { geminiVideo } from "@wrenautomation/read/video";
22
+ import { claimExtractor } from "../document/index.js";
23
+ import { accept, add, brief, check, costOf, extract, fold, follow, joined, open, planRound, RESERVED, recall, registry, run, search, searchTree, tunePlan, } from "../engine/index.js";
24
+ import { FilesLedger, FollowAs, SOURCE_CLASSES, SourceClass, ulids, } from "../ledger/index.js";
25
+ import { Criterion, defaultPlanFile, PLAN_KINDS, Step as PlanStep, QueryTree, TreeGuide, treeOf, } from "../plan/index.js";
26
+ import { accountsFrom, arxivSource, exa, facebookSource, fetcherSource, googleSource, httpLadderClient, ingressLadderClient, ladderSource, ledgerSource, paste, redditSource, searxng, ytdlpSource, } from "../sources/index.js";
27
+ import { SERVE_ORIGINS, SERVE_PORT, serveApp, watchRuns } from "./serve.js";
28
+ const DEFAULT_MODEL = "anthropic";
29
+ /** Resumes `run` makes on its own after a transient failure. */
30
+ export const RESUMES = 5;
31
+ /** The longest pause before a resume, whatever `retry-after` said. */
32
+ const MAX_RESUME_WAIT_MS = 300_000;
33
+ export { costOf };
34
+ /** The pause before a resume: to `retryAfter` (five minutes at most), else 15 s per resume so far. */
35
+ export function resumeWaitMs(n, retryAfter, now = Date.now()) {
36
+ if (retryAfter) {
37
+ const until = Date.parse(retryAfter) - now;
38
+ if (!Number.isNaN(until))
39
+ return Math.max(0, Math.min(until, MAX_RESUME_WAIT_MS));
40
+ }
41
+ return 15_000 * n;
42
+ }
43
+ /** `NAME`, then `NAME_1`, `NAME_2`, ... in order: a fleet of keys for one vendor. */
44
+ export function keys(printed, name) {
45
+ const out = [];
46
+ const one = printed[name];
47
+ if (one)
48
+ out.push(one);
49
+ for (let i = 1;; i++) {
50
+ const k = printed[`${name}_${i}`];
51
+ if (!k)
52
+ break;
53
+ out.push(k);
54
+ }
55
+ return out;
56
+ }
57
+ /** `name=question` lines as criteria; a name the judge already asks, or a repeat, fails. */
58
+ export function criteriaOf(lines) {
59
+ const out = [];
60
+ for (const line of lines ?? []) {
61
+ const at = line.indexOf("=");
62
+ const c = Criterion.safeParse({
63
+ name: line.slice(0, at).trim(),
64
+ question: line.slice(at + 1).trim(),
65
+ });
66
+ if (at < 1 || !c.success)
67
+ throw new Error(`--criterion ${line}: want name=question, name in a-z0-9_`);
68
+ if (RESERVED.has(c.data.name) || out.some((x) => x.name === c.data.name))
69
+ throw new Error(`--criterion ${c.data.name}: the name is taken`);
70
+ out.push(c.data);
71
+ }
72
+ return out;
73
+ }
74
+ const filesLedger = (o) => new FilesLedger(o.ledger, o.writer);
75
+ /** The sources this machine can run, from the environment. */
76
+ export function sourcesHere(printed, ledger, ytDlp, warn = (l) => console.error(l)) {
77
+ const out = [];
78
+ if (printed.SEARXNG_URL)
79
+ out.push(searxng({
80
+ url: printed.SEARXNG_URL,
81
+ ...(printed.SEARXNG_TOKEN ? { token: printed.SEARXNG_TOKEN } : {}),
82
+ }));
83
+ const exaKeys = keys(printed, "EXA_API_KEY");
84
+ if (exaKeys.length)
85
+ out.push(exa({ keys: exaKeys }));
86
+ const geminiKeys = keys(printed, "GEMINI_API_KEY");
87
+ out.push(ytdlpSource({
88
+ ytDlp,
89
+ ...(geminiKeys.length ? { video: () => geminiVideo(geminiKeys) } : {}),
90
+ }));
91
+ if (printed.BIRDWATCH_CONTACT) {
92
+ out.push(fetcherSource({ fetcher: new PoliteFetcher(userAgent(printed.BIRDWATCH_CONTACT)) }));
93
+ const categories = (printed.BIRDWATCH_ARXIV_CATEGORIES ?? "")
94
+ .split(",")
95
+ .map((c) => c.trim())
96
+ .filter(Boolean);
97
+ out.push(arxivSource({
98
+ agent: userAgent(printed.BIRDWATCH_CONTACT),
99
+ categories,
100
+ ...(printed.OPENALEX_API_KEY ? { openalexKey: printed.OPENALEX_API_KEY } : {}),
101
+ ...(printed.SEMANTIC_SCHOLAR_API_KEY ? { s2Key: printed.SEMANTIC_SCHOLAR_API_KEY } : {}),
102
+ }));
103
+ }
104
+ else
105
+ warn("no BIRDWATCH_CONTACT (an email address or URL for site operators): pages stay unopened");
106
+ const sites = printed.AUTOBROWSE_URL
107
+ ? httpLadderClient({ url: printed.AUTOBROWSE_URL, token: printed.AUTOBROWSE_TOKEN ?? null })
108
+ : printed.RESTATE_INGRESS_URL
109
+ ? ingressLadderClient({
110
+ url: printed.RESTATE_INGRESS_URL,
111
+ token: printed.RESTATE_AUTH_TOKEN ?? null,
112
+ ...(printed.AUTOBROWSE_SERVICE ? { service: printed.AUTOBROWSE_SERVICE } : {}),
113
+ caller: `birdwatch:${printed.BIRDWATCH_AGENT ?? "runner"}`,
114
+ })
115
+ : null;
116
+ if (sites) {
117
+ // The account each site reader signs in as: the caller's choice, never a default here.
118
+ const accounts = accountsFrom(printed.BIRDWATCH_ACCOUNTS);
119
+ const reddit = accounts.get("reddit");
120
+ out.push(ladderSource({ sites }), redditSource({ sites, ...(reddit ? { account: reddit } : {}) }), facebookSource({ sites }), googleSource({ sites }));
121
+ }
122
+ out.push(ledgerSource(ledger));
123
+ return registry(...out);
124
+ }
125
+ /**
126
+ * The run's allow-list: the names wanted that exist here. `opens`: unless the list was given by
127
+ * hand, every other opener here, let in to open and follow, never to search (arxiv searched every
128
+ * web run when it joined the list). A name wanted that this machine lacks is dropped with a line on stderr.
129
+ */
130
+ export function allowList(sources, wanted, byHand, warn = (l) => console.error(l)) {
131
+ const out = [];
132
+ for (const name of wanted) {
133
+ if (sources.has(name))
134
+ out.push(name);
135
+ else
136
+ warn(`no source named ${name} here: skipped`);
137
+ }
138
+ const opens = byHand
139
+ ? []
140
+ : [...sources.values()].filter((s) => s.open && !out.includes(s.name)).map((s) => s.name);
141
+ return { sources: out, opens };
142
+ }
143
+ const progress = (e) => console.error(`${e.run} ${e.round}.${e.step} ${e.ok ? "ok" : "gave up"} ${e.ms}ms${e.note ? `: ${e.note}` : ""}`);
144
+ /** A kind the engine plans for; anything else is the caller's mistake, said in words. */
145
+ export const kindOf = (kind) => {
146
+ const k = PLAN_KINDS.find((p) => p === kind);
147
+ if (!k)
148
+ throw new Error(`no kind "${kind}": the kinds are ${PLAN_KINDS.join(", ")}`);
149
+ return k;
150
+ };
151
+ /** Prints what a command did as JSON; a step that gave up exits 2, like `run`. */
152
+ function printDid(did) {
153
+ console.error(`${did.run} ${did.round}.${did.step} ${did.ok ? "ok" : "gave up"}${did.note ? `: ${did.note}` : ""}`);
154
+ console.log(JSON.stringify(did, null, 2));
155
+ if (!did.ok)
156
+ process.exitCode = 2;
157
+ }
158
+ /** Whole stdin, for a batch of queries as JSON. */
159
+ async function stdinText() {
160
+ let text = "";
161
+ for await (const chunk of process.stdin)
162
+ text += chunk;
163
+ return text;
164
+ }
165
+ export function addEngineCommands(program, deps = {}) {
166
+ const ledgerOf = deps.ledger ?? filesLedger;
167
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
168
+ const common = (c) => c
169
+ .option("--ledger <dir>", "the ledger folder", process.env.BIRDWATCH_LEDGER ?? "./ledger")
170
+ .option("--writer <name>", "this terminal's file in the ledger", process.env.BIRDWATCH_WRITER ?? userInfo().username);
171
+ const machine = (c) => c
172
+ .option("--model <spec>", "a models spec: anthropic[:haiku|sonnet|opus], gateway[:alias], gemini[:id], claude-code, command:<cmd>, ...", process.env.BIRDWATCH_MODEL ?? DEFAULT_MODEL)
173
+ .option("--no-model", "no model: nothing extracted or checked")
174
+ .option("--decider <spec>", "the model for the steps that decide what comes next (plan, triage, review); default: anthropic:sonnet when --model is Haiku, else --model", process.env.BIRDWATCH_DECIDER)
175
+ .option("--judge <spec>", "cheap typed answers on every hit, page and finding (a hint and a router): jev[:model], fake, off", process.env.BIRDWATCH_JUDGE ?? (process.env.TYPESAFE_API_KEY ? "jev" : "off"))
176
+ .option("--no-judge", "no judge")
177
+ .option("--yt-dlp <cmd>", "the yt-dlp command", process.env.BIRDWATCH_YTDLP ?? "yt-dlp");
178
+ const agent = (c) => c
179
+ .option("--by <agent>", "the agent's id on what it writes", process.env.BIRDWATCH_AGENT)
180
+ .option("--human [name]", "the record is the person's word, relayed: from human, by their name (default: the writer)");
181
+ /** The ledger, this machine's sources and the model, from the options. */
182
+ const setup = (o) => {
183
+ const ledger = ledgerOf(o);
184
+ const sources = deps.sources
185
+ ? deps.sources(o, ledger)
186
+ : sourcesHere(process.env, ledger, o.ytDlp);
187
+ const model = o.model === false ? null : (deps.model ?? modelFromSpec)(o.model, process.env);
188
+ // Haiku restates, Sonnet decides: a few cents a run.
189
+ const deciderSpec = o.decider ??
190
+ (o.model === "anthropic" || o.model === "anthropic:haiku" ? "anthropic:sonnet" : null);
191
+ const decider = model && deciderSpec ? (deps.model ?? modelFromSpec)(deciderSpec, process.env) : null;
192
+ const judge = o.judge === false ? null : (deps.judge ?? judgeFromSpec)(o.judge, process.env);
193
+ return { ledger, sources, model, judge, ...(decider ? { decider } : {}) };
194
+ };
195
+ /**
196
+ * The ledger's tuned plan file for the kind (the stock one when none), its sources the run's
197
+ * allow-list: the names given by hand, else `first` then the plan file's, as far as this
198
+ * machine has them. `opens`: the other openers, let in to open and follow only.
199
+ */
200
+ const planFileOf = async (ledger, sources, kind, byHand, first = []) => {
201
+ const stock = defaultPlanFile(kind);
202
+ const tuned = (await ledger.readPlan?.(kind)) ?? stock;
203
+ if (tuned.version < stock.version)
204
+ console.error(`plan file ${kind} is version ${tuned.version}; birdwatch ships ${stock.version}: delete it to take the new one`);
205
+ const wanted = byHand ?? [...first, ...tuned.sources];
206
+ // The planner narrows to the plan file's sources, so the run's allow-list goes in as the plan file's.
207
+ const { sources: list, opens } = allowList(sources, wanted, !!byHand);
208
+ return { planFile: { ...tuned, sources: list }, opens };
209
+ };
210
+ const depsOf = (s, runner) => ({
211
+ ...s,
212
+ extractor: deps.extractor ?? claimExtractor,
213
+ clock: () => new Date(),
214
+ ids: ulids(),
215
+ runner,
216
+ });
217
+ /** The agent's id: `--by` or BIRDWATCH_AGENT. Every record it writes names it. */
218
+ const agentOf = (o) => {
219
+ if (!o.by)
220
+ throw new Error("name the agent: --by <id> or BIRDWATCH_AGENT");
221
+ return o.by;
222
+ };
223
+ /** Who a record is from: the person through the agent (`--human`), else the agent. */
224
+ const whoOf = (o) => o.human
225
+ ? { from: "human", by: o.human === true ? o.writer : o.human }
226
+ : { from: "ai", by: agentOf(o) };
227
+ machine(common(program.command("run [ask]")))
228
+ .description("run an ask through plan, fetch, triage, read, extract, check and review; prints the run as JSON")
229
+ .option("--kind <kind>", `one of ${PLAN_KINDS.join(", ")}`, "web")
230
+ .option("--known <line...>", "what is known already, one line each, with its URL or ledger id; a line without one is a premise")
231
+ .option("--premise <line...>", "what the ask takes as given (a buyer, an offer, a price): the tree tests each")
232
+ .option("--criterion <name=question...>", 'a yes-or-no test of a good source (code="Is code released?"): the judge asks it on every hit and every page read whole, a hint only')
233
+ .option("--steer <line...>", "what you say to the model, first in every prompt")
234
+ .option("--site <host>", "the site the ask is about")
235
+ .option("--sources <name...>", "the allow-list for this run (default: the plan file's, as far as this machine has them)")
236
+ .option("--propose <terms...>", "queries from you, merged with the model's in round 1")
237
+ .option("--url <url...>", "pages you want read: a paste source, first in the allow-list")
238
+ .option("--by <who>", "who proposes or pastes", process.env.BIRDWATCH_WRITER ?? userInfo().username)
239
+ .option("--resume <run>", "continue an earlier run from the step it stopped on")
240
+ .option("--run <id>", "the new run's id")
241
+ .option("--pause", "stop after each round's review: `birdwatch steer`, then --resume")
242
+ .option("--parent <run>", "a new run's parent: this run is a child of it (one niche of a wide run)")
243
+ .option("--why <why>", "with --parent: why this child")
244
+ .action(async (ask, o) => {
245
+ if (!ask && !o.resume)
246
+ throw new Error("run needs an ask, or --resume <run>");
247
+ const kind = kindOf(o.kind);
248
+ const s = setup(o);
249
+ const { ledger, sources } = s;
250
+ if (o.url?.length)
251
+ sources.set("paste", paste(o.url.map((url) => ({ url })), o.by));
252
+ const { planFile, opens } = await planFileOf(ledger, sources, kind, o.sources, o.url?.length ? ["paste"] : []);
253
+ const byHand = [
254
+ ...(o.propose ?? []).map((terms) => ({ kind, terms, why: "proposed by hand" })),
255
+ // The paste answers the first query it sees: this one, when nothing else is proposed.
256
+ ...(o.url?.length && !o.propose?.length && ask
257
+ ? [{ kind, terms: ask, why: "the pasted pages" }]
258
+ : []),
259
+ ];
260
+ const proposals = byHand.length
261
+ ? [{ from: "human", by: o.by, queries: byHand }]
262
+ : [];
263
+ const id = o.resume ?? o.run ?? ulids()();
264
+ const engineDeps = depsOf(s, o.writer);
265
+ const options = {
266
+ ...(ask
267
+ ? {
268
+ ask: {
269
+ text: ask,
270
+ kind,
271
+ known: o.known ?? [],
272
+ premises: o.premise ?? [],
273
+ criteria: criteriaOf(o.criterion),
274
+ steer: o.steer ?? [],
275
+ ...(o.site ? { site: o.site } : {}),
276
+ },
277
+ }
278
+ : {}),
279
+ planFile,
280
+ opens,
281
+ proposals,
282
+ onStep: progress,
283
+ ...(o.pause ? { pause: true } : {}),
284
+ ...(o.parent ? { parent: { run: o.parent, why: o.why ?? "" } } : {}),
285
+ };
286
+ let result = null;
287
+ for (let n = 0; result === null; n++) {
288
+ try {
289
+ result = await run(engineDeps, {
290
+ ...options,
291
+ ...(n === 0 && !o.resume ? { run: id } : { resume: id }),
292
+ });
293
+ }
294
+ catch (err) {
295
+ if (!isTransient(err))
296
+ throw err;
297
+ if (n >= RESUMES) {
298
+ console.error(`${id}: ${err.message}; stopped after ${RESUMES} resumes`);
299
+ break;
300
+ }
301
+ const ms = resumeWaitMs(n + 1, err.retryAfter);
302
+ console.error(`${id}: ${err.message}; resume ${n + 1} of ${RESUMES} in ${Math.round(ms / 1000)}s`);
303
+ await sleep(ms);
304
+ }
305
+ }
306
+ const state = result?.state ?? fold(id, await ledger.trace(id));
307
+ if (result?.paused)
308
+ console.error(`${id}: paused after round ${state.edges.length}; birdwatch add steer ${id} "<their words>" --human, then birdwatch run --resume ${id} --pause`);
309
+ if (result?.parked)
310
+ console.error(`${id}: ${result.parked}; birdwatch run --resume ${id} goes one more round, birdwatch plan --run ${id} opens it by hand`);
311
+ const cost = costOf(state.spends);
312
+ // The last review's word: what the ask still lacks, or that it is answered.
313
+ const last = state.edges.at(-1);
314
+ if (last)
315
+ console.error(`${id}: ${last.next}: ${last.why}`);
316
+ console.error(`${id}: ${cost.line}`);
317
+ console.log(JSON.stringify({
318
+ run: id,
319
+ rounds: state.round,
320
+ cost: cost.total,
321
+ finished: result?.finished ?? false,
322
+ ...(result?.paused ? { paused: true } : {}),
323
+ ...(result?.parked ? { parked: result.parked } : {}),
324
+ note: result ? result.note : "stopped on a transient failure: --resume to go on",
325
+ ...(last ? { edge: { next: last.next, why: last.why } } : {}),
326
+ hits: state.hits.length,
327
+ documents: state.documents.length,
328
+ findings: state.findings.filter((f) => state.called.get(f.id) !== "drop"),
329
+ spends: state.spends.map(({ source, op, about, ok, cost, ms, usd, tokens }) => ({
330
+ source,
331
+ op,
332
+ about,
333
+ ok,
334
+ cost,
335
+ ms,
336
+ ...(usd !== undefined ? { usd } : {}),
337
+ ...(tokens ? { tokens } : {}),
338
+ })),
339
+ problems: ledger.problems ?? [],
340
+ }, null, 2));
341
+ if (!result?.finished && !result?.paused && !result?.parked)
342
+ process.exitCode = 2;
343
+ });
344
+ agent(machine(common(program.command("plan [ask]"))))
345
+ .description("open a round: a new run from an ask, or the next round of --run after an edge that said again. Prints the run id")
346
+ .option("--run <id>", "the run whose next round to open")
347
+ .option("--kind <kind>", `one of ${PLAN_KINDS.join(", ")}`, "web")
348
+ .option("--known <line...>", "what is known already, one line each, with its URL or ledger id; a line without one is a premise")
349
+ .option("--premise <line...>", "what the ask takes as given (a buyer, an offer, a price): the tree tests each")
350
+ .option("--criterion <name=question...>", 'a yes-or-no test of a good source (code="Is code released?"): the judge asks it on every hit and every page read whole, a hint only')
351
+ .option("--site <host>", "the site the ask is about")
352
+ .option("--sources <name...>", "the allow-list for this run (default: the plan file's, as far as this machine has them)")
353
+ .option("--parent <run>", "a new run's parent: this run is a child of it (one niche of a wide run)")
354
+ .option("--why <why>", "with --parent: why this child")
355
+ .action(async (ask, o) => {
356
+ if (!ask && !o.run)
357
+ throw new Error("plan needs an ask, or --run <run>");
358
+ const s = setup(o);
359
+ const kind = o.run
360
+ ? (fold(o.run, await s.ledger.trace(o.run)).snapshot?.ask.kind ?? kindOf(o.kind))
361
+ : kindOf(o.kind);
362
+ const { planFile, opens } = await planFileOf(s.ledger, s.sources, kind, o.sources);
363
+ printDid(await planRound(depsOf(s, agentOf(o)), {
364
+ ...(o.run ? { run: o.run } : {}),
365
+ ...(ask
366
+ ? {
367
+ ask: {
368
+ text: ask,
369
+ kind,
370
+ known: o.known ?? [],
371
+ premises: o.premise ?? [],
372
+ criteria: criteriaOf(o.criterion),
373
+ ...(o.site ? { site: o.site } : {}),
374
+ },
375
+ }
376
+ : {}),
377
+ planFile,
378
+ opens,
379
+ model: s.model?.id ?? null,
380
+ ...(o.parent ? { parent: { run: o.parent, why: o.why ?? "" } } : {}),
381
+ }));
382
+ });
383
+ agent(machine(common(program.command("search <run> [terms]"))))
384
+ .description("one query in the agent's name, then every searcher on every unsearched query of the round; the new hits as JSON. No terms: on stdin, a JSON array of {terms, why, site?}, or a tree {facets: [{name, why, tries: [[{terms, why, site?}]]}]}: one tier. First call: every facet's first try. After triage, the same tree again: the next try of each facet under the guide's thin in kept hits; a facet with uncalled hits waits")
385
+ .option("--why <why>", "why this query serves the ask")
386
+ .option("--site <host>", "keep the query on one host")
387
+ .option("--facet <name>", "the tree's facet this query serves")
388
+ .option("--all", "tree: the next try of every facet that has one, whatever triage kept")
389
+ .action(async (runId, terms, o) => {
390
+ if (!terms && process.stdin.isTTY)
391
+ throw new Error("search: terms, or a JSON array of {terms, why} or a tree on stdin");
392
+ const s = setup(o);
393
+ const deps = depsOf(s, agentOf(o));
394
+ if (terms) {
395
+ const q = {
396
+ terms,
397
+ why: o.why ?? "",
398
+ ...(o.site ? { site: o.site } : {}),
399
+ ...(o.facet ? { facet: o.facet } : {}),
400
+ };
401
+ printDid(await search(deps, runId, [q], whoOf(o)));
402
+ return;
403
+ }
404
+ const input = JSON.parse(await stdinText());
405
+ if (Array.isArray(input)) {
406
+ if (!input.length)
407
+ throw new Error("search: no queries");
408
+ printDid(await search(deps, runId, input, whoOf(o)));
409
+ return;
410
+ }
411
+ const did = await searchTree(deps, runId, QueryTree.parse(input), whoOf(o), {
412
+ all: o.all ?? false,
413
+ });
414
+ printDid(did);
415
+ });
416
+ common(program.command("show <run> <document>"))
417
+ .description("a document's text as the run stored it, for the agent to read itself")
418
+ .option("--head <chars>", "only the first N characters", Number)
419
+ .action(async (runId, document, o) => {
420
+ const ledger = ledgerOf(o);
421
+ const state = fold(runId, await ledger.trace(runId));
422
+ const d = state.documents.find((x) => x.id === document);
423
+ if (!d)
424
+ throw new Error(`run ${runId}: no document ${document}`);
425
+ console.error(`${d.id} ${d.url}${d.title ? ` | ${d.title}` : ""}${d.date ? ` | ${d.date}` : ""} | ${d.text.length} chars`);
426
+ console.log(o.head ? d.text.slice(0, o.head) : d.text);
427
+ });
428
+ agent(machine(common(program.command("open <run> [hit...]"))))
429
+ .description("keep the hits named (and a hit per --url), then read every kept hit of the run that has no document yet")
430
+ .option("--url <url...>", "pages the agent names itself")
431
+ .option("--why <why>", "why read these (with hits or --url)")
432
+ .option("--with <source...>", "open sources this machine has that the run's allow-list lacks (`sources` lists them)")
433
+ .action(async (runId, hits, o) => {
434
+ const s = setup(o);
435
+ printDid(await open(depsOf(s, agentOf(o)), runId, {
436
+ hits,
437
+ ...(o.url ? { urls: o.url } : {}),
438
+ ...(o.why ? { why: o.why } : {}),
439
+ ...(o.with ? { with: o.with } : {}),
440
+ }, whoOf(o)));
441
+ });
442
+ agent(machine(common(program.command("follow <run> [url]"))))
443
+ .description(`what a person does after a good page: --as posts (its channel's or author's own posts: a YouTube channel or video, a TikTok profile, a Reddit user), similar (pages like it), refs (what a paper cites) or citers (what cites it, most cited first); the hits go through the same calls and reads.`)
444
+ .requiredOption("--as <as>", "posts, similar, refs or citers")
445
+ .requiredOption("--why <why>", "why this page's neighbors may answer the ask")
446
+ .option("--from <document>", "the read page that led here (its URL when no url is given)")
447
+ .option("--with <source...>", "follow sources this machine has that the run's allow-list lacks")
448
+ .action(async (runId, url, o) => {
449
+ const s = setup(o);
450
+ printDid(await follow(depsOf(s, agentOf(o)), runId, {
451
+ as: FollowAs.parse(o.as),
452
+ why: o.why,
453
+ ...(url ? { url } : {}),
454
+ ...(o.from ? { document: o.from } : {}),
455
+ ...(o.with ? { with: o.with } : {}),
456
+ }, whoOf(o)));
457
+ });
458
+ agent(machine(common(program.command("extract <run> [document...]"))))
459
+ .description("the extractor (a model behind the quote gate) over the documents named, else every one without a verdict; findings as JSON")
460
+ .action(async (runId, documents, o) => {
461
+ const s = setup(o);
462
+ printDid(await extract(depsOf(s, agentOf(o)), runId, documents.length ? documents : undefined));
463
+ });
464
+ agent(machine(common(program.command("check <run> [finding...]"))))
465
+ .description("a model reads each finding against its document: the findings named, else with --patterns every unchecked finding a pattern rests on, else a sample spread across documents. The judge first reads every kept finding; each one it doubts joins the sample. One that fails gets a drop call")
466
+ .option("-n <n>", "the sample's size (default: the budget's checks or a tenth of the kept findings, whichever is more)", (v) => Number(v))
467
+ .option("--patterns", "every unchecked finding a pattern rests on")
468
+ .action(async (runId, findings, o) => {
469
+ const s = setup(o);
470
+ printDid(await check(depsOf(s, agentOf(o)), runId, {
471
+ ...(findings.length ? { findings } : {}),
472
+ ...(o.n !== undefined ? { n: o.n } : {}),
473
+ ...(o.patterns ? { patterns: true } : {}),
474
+ }));
475
+ });
476
+ const added = async (o, runId, a) => {
477
+ const s = { ledger: ledgerOf(o), sources: registry(), model: null, judge: null };
478
+ const { record, warnings } = await add(depsOf(s, agentOf(o)), runId, a, whoOf(o));
479
+ for (const w of warnings)
480
+ console.error(`${runId}: ${w}`);
481
+ console.log(JSON.stringify(record));
482
+ };
483
+ const adds = program
484
+ .command("add")
485
+ .description("one record by hand, in the agent's name or (--human) the person's");
486
+ agent(common(adds.command("query <run> <terms>")))
487
+ .description("a query for the next search")
488
+ .requiredOption("--why <why>", "why it serves the ask")
489
+ .option("--site <host>")
490
+ .option("--facet <name>", "the tree's facet it serves")
491
+ .action((runId, terms, o) => added(o, runId, {
492
+ type: "query",
493
+ terms,
494
+ why: o.why,
495
+ ...(o.site ? { site: o.site } : {}),
496
+ ...(o.facet ? { facet: o.facet } : {}),
497
+ }));
498
+ agent(common(adds.command("check <run> <finding>")))
499
+ .description("the agent's own check of a finding against its document: --ok or --fail (a fail also drops it)")
500
+ .option("--ok", "the quote backs the sentence")
501
+ .option("--fail", "it does not")
502
+ .requiredOption("--why <why>")
503
+ .action((runId, finding, o) => {
504
+ if (o.ok === o.fail)
505
+ throw new Error("check: --ok or --fail");
506
+ return added(o, runId, { type: "check", on: finding, ok: !!o.ok, why: o.why });
507
+ });
508
+ agent(common(adds.command("finding <run> <document>")))
509
+ .description("a finding the agent read itself: the sentence and its quote, which must be on the document word for word")
510
+ .requiredOption("--text <sentence>", "one plain sentence")
511
+ .requiredOption("--quote <quote>", "copied from the document")
512
+ .action((runId, document, o) => added(o, runId, { type: "finding", document, text: o.text, quote: o.quote }));
513
+ agent(common(adds.command("call <run> <on> <verdict>")))
514
+ .description("keep or drop a hit (read it or not) or a finding, with why")
515
+ .requiredOption("--why <why>")
516
+ .action((runId, on, verdict, o) => {
517
+ if (verdict !== "keep" && verdict !== "drop")
518
+ throw new Error("verdict: keep or drop");
519
+ return added(o, runId, { type: "call", on, verdict, why: o.why });
520
+ });
521
+ agent(common(adds.command("edge <run> <next>")))
522
+ .description("the round's edge: again (a new round), stop, or back --to <step>")
523
+ .requiredOption("--why <why>")
524
+ .option("--to <step>", "with back: plan, fetch, triage, read or extract")
525
+ .action((runId, next, o) => {
526
+ if (next !== "again" && next !== "stop" && next !== "back")
527
+ throw new Error("next: again, stop or back");
528
+ return added(o, runId, {
529
+ type: "edge",
530
+ next,
531
+ ...(o.to ? { to: PlanStep.parse(o.to) } : {}),
532
+ why: o.why,
533
+ });
534
+ });
535
+ agent(common(adds.command("steer <run> <text>")))
536
+ .description("what the person says to the run: first in every prompt after it (always --human)")
537
+ .action((runId, text, o) => added({ ...o, human: o.human ?? true }, runId, { type: "steer", text }));
538
+ agent(common(adds.command("gap <run> <text>")))
539
+ .description("what the round still lacks")
540
+ .option("--on <id>", "the query or hit it is about")
541
+ .action((runId, text, o) => added(o, runId, { type: "gap", note: text, ...(o.on ? { on: o.on } : {}) }));
542
+ agent(common(adds.command("pattern <run> <text>")))
543
+ .description("what several kept findings say together: one sentence; the count of distinct sites is computed")
544
+ .requiredOption("--on <finding...>", "the findings it rests on")
545
+ .action((runId, text, o) => added(o, runId, { type: "pattern", text, findings: o.on }));
546
+ agent(common(adds.command("dead <run> <facet>")))
547
+ .description("give up on a facet, with why: later tries and rounds skip it, and a parent's next plan reads it as a dead end")
548
+ .requiredOption("--why <why>")
549
+ .action((runId, facet, o) => added(o, runId, { type: "dead", facet, why: o.why }));
550
+ agent(common(adds.command("person <run> <name>")))
551
+ .description("one person across platforms: their profile URLs and the names they post under. The brief counts kept findings per person; follow each profile's posts")
552
+ .option("--profile <url...>", "a channel, a Substack, an X account, a blog")
553
+ .option("--aka <name...>", "a channel name or handle they post under")
554
+ .requiredOption("--why <why>")
555
+ .action((runId, name, o) => added(o, runId, {
556
+ type: "person",
557
+ name,
558
+ profiles: o.profile ?? [],
559
+ aka: o.aka ?? [],
560
+ why: o.why,
561
+ }));
562
+ agent(common(adds.command("class <run> <on> <class>")))
563
+ .description(`what kind of source a document is (its findings take it), or one finding when it differs: ${SOURCE_CLASSES.join(", ")}. Patterns count sites per class`)
564
+ .requiredOption("--why <why>")
565
+ .action((runId, on, cls, o) => added(o, runId, { type: "class", on, class: SourceClass.parse(cls), why: o.why }));
566
+ agent(common(program.command("accept <run> <take>")))
567
+ .description("take the judge's guesses over a bar, in your name with your why: `class --min p` classes every unclassed document whose guess is at least p; `keep --over p` and `drop --under p` call every uncalled hit by how likely it serves the ask")
568
+ .requiredOption("--why <why>", "why this bar")
569
+ .option("--min <p>", "class: the guess's least probability", (v) => Number(v))
570
+ .option("--over <p>", "keep: serves the ask over p", (v) => Number(v))
571
+ .option("--under <p>", "drop: serves the ask under p", (v) => Number(v))
572
+ .action(async (runId, take, o) => {
573
+ const bar = (v, flag) => {
574
+ if (v === undefined)
575
+ throw new Error(`accept ${take}: ${flag} <p>`);
576
+ return v;
577
+ };
578
+ const a = take === "class"
579
+ ? { take, min: bar(o.min, "--min"), why: o.why }
580
+ : take === "keep"
581
+ ? { take, over: bar(o.over, "--over"), why: o.why }
582
+ : take === "drop"
583
+ ? { take, under: bar(o.under, "--under"), why: o.why }
584
+ : (() => {
585
+ throw new Error(`accept: class, keep or drop; got ${take}`);
586
+ })();
587
+ const s = { ledger: ledgerOf(o), sources: registry(), model: null, judge: null };
588
+ const out = await accept(depsOf(s, agentOf(o)), runId, a, whoOf(o));
589
+ console.error(`${runId}: ${out.wrote.length} taken, ${out.skipped} under the bar`);
590
+ console.log(JSON.stringify(out, null, 2));
591
+ });
592
+ agent(common(adds.command("draft <run>")))
593
+ .description("a rough draft over the run's kept findings and patterns, on stdin as JSON [{title, body, cites: [finding or pattern id...]}]: an item that cites nothing kept, or has a number no cited finding has, is dropped. A draft, never the final")
594
+ .requiredOption("--for <what>", "what it is for: a decision memo, an SOP outline")
595
+ .action(async (runId, o) => {
596
+ const parsed = JSON.parse(await stdinText());
597
+ const items = Array.isArray(parsed) ? parsed : parsed?.items;
598
+ if (!Array.isArray(items))
599
+ throw new Error("draft: stdin is a JSON array of items, or {items}");
600
+ return added(o, runId, { type: "draft", for: o.for, items: items });
601
+ });
602
+ common(program.command("learn <kind>"))
603
+ .description("the next version of a kind's plan file, from what a run taught: sources, budget, age, a prompt; --show prints the current one")
604
+ .option("--show", "print the current plan file, write nothing")
605
+ .option("--sources <name...>", "the allow-list, in order of preference")
606
+ .option("--queries <n>", "queries per round", Number)
607
+ .option("--reads <n>", "reads per round", Number)
608
+ .option("--rounds <n>", "rounds before the runner parks", Number)
609
+ .option("--checks <n>", "findings checked per round", Number)
610
+ .option("--age <sentence>", "how much a source's age counts")
611
+ .option("--propose <file>", "the propose prompt, from a file")
612
+ .option("--triage <file>", "the triage prompt, from a file")
613
+ .option("--review <file>", "the review prompt, from a file")
614
+ .option("--thin <n>", "new hits under which a tree's try is thin, so the next try runs", Number)
615
+ .option("--tree <file>", "the query tree's guide, as JSON {facets: [{name, why}], thin}")
616
+ .action(async (kind, o) => {
617
+ const k = kindOf(kind);
618
+ const ledger = ledgerOf(o);
619
+ const current = (await ledger.readPlan?.(k)) ?? defaultPlanFile(k);
620
+ if (o.show) {
621
+ console.log(JSON.stringify(current, null, 2));
622
+ return;
623
+ }
624
+ if (!ledger.writePlan)
625
+ throw new Error("this ledger keeps no plan files");
626
+ const text = (file) => readFile(file, "utf8");
627
+ const budget = {
628
+ ...(o.queries !== undefined ? { queries: o.queries } : {}),
629
+ ...(o.reads !== undefined ? { reads: o.reads } : {}),
630
+ ...(o.rounds !== undefined ? { rounds: o.rounds } : {}),
631
+ ...(o.checks !== undefined ? { checks: o.checks } : {}),
632
+ };
633
+ const next = tunePlan(current, {
634
+ ...(o.sources ? { sources: o.sources } : {}),
635
+ budget,
636
+ ...(o.age !== undefined ? { age: o.age } : {}),
637
+ ...(o.propose ? { propose: await text(o.propose) } : {}),
638
+ ...(o.triage ? { triage: await text(o.triage) } : {}),
639
+ ...(o.review ? { review: await text(o.review) } : {}),
640
+ ...(o.tree
641
+ ? { tree: TreeGuide.parse(JSON.parse(await text(o.tree))) }
642
+ : o.thin !== undefined
643
+ ? { tree: { ...treeOf(current), thin: o.thin } }
644
+ : {}),
645
+ });
646
+ const path = await ledger.writePlan(next);
647
+ console.error(`${k}: plan file version ${next.version} at ${path}`);
648
+ console.log(JSON.stringify(next, null, 2));
649
+ });
650
+ common(program.command("brief <run>"))
651
+ .description("the run as the agent needs it: the ask, what is known, what the last round left open, hits to call, documents to extract, findings, checks, edges, yield, cost")
652
+ .action(async (runId, o) => {
653
+ const s = { ledger: ledgerOf(o), sources: registry(), model: null, judge: null };
654
+ console.log(JSON.stringify(await brief(depsOf(s, o.writer), runId), null, 2));
655
+ });
656
+ common(program.command("recall <terms...>"))
657
+ .description("what earlier runs found on these terms: each finding with its quote, URL, source date and run, best match first; dropped findings left out")
658
+ .option("-n <n>", "at most this many (default 20)", Number)
659
+ .option("--not <run>", "leave out this run (the one asking)")
660
+ .option("--kind <kind>", "only runs of this ask kind")
661
+ .option("--also <dir...>", "more ledger folders to read with --ledger (one per day)")
662
+ .action(async (terms, o) => {
663
+ const ledgers = [
664
+ ledgerOf(o),
665
+ ...(o.also ?? []).map((dir) => ledgerOf({ ...o, ledger: dir })),
666
+ ];
667
+ const found = await recall(joined(ledgers), terms.join(" "), {
668
+ ...(o.n !== undefined ? { n: o.n } : {}),
669
+ ...(o.not ? { not: o.not } : {}),
670
+ ...(o.kind ? { kind: kindOf(o.kind) } : {}),
671
+ });
672
+ console.log(JSON.stringify(found, null, 2));
673
+ });
674
+ common(program.command("trace <run>"))
675
+ .description("every record of a run, in time order, as JSON lines")
676
+ .option("--type <type...>", "only these record types")
677
+ .action(async (id, o) => {
678
+ const ledger = ledgerOf(o);
679
+ const records = await ledger.trace(id);
680
+ if (!records.length)
681
+ throw new Error(`run ${id}: no records`);
682
+ for (const r of records)
683
+ if (!o.type || o.type.includes(r.type))
684
+ console.log(JSON.stringify(r));
685
+ for (const p of ledger.problems ?? [])
686
+ console.error(`${p.file}:${p.line}: ${p.why}`);
687
+ });
688
+ common(program.command("serve"))
689
+ .description("the ledger over HTTP for a viewer, read only: GET /runs, /runs/:run, /runs/:run/documents/:doc, and /events (one event per changed run)")
690
+ .option("--port <n>", "the port", (v) => Number(v), SERVE_PORT)
691
+ .option("--host <host>", "the address to bind", "127.0.0.1")
692
+ .option("--origin <url...>", "the pages that may read it, besides any localhost", SERVE_ORIGINS)
693
+ .action(async (o) => {
694
+ const ledger = new FilesLedger(o.ledger, o.writer);
695
+ await mkdir(`${o.ledger}/runs`, { recursive: true });
696
+ const s = { ledger, sources: registry(), model: null, judge: null };
697
+ const app = serveApp({
698
+ deps: depsOf(s, o.writer),
699
+ runs: () => ledger.runs(),
700
+ changes: watchRuns(o.ledger),
701
+ origins: o.origin,
702
+ });
703
+ serve({ fetch: app.fetch, port: o.port, hostname: o.host }, (a) => console.error(`birdwatch serve: http://${a.address}:${a.port} over ${o.ledger}`));
704
+ await new Promise(() => { });
705
+ });
706
+ common(program.command("yield"))
707
+ .description("hits, reads, proposed and kept per source, by, channel and kind, over every run")
708
+ .option("--source <name>")
709
+ .option("--by <who>")
710
+ .option("--channel <name>")
711
+ .option("--kind <kind>")
712
+ .action(async (o) => {
713
+ const rows = await ledgerOf(o).yield({
714
+ ...(o.source ? { source: o.source } : {}),
715
+ ...(o.by ? { by: o.by } : {}),
716
+ ...(o.channel ? { channel: o.channel } : {}),
717
+ ...(o.kind ? { kind: defaultPlanFile(o.kind).kind } : {}),
718
+ });
719
+ console.log(JSON.stringify(rows, null, 2));
720
+ });
721
+ common(program.command("sources"))
722
+ .description("the sources this machine can run, from the environment")
723
+ .option("--yt-dlp <cmd>", "the yt-dlp command", process.env.BIRDWATCH_YTDLP ?? "yt-dlp")
724
+ .action(async (o) => {
725
+ const ledger = ledgerOf(o);
726
+ const sources = deps.sources
727
+ ? deps.sources(o, ledger)
728
+ : sourcesHere(process.env, ledger, o.ytDlp);
729
+ const rows = [...sources.values()].map(({ name, kind, cost, from }) => ({
730
+ name,
731
+ kind,
732
+ cost,
733
+ from: from ?? "tool",
734
+ }));
735
+ console.log(JSON.stringify(rows, null, 2));
736
+ });
737
+ }
738
+ //# sourceMappingURL=engine-cli.js.map