@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
package/LICENSE.md ADDED
@@ -0,0 +1,61 @@
1
+ # PolyForm Strict License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/strict/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose, other than distributing the software or making changes or new works based on the software.
12
+
13
+ ## Patent License
14
+
15
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
16
+
17
+ ## Noncommercial Purposes
18
+
19
+ Any noncommercial purpose is a permitted purpose.
20
+
21
+ ## Personal Uses
22
+
23
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
24
+
25
+ ## Noncommercial Organizations
26
+
27
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
28
+
29
+ ## Fair Use
30
+
31
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
32
+
33
+ ## No Other Rights
34
+
35
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
36
+
37
+ ## Patent Defense
38
+
39
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
40
+
41
+ ## Violations
42
+
43
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
44
+
45
+ ## No Liability
46
+
47
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
48
+
49
+ ## Definitions
50
+
51
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
52
+
53
+ **You** refers to the individual or entity agreeing to these terms.
54
+
55
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
56
+
57
+ **Your licenses** are all the licenses granted to you for the software under these terms.
58
+
59
+ **Use** means anything you do with the software requiring one of your licenses.
60
+
61
+ Required Notice: Copyright 2026 William Jin (https://wrenautomation.com)
package/README.md CHANGED
@@ -1,3 +1,338 @@
1
- # Temporary Holding Version
1
+ # birdwatch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Research from the open web, driven by an agent one step per command. The agent plans, triages,
4
+ reviews and stops; birdwatch searches, opens pages, extracts findings behind a quote gate, checks
5
+ them, and keeps every record in a ledger. Every finding is a sentence with a quote on a page the
6
+ run read. `skills/birdwatch/SKILL.md` (in this repo) is how an agent drives it.
7
+
8
+ Reading the web and files (the fetch ladder, HTML and PDF to text, video) is
9
+ [`@wrenautomation/read`](https://github.com/wrenautomation/engine/tree/main/packages/read)'s. Every model call goes through
10
+ [`@wrenautomation/models`](https://github.com/wrenautomation/engine/tree/main/packages/models). Until 0.29.0 this was three packages: raven (the planner,
11
+ now `./plan`), falcon's `./document` and weaver (the engine, sources, ledger and CLI).
12
+
13
+ | Import | Does |
14
+ |---|---|
15
+ | `@wrenautomation/birdwatch/engine` | `Source` (a name, `search` and/or `open`, a cost), `registry` and the plan's allow-list; the agent's calls (`planRound`, `search`, `open`, `extract`, `check`, `add`, `accept`, `brief`), one step each; the judge's passes (`judgeHits`, `judgeDocuments`, `judgeFindings`, `judgeStats`); `run(deps, options)`: plan, fetch, triage, read, extract, check (a sample of findings against their documents), review, round after round until the planner's edge says stop; `fold` rebuilds a run's state from its records, so a run resumes from the step it stopped on; `Transient`, the failure worth another try; the planner's `Ask`, `PlanFile` and record shapes re-exported |
16
+ | `@wrenautomation/birdwatch/sources` | `searxng`, `exa` (search), `youtube` (search and read, yt-dlp), `fetcher` (open, the polite fetcher), `ladder` (open, autobrowse's fetch ladder up to a browser), `reddit` and `facebook` (open a thread or a group post through autobrowse's site routes), `google` (Google's own page through the desk's browser: each result a hit, the AI Overview one hit with its text as the snippet, each page it cites a hit), `arxiv` (search and read: the API inside `BIRDWATCH_ARXIV_CATEGORIES`, each hit led by its OpenAlex citation count and comment; the paper read whole from its HTML, else its PDF; one call per 3 s), `paste` (a person's URLs), `ledger` (hits earlier runs saw). A query with a `site` skips a searcher that cannot keep to it (`Source.keeps`: `youtube` keeps to YouTube only). A follow (`Source.follows`, `follow`) lists a page's author's posts (`youtube`: a channel's uploads, from a video or the channel, and a TikTok profile; `reddit`: a user's top posts and comments) or pages like it (`exa`: findSimilar) |
17
+ | `@wrenautomation/birdwatch/ledger` | the records (`query`, `hit`, `document`, `finding`, `verdict`, `call`, `spend`, `snapshot`, `edge`, `done`, `steer`, `gap`, `check`, `pattern`, `found`, `judge`, `person`), `FilesLedger` (a folder: `runs/<run>/<writer>.jsonl`, `plans/<kind>.json`), `MemoryLedger`, yield rows per source, person, channel and kind |
18
+ | `@wrenautomation/birdwatch/plan` | the planner: `plan` (an ask, proposals and yield rows in; typed queries out, each with `why`, `from`, `by`), `triage` (read or skip per hit), `review` (the edge after a round: `stop`, `again`, `back`), the shapes (`Ask`, `Plan`, `PlanFile`, `Query`, `Edge`, `Snapshot`), a stock plan file per ask kind (`web`, `video`, `channel`, `person`, `site`: prompts, sources, budget, `age`, the query tree's guide), the renderers. Pure functions and prompts; the model is injected. The runner uses all of it; an agent makes the three decisions itself and uses the shapes |
19
+ | `@wrenautomation/birdwatch/document` | the document side: `Document`, `Finding`, `Proof`, `Verdict` (zod), readers by media (`html`, `pdf`, `text`), `claimExtractor` (findings that quote the document, through the gate, and a verdict on the document: its quality and the ask's gap; no cap on findings or their length, and an answer the model's output cap cut is continued from its last kept quote), the `Model` seam it takes and the `Cut` a model throws when its cap cut an answer |
20
+
21
+ Every record carries `from` (ai, human or tool) and `by` (a model id, a source, a person). A
22
+ person's pasted URL, a person's keep or drop, are the same records as the model's. The plan
23
+ file in `plans/<kind>.json` beside the ledger (prompts, sources, budget) is where a run learns:
24
+ `birdwatch learn <kind>` writes its next version from what a run taught (sources, budget, age, a
25
+ prompt), on the person's agreement; a kind with no tuned file runs the stock one
26
+ (`birdwatch plans <kind>` prints it).
27
+
28
+ ## CLI
29
+
30
+ Needs Node 24+. `read` and `watch` need yt-dlp and ffmpeg.
31
+
32
+ ```sh
33
+ npm i @wrenautomation/birdwatch
34
+ npx birdwatch fetch https://example.com/report --contact you@example.com # a page as text
35
+ GEMINI_API_KEY=... npx birdwatch read https://www.youtube.com/watch?v=... # captions + screen
36
+ COHERE_API_KEY=... npx birdwatch read https://www.instagram.com/reel/... # watched: speech, screen, cuts
37
+ npx birdwatch list 'ytsearch10:cold email' --to 10 # hits as JSON, no downloads
38
+ npx birdwatch text report.pdf # a file on disk as text
39
+ npx birdwatch page saved.html --url https://example.com/report # title, text, links as JSON
40
+ COHERE_API_KEY=... npx birdwatch watch talk.mp4 # speech, on-screen text, cuts
41
+ npx birdwatch plans web > ledger/plans/web.json # a stock plan file, to tune
42
+ ```
43
+
44
+ The engine has two drivers over the same steps. An agent (Claude Code, or any agent with a
45
+ shell) drives it one command at a time: it plans, triages, reviews and stops in its own head and
46
+ the engine does the fixed work (search, open, extract behind the quote gate, check) and keeps the
47
+ records. `run` is the runner: the planner's prompts make those decisions and the loop runs alone. Both
48
+ write the same ledger, so `run --resume` takes over a run an agent started and `brief` reads a
49
+ run the runner made. Every command prints JSON.
50
+
51
+ ```sh
52
+ export BIRDWATCH_AGENT=claude-code # the agent's id on every record it writes
53
+ birdwatch plan "how do staffing firms win repeat clients" --known "referrals convert" # a run; prints its id
54
+ birdwatch plan "who buys growth help" --premise "coaches are the buyer" # a premise gets a tree facet that tests it
55
+ birdwatch brief 01M5YS... # the ask, budget, sources, what is open, hits, findings, cost
56
+ birdwatch recall staffing repeat clients --not 01M5YS... # earlier runs' findings: quote, URL, source date, run; --also <ledger> per extra folder
57
+ birdwatch search 01M5YS... "staffing repeat client rate" --why "the ask's words" # one query, then every searcher
58
+ echo '[{"terms":"...","why":"..."},{"terms":"...","why":"..."}]' | birdwatch search 01M5YS... # a batch
59
+ birdwatch search 01M5YS... < tree.json # a query tree, one tier: {facets: [{name, why, tries: [[{terms, why}], [...]]}]}
60
+ birdwatch search 01M5YS... < tree.json # after triage, the same file: the next try of each facet still thin in kept hits
61
+ birdwatch add call 01M5YS... 01M5YT... keep --why "a survey with numbers" # triage: a hit kept
62
+ birdwatch add call 01M5YS... 01M5YU... drop --why "a vendor page"
63
+ birdwatch open 01M5YS... 01M5YT... --url https://a.example/report --why "numbers" # keep these, read every kept hit
64
+ birdwatch open 01M5YS... --with ladder # an opener the run lacks joins it, to open only
65
+ birdwatch show 01M5YS... 01M5YV... # a document's text, for the agent to read itself
66
+ birdwatch add finding 01M5YS... 01M5YV... --text "412 firms were surveyed." --quote "A 2025 survey of 412 firms"
67
+ birdwatch add check 01M5YS... 01M5YW... --ok --why "the quote says it" # the agent's own check; --fail also drops
68
+ birdwatch extract 01M5YS... # or a model behind the quote gate, every document without a verdict
69
+ birdwatch check 01M5YS... -n 3 # or a model's sample of findings read against their documents
70
+ birdwatch check 01M5YS... --patterns # every unchecked finding a pattern rests on
71
+ birdwatch accept 01M5YS... keep --over 0.8 --why "serves the ask" # the judge's hints in bulk, at the agent's bar
72
+ birdwatch accept 01M5YS... class --min 0.8 --why "head of page" # also: drop --under 0.2
73
+ birdwatch add gap 01M5YS... "no UK numbers yet"
74
+ birdwatch add steer 01M5YS... "numbers only" --human will # the person's word, relayed
75
+ birdwatch add pattern 01M5YS... "Repeat clients come from referrals." --on 01M5YV... 01M5YW... # what findings say together; sites counted
76
+ birdwatch add edge 01M5YS... again --why "thin on the UK" # the review: a new round
77
+ birdwatch plan --run 01M5YS... # round 2: known, steer and what round 1 left open, in its snapshot
78
+ birdwatch plan "coaches: who pays for setters" --parent 01M5YS... --why "the best niche of round 1" # a child run
79
+ birdwatch add dead 01M5YT... spend --why "found consumers, not buyers" # later tries skip the facet
80
+ birdwatch follow 01M5YS... --from 01M5YV... --as posts --why "the speaker's other talks" # the author's own posts; --as similar: pages like it; a paper: --as refs (what it cites) or citers (what cites it)
81
+ birdwatch add person 01M5YS... "Bee Speaker" --profile https://www.youtube.com/@bee --aka Bee --why "one speaker" # one person across sites
82
+ birdwatch learn web --sources searxng exa --rounds 4 # the plan file's next version, after a good run
83
+ birdwatch learn web --thin 2 # the tree's guide: a try under 2 new hits falls through
84
+ birdwatch learn web --show # the current one
85
+ birdwatch add edge 01M5YS... stop --why "enough"
86
+ ```
87
+
88
+ `plan` opens a round: its snapshot carries the ask, what is known (the findings so far), every
89
+ steer, what the last round left open, the allow-list and the budget. No queries: the agent's come
90
+ through `search` or `add query`, never capped. The budget is a guard rail: `brief` shows it, and
91
+ an edge `again` past `budget.rounds` prints a warning, never a refusal. `search`, `open`,
92
+ `extract` and `check` each run one step and print what it wrote (a document's text as `chars`);
93
+ a step that gave up exits 2 with its note. `open` reads every kept hit of the run that has no
94
+ document; `extract` every document without a verdict; `check` a sample across documents of the
95
+ kept findings not yet checked. `add` writes one record by hand: a query, a finding (its quote
96
+ must be on the document word for word, or the add fails with the gate's why), a check (the
97
+ agent's own read of a finding against its document; a fail also drops it), a call (keep or
98
+ drop a hit or a finding), an edge (`again`, `stop`, `back --to <step>`), a gap, or a steer (always
99
+ the person's: `--human [name]`). Every `add` takes `--why` where a decision needs one. A
100
+ `--human` record is the person's word through the agent: `from: human`, `by` their name.
101
+
102
+ A round's search is a tree when the agent gives one: facets (the sides a researcher covers: a
103
+ number, a how, a who, a counter-view, a primary source; the plan file's `tree` guide names them),
104
+ each with tries in order, each try a list of queries. `search` runs one tier per call, in one
105
+ fetch: the first call, every facet's first try; each call after, the next try of each facet
106
+ with fewer kept hits than the guide's `thin` (3 by default). A facet at `thin` is done; a facet
107
+ with hits no call has named waits for triage; a facet out of tries and still thin is a gap,
108
+ written once. The yield a try falls through on is what triage kept, never what a searcher
109
+ returned: every searcher returns a page. `--all` runs every facet's next try, whatever triage
110
+ kept. A page a second query brings back is a `found` record on the first hit, never a second
111
+ hit, and it counts as kept for every facet that reached it. `brief.hits.uncalled` lists each
112
+ hit with the facets that found it, most first; `brief.tree` shows every try with what it wrote,
113
+ `kept` and `uncalled` per facet. A guide facet no query served is named in the note.
114
+ `learn --thin <n>` or `--tree <file>` tunes the guide.
115
+
116
+ The agent reads for itself: `show <run> <document>` prints a document's stored text, `add
117
+ finding` writes what it read behind the same quote gate, `add check` its own check. `extract` and
118
+ `check` put a model behind the same gate instead, for bulk or for the runner; both paths write
119
+ the same records.
120
+
121
+ The runner alone:
122
+
123
+ ```sh
124
+ BIRDWATCH_CONTACT=you@example.com npx birdwatch run "how do staffing firms win repeat clients" --known "referrals convert"
125
+ npx birdwatch run "..." --kind video --propose "cold email teardown" --by will # your queries, merged with the model's
126
+ npx birdwatch run "..." --url https://a.example/report https://b.example/post # pages you want read, first
127
+ npx birdwatch run --resume 01M5YS... --url https://a.example/report # from the step it stopped on; same --url, same --model
128
+ npx birdwatch run "..." --pause # stop after each round's review
129
+ npx birdwatch add steer 01M5YS... "numbers only; skip vendor pages" --human # what you say, first in every prompt after
130
+ npx birdwatch run --resume 01M5YS... --pause # the next round, steered
131
+ npx birdwatch add gap 01M5YS... "no UK numbers yet" --human # what the run lacks: its review and next plan read it
132
+ npx birdwatch add edge 01M5YS... back --to fetch --why "searxng was down" --human # rerun one step of the round; then --resume
133
+ npx birdwatch trace 01M5YS... --type finding spend # the run's records, JSON lines
134
+ npx birdwatch add call 01M5YS... 01M5YT... drop --why "a forecast, not a result" --human # a call in your name
135
+ npx birdwatch yield --source exa # hits, reads, proposed, kept
136
+ npx birdwatch sources # what this machine can run
137
+ npx birdwatch serve --port 4747 # the ledger over HTTP for a viewer, read only
138
+ ```
139
+
140
+ The model is `anthropic` by default (Claude Haiku direct, `ANTHROPIC_API_KEY`; `--model
141
+ anthropic:sonnet|opus|id` for another), `--model gateway[:alias]` for Wren's LLM gateway
142
+ (`WREN_LLM_GATEWAY_URL`, `WREN_LLM_GATEWAY_TOKEN`), or any command that reads a prompt on stdin
143
+ and prints the answer (`claude -p --output-format json` is Claude Code headless). The model does
144
+ the bulk (extract, check); `--decider <spec>` (`BIRDWATCH_DECIDER`) takes the steps that decide what
145
+ comes next (plan, triage, review): `anthropic:sonnet` when the model is Haiku, else the model. Sources come
146
+ from the environment: `SEARXNG_URL` (and `SEARXNG_TOKEN`, a bearer, for an instance behind a
147
+ proxy), `EXA_API_KEY` (or `_1..n`), `GEMINI_API_KEY` for videos without captions,
148
+ `BIRDWATCH_CONTACT` for the fetcher and `arxiv` (no contact, no page opens; `OPENALEX_API_KEY`, free, gives `arxiv` its own citation budget, and `SEMANTIC_SCHOLAR_API_KEY`, free, its own rate for `--as refs`; `--sources arxiv` names it, since no stock plan file does), and for the ladder either
149
+ `AUTOBROWSE_URL` and `AUTOBROWSE_TOKEN` (a worker's HTTP face) or `RESTATE_INGRESS_URL` and
150
+ `RESTATE_AUTH_TOKEN` (the Restate ingress a desk worker sits behind: `POST /desk/call`;
151
+ `AUTOBROWSE_SERVICE` for another service name). The same worker gives the `reddit` and
152
+ `facebook` readers. `BIRDWATCH_ACCOUNTS` names the account each site reader may sign in as
153
+ (`reddit=reddit@alt`); a site with no entry reads signed out only. The ledger folder is `--ledger` or
154
+ `BIRDWATCH_LEDGER` (default `./ledger`); `--writer` or `BIRDWATCH_WRITER` names your file in it.
155
+ The judge is `--judge` or `BIRDWATCH_JUDGE` (`jev`, `jev:<model>`, `fake`, `off`); with neither,
156
+ `jev` when `TYPESAFE_API_KEY` is set, else none. `--no-judge` turns it off for one command.
157
+
158
+ Each open source claims the URLs it opens (`opens`): `youtube` a YouTube page, `fetcher` and
159
+ `ladder` any other http(s) page, `reddit` a Reddit thread or comment permalink, `facebook` a
160
+ public group post. A site's own reader (`first`) gets its turn before the general openers,
161
+ whatever the allow-list's order: it answers the thread as data, where a fetched page is a wall.
162
+ `reddit` reads signed out first (`reddit-public`, the thread's feed); only when that misses and
163
+ `BIRDWATCH_ACCOUNTS` names an account does it ask again signed in as it. `facebook` is always
164
+ signed out. A spent cap or a refused read is a miss; a busy profile (503) or no answer retries.
165
+ A refusal carries the worker's access code in its reason: `need_login`, `platform_blocked`,
166
+ `rate_limited` or `upstream_broken`. The last two never read signed in: that fixes neither.
167
+ The read step asks before a call, so a source that was never
168
+ going to open a hit writes no spend. A page that was fetched with nothing usable (a 403, a block
169
+ the ladder's last rung still hit, a media no reader takes, a ladder answer that is not HTML such
170
+ as a PDF) is a miss: the source throws, its `spend` says `ok: false` with the reason, the next
171
+ opener gets its turn, and a hit every opener missed is a `gap` that lists each reason. A page
172
+ that answered 200 and still holds nothing (a captcha wall, a "not found" shell, under 200
173
+ characters of text) is the same miss, with the title or the count as the reason. A run's
174
+ snapshot pins its sources at `plan`: `sources`, the allow-list, which search, open and follow;
175
+ `opens`, the registry's other openers (unless `--sources` was given), which open and follow but
176
+ never search. `open --with <source>` writes a new snapshot that adds one to `opens` (the read
177
+ note names them when hits missed). The
178
+ ladder costs browser time and one of the worker's daily `fetch` bucket per call. Exa's search
179
+ asks for one highlight per result (three sentences, billed per result) as the hit's `snippet`.
180
+
181
+ Every model call is a `spend` record with the model id, the tokens in and out, and `usd` from
182
+ the list price (`src/app/prices.ts`: Claude Haiku, Sonnet and Opus 5.5; the gateway's `free`
183
+ alias is $0; another model keeps its tokens, no price). `run` prints the run's total on stderr
184
+ and as `cost` in its JSON, and the last review's edge (`edge`: next and why) the same way;
185
+ `brief` prints the total as `cost`.
186
+
187
+ `run` exits 2 when the run stopped on a step that gave up (no model, no source, a block):
188
+ `--resume` continues it once the cause is fixed. A failure worth another try (a 429, a 5xx, a
189
+ dropped connection) is tried three times with a pause, every attempt a `spend` record; a step
190
+ that still cannot go on writes no `done`, and `run` waits and resumes it, five times at most,
191
+ before it exits 2. Searches, reads and extracts fan out (four reads or extracts at a time);
192
+ each document's records land as its batch settles, and a step run again skips what its records
193
+ show done. An extract that fails for good (the model's answer would not parse, a 400) stops the
194
+ step with its findings so far on record and `run` exits 2: `--resume` asks only for the
195
+ documents without a verdict. A model answer the output cap cut is not a failure: the whole
196
+ findings before the cut go through the gate and the model is asked again for the rest of the
197
+ document, with those findings known (`./document`'s `Cut`; the `spend` carries both calls' tokens).
198
+
199
+ `--pause` stops the run after a review that says `again`, with no exit code: between rounds
200
+ you (or a Claude Code session) read `brief`, keep or drop findings (`add call`), and steer the
201
+ run with free text (`add steer`). Every `steer` record is rendered first in every prompt of the
202
+ rounds after it, under "The person says:". `--resume` goes on. A review that says `again` into
203
+ a round past `budget.rounds` parks the run the same way (`parked` says so, no exit code): the
204
+ budget is a guard rail, not a stop rule. `--resume` goes one more round; `plan --run` opens it
205
+ by hand.
206
+
207
+ The steps talk in English records. A query every searcher answered with no new hit, and a kept
208
+ hit no source could open (a 403, a block the ladder could not pass), each write a `gap`; so does
209
+ `add gap`. The round's review reads the gaps and a person's keep or drop with its why; the next
210
+ round's plan reads them, and the review's why on `again`, under "The last round left open".
211
+ `add edge back --to <step>` writes a person's edge: the fold takes it like the planner's. A `pattern`
212
+ (`add pattern`) is what several kept findings say together, in the agent's words; its count of
213
+ distinct sites is computed from the findings' proofs, never claimed, and `brief.patterns` lists
214
+ them for the report. A run can be a child (`--parent <run> --why`): its snapshot names the parent, its plan reads
215
+ what the parent and siblings found (recall over those runs), and its budget is the parent's at
216
+ most. The plan file's `runs.depth` (2) is a guard rail: a deeper child warns. Two caps stop: the
217
+ children per parent round (`runs.children`, 6) and the whole tree's reads (`runs.reads`, pinned
218
+ on the root's snapshot as `treeReads`; by default the root's reads × rounds × (children + 1)).
219
+ `open` stops or cuts its hits at the tree's cap; `brief.lineage` and `brief.treeRoom` show the
220
+ ancestors and the room left. A
221
+ stopped child's stop, patterns and dead ends (`add dead`) reach the parent's next plan as
222
+ known lines; a running child is open. A `class` (`add class`) says what kind of source a document is (primary data, survey,
223
+ practitioner, vendor, forum, news, other), with why; its findings take it, and a class on one
224
+ finding overrides it. `brief.patterns[].classes` counts sources per class: distinct sites,
225
+ but each forum thread on its own; `echo` flags a pattern whose classed sources are all vendors,
226
+ and `unchecked` lists its findings no check has read. Only the agent or a person writes a
227
+ class. A tree facet's `needs` names the classes that can prove it; `tree.facets[].lacks` says
228
+ its read pages have none (and the facet is not `enough`), and a facet whose every try pins one site is flagged. A `draft` (`add draft`, JSON items on stdin) is a rough draft over the kept
229
+ findings and patterns: the study's draft gate drops an item that cites nothing kept or has a number
230
+ no cited quote has. `brief.draft` shows the latest. It is never the final write.
231
+
232
+ The check step reads a sample of the round's kept findings (`budget.checks`, 3 by default, or a
233
+ tenth of the kept findings, whichever is more; 0 is none; spread across documents);
234
+ `check --patterns` reads every unchecked finding a pattern rests on, against the text around each quote. The extract gate proves a quote is
235
+ on the page; the check asks whether it backs the sentence. A finding that fails gets a `check`
236
+ record and a drop call; `add call <finding> keep` puts it back. A fail comes with a fix when one
237
+ fits: a narrower sentence the same quote backs. The fix passes the extract gate and a second
238
+ check, then lands as a new finding with `fixes` naming the old one. The review sees every check, and a finding
239
+ carries its document's date, so the review weighs age as the plan file's `age` line says.
240
+
241
+ The judge (Jev, from `@wrenautomation/models/judge`) answers typed questions with probabilities
242
+ and no text, so it never writes a call or a check. `search` asks it of each new hit: does it
243
+ serve the ask, whose side wrote it (the people the ask is about, or a seller to them), what
244
+ class. `open` asks each new page its class. `check` asks each kept finding with a quote: does the
245
+ quote back it, does it credit another source. Each answer is a `judge` record and each call a
246
+ spend. They hint and route: `brief.hits.uncalled` sorts by facets then the judge's serves, and
247
+ shows its answers; `documents.unclassed[].guess` shows its class; a facet with `needs` is not
248
+ enough while its kept hits have a class (the agent's, else the judge's) and none is needed;
249
+ `patterns[].cites` lists findings that credit another source, and the vendor echo warning names
250
+ them. `check` reads the sample plus each finding the judge backs under 0.8: a judge's yes never
251
+ takes one out. `accept` turns the hints into records at the agent's own bar, each with the
252
+ judge's number in its why. `brief.judge` counts its calls, cost, and how often the agent and the
253
+ check agreed with it. A trial (0.22.0): `open` also asks each page its `worth` (would a careful
254
+ researcher cite it for the ask). `documents.toExtract[].worth` shows it; `brief.judge.worth`
255
+ gives its AUC against "a finding on it was kept", and what a gate at a half would skip. Nothing
256
+ gates on it yet.
257
+
258
+ From 0.25.0 the judge reads a page whole: the body to its references (an appendix stays), in
259
+ parts of 64,000 characters, 4 at most, each answer the most yes of any part. Jev's input is
260
+ cheap (about $0.04 per million tokens), so a paper costs about a tenth of a cent. A run's
261
+ criteria (`--criterion name="question"`, on `run` and `plan`) are the person's own yes-or-no
262
+ tests of a good source: the judge asks each one on every hit and every page. Every prompt shows
263
+ them with the person's steer, and the runner's triage sees the judge's odds on each hit (serves
264
+ and each test) as a hint. Their mean sorts `hits.uncalled` and `documents.toExtract`; `brief.judge.criteria` gives each one's AUC against
265
+ "a finding on it was kept", on hits and on pages. A sort and a hint, never a gate.
266
+
267
+ From 0.27.0 the extractor reads the body too (the reference list holds no finding). Each review
268
+ sees every earlier round's new hits and kept findings, and decides when to stop. A paper's
269
+ `follow --as refs` and `--as citers` list what it cites and what cites it. `--decider` runs plan,
270
+ triage and review on Sonnet while Haiku does the bulk. A check (25 papers, Sonnet as reference):
271
+ on the whole page, Jev's criteria odds rank Sonnet's yes over its no with AUC 0.94 to 1.00; on the
272
+ abstract, 0.77 to 1.00.
273
+
274
+ A follow is a query record with `follow: {url, as, document?}` and terms `<as> of <url>`. The
275
+ fetch step sends it to the sources whose `follows` takes it, `follow` lists up to 30, and the
276
+ hits go through triage, the read and the caps like a search's. Each query goes to the allow-list
277
+ as it stood when it was written: a source `follow --with` lets in joins `opens`, so it runs
278
+ follows, never searches. A
279
+ `person` names one author across sites (profile URLs, names they post under); a document is
280
+ theirs when a posts follow from their profile found it, when its URL sits under a profile, or
281
+ when its channel (or its hit's) is one of their names. `brief.people` counts each person's
282
+ documents, kept findings and sites; `brief.unfollowed` lists kept pages no follow started from
283
+ and no person owns.
284
+
285
+ `serve` puts the ledger on 127.0.0.1 for a viewer (the Wren portal's Research page). Read only:
286
+ the CLI stays the one writer. `GET /runs` gives a row per run (ask, parent, round, next step,
287
+ counts, spend). `GET /runs/:run` gives a `RunView` (`v: 1`): the brief, every query with the hits
288
+ it wrote and found again, every hit with its sources, queries, call and judge, every page with its
289
+ class, verdict and person, every finding with its checks, the timeline of decisions and step ends
290
+ with their whys, and spend per source. `GET /runs/:run/documents/:doc` gives a page's stored text.
291
+ `GET /events` sends `event: run` with `{run}` each time a run's files change, and a ping each 25 s;
292
+ the viewer then gets that run again. CORS answers `--origin` (the portal by default) and any
293
+ localhost, and says yes to Chrome's private-network preflight. The types live in
294
+ `src/engine/view.ts`; a host copies them and checks `v`, never imports them.
295
+
296
+ ## Library
297
+
298
+ ```ts
299
+ import { PoliteFetcher, userAgent } from "@wrenautomation/read/fetch";
300
+ import { readPage } from "@wrenautomation/read/text";
301
+
302
+ const fetcher = new PoliteFetcher(userAgent("you@example.com"));
303
+ const res = await fetcher.get(url);
304
+ const page = readPage(res.text, res.url);
305
+ ```
306
+
307
+ The engine, with your own model and sources:
308
+
309
+ ```ts
310
+ import { claimExtractor } from "@wrenautomation/birdwatch/document";
311
+ import { registry, run } from "@wrenautomation/birdwatch/engine";
312
+ import { FilesLedger, ulids } from "@wrenautomation/birdwatch/ledger";
313
+ import { exa, fetcherSource } from "@wrenautomation/birdwatch/sources";
314
+
315
+ const ledger = new FilesLedger("./ledger", "will");
316
+ const sources = registry(exa({ keys }), fetcherSource({ fetcher }));
317
+ const result = await run(
318
+ { sources, ledger, model, extractor: claimExtractor, clock: () => new Date(), ids: ulids(), runner: "will" },
319
+ { ask: { text: "how do staffing firms win repeat clients", kind: "web", known: [] }, planFile },
320
+ );
321
+ result.state.findings; // each with its proof: a quote on a page the run read
322
+ ```
323
+
324
+ `RunOptions.steps` bounds one call (64 by default): a loop that advances a run a little per pass
325
+ passes a few and calls again with `resume`. A host with its own ledger, sources or model mounts
326
+ the same commands: `addEngineCommands(program, { ledger, sources, model, extractor })` from
327
+ `@wrenautomation/birdwatch` (wren's `research` command is these over Postgres).
328
+
329
+ ## Develop
330
+
331
+ ```sh
332
+ pnpm install
333
+ pnpm lint && pnpm typecheck && pnpm test
334
+ ```
335
+
336
+ A `v*` tag publishes to npm (`.github/workflows/release.yml`, trusted publishing: no stored token).
337
+
338
+ License: PolyForm Strict 1.0.0 (`LICENSE.md`).
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { type VideoRead } from "@wrenautomation/read/video";
3
+ /** The read as one markdown document: its facts as front matter, its title, its sections. */
4
+ export declare function renderRead(r: VideoRead): string;
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `birdwatch`: the engine and its readers from a shell. The readers: `fetch <url>` prints a page
4
+ * as text, `read <url>` a video as one markdown document, `list <target>` what a profile, channel
5
+ * or search holds, `text <file>` an HTML or PDF file as text, `page <file>` an HTML file's title,
6
+ * text and links, `watch <video>` what a video file shows and says. `plans [kind]` prints a stock
7
+ * plan file to tune. Keys come from the environment: COHERE_API_KEY (or COHERE_API_KEY_1..n),
8
+ * GEMINI_API_KEY. The engine's commands are in `engine-cli.ts`: the agent's (`plan`, `search`,
9
+ * `open`, `extract`, `check`, `add`, `brief`), the runner's (`run`), the plan file's (`learn`) and
10
+ * the ledger's (`trace`, `yield`, `sources`). A person's word goes through `add ... --human`.
11
+ */
12
+ import { readFile } from "node:fs/promises";
13
+ import { PoliteFetcher, userAgent } from "@wrenautomation/read/fetch";
14
+ import { htmlText, pdfText, readPage } from "@wrenautomation/read/text";
15
+ import { cohereEye, cohereSpeech, flatPlaylist, frameCounts, geminiVideo, readVideo, readYoutube, renderWatched, watchFile, } from "@wrenautomation/read/video";
16
+ import { Command } from "commander";
17
+ import { AskKind, defaultPlanFile, PLAN_KINDS } from "../plan/index.js";
18
+ import { addEngineCommands, keys } from "./engine-cli.js";
19
+ const program = new Command()
20
+ .name("birdwatch")
21
+ .description("research from the open web: the engine's steps, its ledger, and its readers");
22
+ program
23
+ .command("fetch <url>")
24
+ .description("a page as text (polite: one request at a time per host, a contact in the UA)")
25
+ .option("--contact <who>", "an email address or URL a site operator could reach you at (sent in the User-Agent)", process.env.BIRDWATCH_CONTACT)
26
+ .option("--json", "title, text, links, mailtos and tels as JSON")
27
+ .action(async (url, o) => {
28
+ const fetcher = new PoliteFetcher(userAgent(o.contact));
29
+ const res = await fetcher.get(url);
30
+ const page = readPage(res.text, res.url);
31
+ if (o.json)
32
+ console.log(JSON.stringify({ status: res.status, url: res.url, ...page }, null, 2));
33
+ else
34
+ console.log(page.title ? `${page.title}\n\n${page.text}` : page.text);
35
+ });
36
+ /** The read as one markdown document: its facts as front matter, its title, its sections. */
37
+ export function renderRead(r) {
38
+ const facts = [
39
+ ["source", `${r.kind}:${r.id}`],
40
+ ["title", r.title],
41
+ ["url", r.url],
42
+ ["channel", r.channel],
43
+ ["uploaded", r.uploaded],
44
+ ["duration", r.duration],
45
+ ["thumbnail", r.thumbnail],
46
+ ];
47
+ const front = facts
48
+ .filter((f) => f[1] !== undefined)
49
+ .map(([k, v]) => `${k}: ${typeof v === "string" ? JSON.stringify(v) : v}`)
50
+ .join("\n");
51
+ return `---\n${front}\n---\n\n# ${r.title}\n\n${r.body}`;
52
+ }
53
+ program
54
+ .command("read <url>")
55
+ .description("a video as one markdown document: YouTube from its captions (plus the screen with GEMINI_API_KEY), any other video by watching it (COHERE_API_KEY)")
56
+ .option("--json", "the read as JSON")
57
+ .option("--ffmpeg <path>", "the ffmpeg binary", "ffmpeg")
58
+ .option("--yt-dlp <cmd>", "the yt-dlp command", "yt-dlp")
59
+ .action(async (url, o) => {
60
+ const video = await geminiVideo(keys(process.env, "GEMINI_API_KEY"));
61
+ let read;
62
+ if (/^https?:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//.test(url))
63
+ read = await readYoutube(url, { ytDlp: o.ytDlp, video });
64
+ else {
65
+ const cohereKeys = keys(process.env, "COHERE_API_KEY");
66
+ if (!cohereKeys.length)
67
+ throw new Error("read needs COHERE_API_KEY for a video with no captions");
68
+ read = await readVideo(url, {
69
+ ytDlp: o.ytDlp,
70
+ ffmpeg: o.ffmpeg,
71
+ eye: cohereEye(cohereKeys),
72
+ ear: cohereSpeech(cohereKeys),
73
+ video,
74
+ });
75
+ }
76
+ console.log(o.json ? JSON.stringify(read, null, 2) : renderRead(read));
77
+ });
78
+ program
79
+ .command("list <target>")
80
+ .description("what a profile, channel or search (ytsearch10:<words>) holds, as JSON, no downloads")
81
+ .option("--from <n>", "first entry, 1-based", Number)
82
+ .option("--to <n>", "last entry, inclusive", Number)
83
+ .option("--yt-dlp <cmd>", "the yt-dlp command", "yt-dlp")
84
+ .action(async (target, o) => {
85
+ const range = o.from || o.to ? { start: o.from ?? 1, end: o.to ?? Number.MAX_SAFE_INTEGER } : undefined;
86
+ console.log(JSON.stringify(await flatPlaylist(o.ytDlp, target, range), null, 2));
87
+ });
88
+ program
89
+ .command("text <file>")
90
+ .description("an HTML or PDF file as text")
91
+ .action(async (file) => {
92
+ const bytes = await readFile(file);
93
+ const out = /\.pdf$/i.test(file) ? await pdfText(bytes) : htmlText(bytes.toString("utf8"));
94
+ if (out === null)
95
+ throw new Error(`${file}: no text (scanned, encrypted or broken)`);
96
+ console.log(out);
97
+ });
98
+ program
99
+ .command("page <file>")
100
+ .description("an HTML file's title, text, links, mailtos and tels as JSON")
101
+ .option("--url <url>", "the page's address, to resolve its links")
102
+ .action(async (file, o) => {
103
+ const html = await readFile(file, "utf8");
104
+ console.log(JSON.stringify(readPage(html, o.url ?? `file://${file}`), null, 2));
105
+ });
106
+ program
107
+ .command("watch <video>")
108
+ .description("a video file's speech, on-screen text and cuts")
109
+ .option("--json", "the result as JSON")
110
+ .option("--no-speech", "skip the transcription")
111
+ .option("--language <code>", "spoken language, ISO 639-1", "en")
112
+ .option("--every <s>", "seconds between frames", Number, 2)
113
+ .option("--ffmpeg <path>", "the ffmpeg binary", "ffmpeg")
114
+ .action(async (input, o) => {
115
+ const cohereKeys = keys(process.env, "COHERE_API_KEY");
116
+ if (!cohereKeys.length)
117
+ throw new Error("watch needs COHERE_API_KEY");
118
+ const watched = await watchFile(input, {
119
+ eye: cohereEye(cohereKeys),
120
+ ear: cohereSpeech(cohereKeys),
121
+ video: await geminiVideo(keys(process.env, "GEMINI_API_KEY")),
122
+ ffmpeg: o.ffmpeg,
123
+ speech: o.speech,
124
+ language: o.language,
125
+ everyS: o.every,
126
+ });
127
+ if (o.json)
128
+ console.log(JSON.stringify(watched, null, 2));
129
+ else
130
+ console.log(renderWatched(watched));
131
+ const { read, unread } = frameCounts(watched);
132
+ console.error(`${watched.durationS}s, ${read} frames read, ${unread} not read, ${watched.cuts.length} cuts, ${watched.speech.length} chars of speech`);
133
+ });
134
+ program
135
+ .command("plans [kind]")
136
+ .description("a stock plan file to copy and tune; no kind lists them all")
137
+ .action((kind) => {
138
+ const print = (x) => console.log(JSON.stringify(x, null, 2));
139
+ if (kind)
140
+ print(defaultPlanFile(AskKind.parse(kind)));
141
+ else
142
+ print(PLAN_KINDS.map((k) => defaultPlanFile(k)));
143
+ });
144
+ addEngineCommands(program);
145
+ program.parseAsync(process.argv).catch((err) => {
146
+ console.error(err instanceof Error ? err.message : String(err));
147
+ process.exit(1);
148
+ });
149
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/app/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { type Model } from "@wrenautomation/models";
2
+ import { type Judge } from "@wrenautomation/models/judge";
3
+ import type { Command } from "commander";
4
+ import type { Extractor } from "../document/index.js";
5
+ import { costOf, type Sources } from "../engine/index.js";
6
+ import { FilesLedger, type Ledger, type Problem } from "../ledger/index.js";
7
+ import { type AskKind, Criterion } from "../plan/index.js";
8
+ /** Resumes `run` makes on its own after a transient failure. */
9
+ export declare const RESUMES = 5;
10
+ export { costOf };
11
+ /** The pause before a resume: to `retryAfter` (five minutes at most), else 15 s per resume so far. */
12
+ export declare function resumeWaitMs(n: number, retryAfter: string | undefined, now?: number): number;
13
+ /** `NAME`, then `NAME_1`, `NAME_2`, ... in order: a fleet of keys for one vendor. */
14
+ export declare function keys(printed: NodeJS.ProcessEnv, name: string): string[];
15
+ /** `name=question` lines as criteria; a name the judge already asks, or a repeat, fails. */
16
+ export declare function criteriaOf(lines: readonly string[] | undefined): Criterion[];
17
+ export interface Common {
18
+ ledger: string;
19
+ writer: string;
20
+ }
21
+ /** A ledger a host hands the commands: the contract, and what the files backend adds when it has it. */
22
+ export type HostLedger = Ledger & {
23
+ problems?: Problem[];
24
+ };
25
+ /** What a host swaps in: the files backend, this machine's sources and `--model` are the defaults. */
26
+ export interface EngineCliDeps {
27
+ ledger?(o: Common): HostLedger;
28
+ sources?(o: Common, ledger: HostLedger): Sources;
29
+ /** The model for a `--model` spec; the default is models' `modelFromSpec`. */
30
+ model?(spec: string, printed: NodeJS.ProcessEnv): Model;
31
+ /** The judge for a `--judge` spec; the default is models' `judgeFromSpec`. */
32
+ judge?(spec: string, printed: NodeJS.ProcessEnv): Judge | null;
33
+ extractor?: Extractor;
34
+ /** The pause before a resume; a test passes a stub. */
35
+ sleep?(ms: number): Promise<void>;
36
+ }
37
+ /** The sources this machine can run, from the environment. */
38
+ export declare function sourcesHere(printed: NodeJS.ProcessEnv, ledger: FilesLedger, ytDlp: string, warn?: (line: string) => void): Sources;
39
+ /**
40
+ * The run's allow-list: the names wanted that exist here. `opens`: unless the list was given by
41
+ * hand, every other opener here, let in to open and follow, never to search (arxiv searched every
42
+ * web run when it joined the list). A name wanted that this machine lacks is dropped with a line on stderr.
43
+ */
44
+ export declare function allowList(sources: Sources, wanted: readonly string[], byHand: boolean, warn?: (line: string) => void): {
45
+ sources: string[];
46
+ opens: string[];
47
+ };
48
+ /** A kind the engine plans for; anything else is the caller's mistake, said in words. */
49
+ export declare const kindOf: (kind: string) => AskKind;
50
+ export declare function addEngineCommands(program: Command, deps?: EngineCliDeps): void;