@wrenautomation/birdwatch 0.0.0-stage → 0.30.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.
- package/LICENSE.md +61 -0
- package/README.md +320 -2
- package/dist/app/cli.d.ts +4 -0
- package/dist/app/cli.js +149 -0
- package/dist/app/cli.js.map +1 -0
- package/dist/app/engine-cli.d.ts +44 -0
- package/dist/app/engine-cli.js +596 -0
- package/dist/app/engine-cli.js.map +1 -0
- package/dist/app/index.d.ts +6 -0
- package/dist/app/index.js +7 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/serve.d.ts +14 -0
- package/dist/app/serve.js +102 -0
- package/dist/app/serve.js.map +1 -0
- package/dist/document/body.d.ts +6 -0
- package/dist/document/body.js +18 -0
- package/dist/document/body.js.map +1 -0
- package/dist/document/check.d.ts +35 -0
- package/dist/document/check.js +68 -0
- package/dist/document/check.js.map +1 -0
- package/dist/document/extract.d.ts +81 -0
- package/dist/document/extract.js +226 -0
- package/dist/document/extract.js.map +1 -0
- package/dist/document/index.d.ts +11 -0
- package/dist/document/index.js +12 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/model.d.ts +10 -0
- package/dist/document/model.js +2 -0
- package/dist/document/model.js.map +1 -0
- package/dist/document/readers.d.ts +24 -0
- package/dist/document/readers.js +87 -0
- package/dist/document/readers.js.map +1 -0
- package/dist/document/types.d.ts +99 -0
- package/dist/document/types.js +93 -0
- package/dist/document/types.js.map +1 -0
- package/dist/engine/agent.d.ts +475 -0
- package/dist/engine/agent.js +990 -0
- package/dist/engine/agent.js.map +1 -0
- package/dist/engine/canonical.d.ts +4 -0
- package/dist/engine/canonical.js +55 -0
- package/dist/engine/canonical.js.map +1 -0
- package/dist/engine/context.d.ts +9 -0
- package/dist/engine/context.js +141 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/cost.d.ts +13 -0
- package/dist/engine/cost.js +31 -0
- package/dist/engine/cost.js.map +1 -0
- package/dist/engine/index.d.ts +16 -0
- package/dist/engine/index.js +17 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/judge.d.ts +158 -0
- package/dist/engine/judge.js +323 -0
- package/dist/engine/judge.js.map +1 -0
- package/dist/engine/people.d.ts +34 -0
- package/dist/engine/people.js +125 -0
- package/dist/engine/people.js.map +1 -0
- package/dist/engine/plan.d.ts +6 -0
- package/dist/engine/plan.js +7 -0
- package/dist/engine/plan.js.map +1 -0
- package/dist/engine/plans/index.d.ts +19 -0
- package/dist/engine/plans/index.js +104 -0
- package/dist/engine/plans/index.js.map +1 -0
- package/dist/engine/ports.d.ts +76 -0
- package/dist/engine/ports.js +28 -0
- package/dist/engine/ports.js.map +1 -0
- package/dist/engine/recall.d.ts +42 -0
- package/dist/engine/recall.js +71 -0
- package/dist/engine/recall.js.map +1 -0
- package/dist/engine/state.d.ts +96 -0
- package/dist/engine/state.js +162 -0
- package/dist/engine/state.js.map +1 -0
- package/dist/engine/steps.d.ts +98 -0
- package/dist/engine/steps.js +435 -0
- package/dist/engine/steps.js.map +1 -0
- package/dist/engine/tree.d.ts +76 -0
- package/dist/engine/tree.js +148 -0
- package/dist/engine/tree.js.map +1 -0
- package/dist/engine/types.d.ts +250 -0
- package/dist/engine/types.js +168 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/engine/view.d.ts +138 -0
- package/dist/engine/view.js +188 -0
- package/dist/engine/view.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger/files.d.ts +39 -0
- package/dist/ledger/files.js +113 -0
- package/dist/ledger/files.js.map +1 -0
- package/dist/ledger/ids.d.ts +4 -0
- package/dist/ledger/ids.js +9 -0
- package/dist/ledger/ids.js.map +1 -0
- package/dist/ledger/index.d.ts +11 -0
- package/dist/ledger/index.js +12 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/ledger/ledger.d.ts +46 -0
- package/dist/ledger/ledger.js +105 -0
- package/dist/ledger/ledger.js.map +1 -0
- package/dist/ledger/memory.d.ts +19 -0
- package/dist/ledger/memory.js +19 -0
- package/dist/ledger/memory.js.map +1 -0
- package/dist/ledger/records.d.ts +1424 -0
- package/dist/ledger/records.js +292 -0
- package/dist/ledger/records.js.map +1 -0
- package/dist/sources/arxiv.d.ts +105 -0
- package/dist/sources/arxiv.js +321 -0
- package/dist/sources/arxiv.js.map +1 -0
- package/dist/sources/exa.d.ts +19 -0
- package/dist/sources/exa.js +59 -0
- package/dist/sources/exa.js.map +1 -0
- package/dist/sources/fetcher.d.ts +35 -0
- package/dist/sources/fetcher.js +67 -0
- package/dist/sources/fetcher.js.map +1 -0
- package/dist/sources/google.d.ts +16 -0
- package/dist/sources/google.js +64 -0
- package/dist/sources/google.js.map +1 -0
- package/dist/sources/index.d.ts +17 -0
- package/dist/sources/index.js +18 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/sources/junk.d.ts +10 -0
- package/dist/sources/junk.js +16 -0
- package/dist/sources/junk.js.map +1 -0
- package/dist/sources/ladder.d.ts +47 -0
- package/dist/sources/ladder.js +158 -0
- package/dist/sources/ladder.js.map +1 -0
- package/dist/sources/ledger.d.ts +7 -0
- package/dist/sources/ledger.js +35 -0
- package/dist/sources/ledger.js.map +1 -0
- package/dist/sources/paste.d.ts +8 -0
- package/dist/sources/paste.js +22 -0
- package/dist/sources/paste.js.map +1 -0
- package/dist/sources/searxng.d.ts +11 -0
- package/dist/sources/searxng.js +38 -0
- package/dist/sources/searxng.js.map +1 -0
- package/dist/sources/sites.d.ts +53 -0
- package/dist/sources/sites.js +285 -0
- package/dist/sources/sites.js.map +1 -0
- package/dist/sources/ytdlp.d.ts +43 -0
- package/dist/sources/ytdlp.js +117 -0
- package/dist/sources/ytdlp.js.map +1 -0
- package/package.json +61 -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,321 @@
|
|
|
1
|
-
#
|
|
1
|
+
# birdwatch
|
|
2
2
|
|
|
3
|
-
|
|
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 (a model planner, gone at 0.30.0: the
|
|
11
|
+
agent plans), falcon (`./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`); `fold` rebuilds a run's state from its records, so a step run again skips what is done; `Transient`, the failure worth another try; the ask and plan file shapes (`Ask`, `PlanFile`, `Query`, `QueryTree`, `Edge`, `Snapshot`) and a stock plan file per ask kind (`web`, `video`, `channel`, `person`, `site`: sources, budget, `age`, `rules`, the query tree's guide) |
|
|
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/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 |
|
|
19
|
+
|
|
20
|
+
Every record carries `from` (ai, human or tool) and `by` (an agent or model id, a source, a
|
|
21
|
+
person). A person's keep or drop is the same record as the agent's. The plan file in
|
|
22
|
+
`plans/<kind>.json` beside the ledger (sources, budget, age, rules, the tree guide) is where a run
|
|
23
|
+
learns: `birdwatch learn <kind>` writes its next version from what a run taught, on the person's
|
|
24
|
+
agreement; a kind with no tuned file runs the stock one (`birdwatch plans <kind>` prints it).
|
|
25
|
+
`brief.guide` shows the kind's age sentence and rules to the agent.
|
|
26
|
+
|
|
27
|
+
## CLI
|
|
28
|
+
|
|
29
|
+
Needs Node 24+. `read` and `watch` need yt-dlp and ffmpeg.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm i @wrenautomation/birdwatch
|
|
33
|
+
npx birdwatch fetch https://example.com/report --contact you@example.com # a page as text
|
|
34
|
+
GEMINI_API_KEY=... npx birdwatch read https://www.youtube.com/watch?v=... # captions + screen
|
|
35
|
+
COHERE_API_KEY=... npx birdwatch read https://www.instagram.com/reel/... # watched: speech, screen, cuts
|
|
36
|
+
npx birdwatch list 'ytsearch10:cold email' --to 10 # hits as JSON, no downloads
|
|
37
|
+
npx birdwatch text report.pdf # a file on disk as text
|
|
38
|
+
npx birdwatch page saved.html --url https://example.com/report # title, text, links as JSON
|
|
39
|
+
COHERE_API_KEY=... npx birdwatch watch talk.mp4 # speech, on-screen text, cuts
|
|
40
|
+
npx birdwatch plans web > ledger/plans/web.json # a stock plan file, to tune
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
An agent (Claude Code, or any agent with a shell) drives the engine one command at a time: it
|
|
44
|
+
plans, triages, reviews and stops in its own head, and the engine does the fixed work (search,
|
|
45
|
+
open, extract behind the quote gate, check) and keeps the records. Every command prints JSON.
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
export BIRDWATCH_AGENT=claude-code # the agent's id on every record it writes
|
|
49
|
+
birdwatch plan "how do staffing firms win repeat clients" --known "referrals convert" # a run; prints its id
|
|
50
|
+
birdwatch plan "who buys growth help" --premise "coaches are the buyer" # a premise gets a tree facet that tests it
|
|
51
|
+
birdwatch brief 01M5YS... # the ask, budget, sources, what is open, hits, findings, cost
|
|
52
|
+
birdwatch recall staffing repeat clients --not 01M5YS... # earlier runs' findings: quote, URL, source date, run; --also <ledger> per extra folder
|
|
53
|
+
birdwatch search 01M5YS... "staffing repeat client rate" --why "the ask's words" # one query, then every searcher
|
|
54
|
+
echo '[{"terms":"...","why":"..."},{"terms":"...","why":"..."}]' | birdwatch search 01M5YS... # a batch
|
|
55
|
+
birdwatch search 01M5YS... < tree.json # a query tree, one tier: {facets: [{name, why, tries: [[{terms, why}], [...]]}]}
|
|
56
|
+
birdwatch search 01M5YS... < tree.json # after triage, the same file: the next try of each facet still thin in kept hits
|
|
57
|
+
birdwatch add call 01M5YS... 01M5YT... keep --why "a survey with numbers" # triage: a hit kept
|
|
58
|
+
birdwatch add call 01M5YS... 01M5YU... drop --why "a vendor page"
|
|
59
|
+
birdwatch open 01M5YS... 01M5YT... --url https://a.example/report --why "numbers" # keep these, read every kept hit
|
|
60
|
+
birdwatch open 01M5YS... --with ladder # an opener the run lacks joins it, to open only
|
|
61
|
+
birdwatch show 01M5YS... 01M5YV... # a document's text, for the agent to read itself
|
|
62
|
+
birdwatch add finding 01M5YS... 01M5YV... --text "412 firms were surveyed." --quote "A 2025 survey of 412 firms"
|
|
63
|
+
birdwatch add check 01M5YS... 01M5YW... --ok --why "the quote says it" # the agent's own check; --fail also drops
|
|
64
|
+
birdwatch extract 01M5YS... # or a model behind the quote gate, every document without a verdict
|
|
65
|
+
birdwatch check 01M5YS... -n 3 # or a model's sample of findings read against their documents
|
|
66
|
+
birdwatch check 01M5YS... --patterns # every unchecked finding a pattern rests on
|
|
67
|
+
birdwatch accept 01M5YS... keep --over 0.8 --why "serves the ask" # the judge's hints in bulk, at the agent's bar
|
|
68
|
+
birdwatch accept 01M5YS... class --min 0.8 --why "head of page" # also: drop --under 0.2
|
|
69
|
+
birdwatch add gap 01M5YS... "no UK numbers yet"
|
|
70
|
+
birdwatch add steer 01M5YS... "numbers only" --human will # the person's word, relayed
|
|
71
|
+
birdwatch add pattern 01M5YS... "Repeat clients come from referrals." --on 01M5YV... 01M5YW... # what findings say together; sites counted
|
|
72
|
+
birdwatch add edge 01M5YS... again --why "thin on the UK" # the review: a new round
|
|
73
|
+
birdwatch plan --run 01M5YS... # round 2: known, steer and what round 1 left open, in its snapshot
|
|
74
|
+
birdwatch plan "coaches: who pays for setters" --parent 01M5YS... --why "the best niche of round 1" # a child run
|
|
75
|
+
birdwatch add dead 01M5YT... spend --why "found consumers, not buyers" # later tries skip the facet
|
|
76
|
+
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)
|
|
77
|
+
birdwatch add person 01M5YS... "Bee Speaker" --profile https://www.youtube.com/@bee --aka Bee --why "one speaker" # one person across sites
|
|
78
|
+
birdwatch learn web --sources searxng exa --rounds 4 # the plan file's next version, after a good run
|
|
79
|
+
birdwatch learn web --thin 2 # the tree's guide: a try under 2 new hits falls through
|
|
80
|
+
birdwatch learn web --show # the current one
|
|
81
|
+
birdwatch add edge 01M5YS... stop --why "enough"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`plan` opens a round: its snapshot carries the ask, what is known (the findings so far), every
|
|
85
|
+
steer, what the last round left open, the allow-list and the budget. No queries: the agent's come
|
|
86
|
+
through `search` or `add query`, never capped. The budget is a guard rail: `brief` shows it, and
|
|
87
|
+
an edge `again` past `budget.rounds` prints a warning, never a refusal. `search`, `open`,
|
|
88
|
+
`extract` and `check` each run one step and print what it wrote (a document's text as `chars`);
|
|
89
|
+
a step that gave up exits 2 with its note. `open` reads every kept hit of the run that has no
|
|
90
|
+
document; `extract` every document without a verdict; `check` a sample across documents of the
|
|
91
|
+
kept findings not yet checked. `add` writes one record by hand: a query, a finding (its quote
|
|
92
|
+
must be on the document word for word, or the add fails with the gate's why), a check (the
|
|
93
|
+
agent's own read of a finding against its document; a fail also drops it), a call (keep or
|
|
94
|
+
drop a hit or a finding), an edge (`again`, `stop`, `back --to <step>`), a gap, or a steer (always
|
|
95
|
+
the person's: `--human [name]`). Every `add` takes `--why` where a decision needs one. A
|
|
96
|
+
`--human` record is the person's word through the agent: `from: human`, `by` their name.
|
|
97
|
+
|
|
98
|
+
A round's search is a tree when the agent gives one: facets (the sides a researcher covers: a
|
|
99
|
+
number, a how, a who, a counter-view, a primary source; the plan file's `tree` guide names them),
|
|
100
|
+
each with tries in order, each try a list of queries. `search` runs one tier per call, in one
|
|
101
|
+
fetch: the first call, every facet's first try; each call after, the next try of each facet
|
|
102
|
+
with fewer kept hits than the guide's `thin` (3 by default). A facet at `thin` is done; a facet
|
|
103
|
+
with hits no call has named waits for triage; a facet out of tries and still thin is a gap,
|
|
104
|
+
written once. The yield a try falls through on is what triage kept, never what a searcher
|
|
105
|
+
returned: every searcher returns a page. `--all` runs every facet's next try, whatever triage
|
|
106
|
+
kept. A page a second query brings back is a `found` record on the first hit, never a second
|
|
107
|
+
hit, and it counts as kept for every facet that reached it. `brief.hits.uncalled` lists each
|
|
108
|
+
hit with the facets that found it, most first; `brief.tree` shows every try with what it wrote,
|
|
109
|
+
`kept` and `uncalled` per facet. A guide facet no query served is named in the note.
|
|
110
|
+
`learn --thin <n>` or `--tree <file>` tunes the guide.
|
|
111
|
+
|
|
112
|
+
The agent reads for itself: `show <run> <document>` prints a document's stored text, `add
|
|
113
|
+
finding` writes what it read behind the same quote gate, `add check` its own check. `extract` and
|
|
114
|
+
`check` put a model behind the same gate instead, for bulk; both paths write the same records.
|
|
115
|
+
|
|
116
|
+
The person, through the agent or by hand:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
npx birdwatch add steer 01M5YS... "numbers only; skip vendor pages" --human # what you say: the brief shows it first
|
|
120
|
+
npx birdwatch add gap 01M5YS... "no UK numbers yet" --human # what the run lacks: the next plan reads it
|
|
121
|
+
npx birdwatch add edge 01M5YS... back --to fetch --why "searxng was down" --human # the step to run again
|
|
122
|
+
npx birdwatch trace 01M5YS... --type finding spend # the run's records, JSON lines
|
|
123
|
+
npx birdwatch add call 01M5YS... 01M5YT... drop --why "a forecast, not a result" --human # a call in your name
|
|
124
|
+
npx birdwatch yield --source exa # hits, reads, proposed, kept
|
|
125
|
+
npx birdwatch sources # what this machine can run
|
|
126
|
+
npx birdwatch serve --port 4747 # the ledger over HTTP for a viewer, read only
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The model is `anthropic` by default (Claude Haiku direct, `ANTHROPIC_API_KEY`; `--model
|
|
130
|
+
anthropic:sonnet|opus|id` for another), `--model gateway[:alias]` for Wren's LLM gateway
|
|
131
|
+
(`WREN_LLM_GATEWAY_URL`, `WREN_LLM_GATEWAY_TOKEN`), or any command that reads a prompt on stdin
|
|
132
|
+
and prints the answer (`claude -p --output-format json` is Claude Code headless). The model does
|
|
133
|
+
the bulk (extract, check) only; the agent decides. Sources come
|
|
134
|
+
from the environment: `SEARXNG_URL` (and `SEARXNG_TOKEN`, a bearer, for an instance behind a
|
|
135
|
+
proxy), `EXA_API_KEY` (or `_1..n`), `GEMINI_API_KEY` for videos without captions,
|
|
136
|
+
`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
|
|
137
|
+
`AUTOBROWSE_URL` and `AUTOBROWSE_TOKEN` (a worker's HTTP face) or `RESTATE_INGRESS_URL` and
|
|
138
|
+
`RESTATE_AUTH_TOKEN` (the Restate ingress a desk worker sits behind: `POST /desk/call`;
|
|
139
|
+
`AUTOBROWSE_SERVICE` for another service name). The same worker gives the `reddit` and
|
|
140
|
+
`facebook` readers. `BIRDWATCH_ACCOUNTS` names the account each site reader may sign in as
|
|
141
|
+
(`reddit=reddit@alt`); a site with no entry reads signed out only. The ledger folder is `--ledger` or
|
|
142
|
+
`BIRDWATCH_LEDGER` (default `./ledger`); `--writer` or `BIRDWATCH_WRITER` names your file in it.
|
|
143
|
+
The judge is `--judge` or `BIRDWATCH_JUDGE` (`jev`, `jev:<model>`, `fake`, `off`); with neither,
|
|
144
|
+
`jev` when `TYPESAFE_API_KEY` is set, else none. `--no-judge` turns it off for one command.
|
|
145
|
+
|
|
146
|
+
Each open source claims the URLs it opens (`opens`): `youtube` a YouTube page, `fetcher` and
|
|
147
|
+
`ladder` any other http(s) page, `reddit` a Reddit thread or comment permalink, `facebook` a
|
|
148
|
+
public group post. A site's own reader (`first`) gets its turn before the general openers,
|
|
149
|
+
whatever the allow-list's order: it answers the thread as data, where a fetched page is a wall.
|
|
150
|
+
`reddit` reads signed out first (`reddit-public`, the thread's feed); only when that misses and
|
|
151
|
+
`BIRDWATCH_ACCOUNTS` names an account does it ask again signed in as it. `facebook` is always
|
|
152
|
+
signed out. A spent cap or a refused read is a miss; a busy profile (503) or no answer retries.
|
|
153
|
+
A refusal carries the worker's access code in its reason: `need_login`, `platform_blocked`,
|
|
154
|
+
`rate_limited` or `upstream_broken`. The last two never read signed in: that fixes neither.
|
|
155
|
+
The read step asks before a call, so a source that was never
|
|
156
|
+
going to open a hit writes no spend. A page that was fetched with nothing usable (a 403, a block
|
|
157
|
+
the ladder's last rung still hit, a media no reader takes, a ladder answer that is not HTML such
|
|
158
|
+
as a PDF) is a miss: the source throws, its `spend` says `ok: false` with the reason, the next
|
|
159
|
+
opener gets its turn, and a hit every opener missed is a `gap` that lists each reason. A page
|
|
160
|
+
that answered 200 and still holds nothing (a captcha wall, a "not found" shell, under 200
|
|
161
|
+
characters of text) is the same miss, with the title or the count as the reason. A run's
|
|
162
|
+
snapshot pins its sources at `plan`: `sources`, the allow-list, which search, open and follow;
|
|
163
|
+
`opens`, the registry's other openers (unless `--sources` was given), which open and follow but
|
|
164
|
+
never search. `open --with <source>` writes a new snapshot that adds one to `opens` (the read
|
|
165
|
+
note names them when hits missed). The
|
|
166
|
+
ladder costs browser time and one of the worker's daily `fetch` bucket per call. Exa's search
|
|
167
|
+
asks for one highlight per result (three sentences, billed per result) as the hit's `snippet`.
|
|
168
|
+
|
|
169
|
+
Every model call is a `spend` record with the model id, the tokens in and out, and `usd` from
|
|
170
|
+
the list price (`src/app/prices.ts`: Claude Haiku, Sonnet and Opus 5.5; the gateway's `free`
|
|
171
|
+
alias is $0; another model keeps its tokens, no price). `run` prints the run's total on stderr
|
|
172
|
+
and as `cost` in its JSON, and the last review's edge (`edge`: next and why) the same way;
|
|
173
|
+
`brief` prints the total as `cost`.
|
|
174
|
+
|
|
175
|
+
`run` exits 2 when the run stopped on a step that gave up (no model, no source, a block):
|
|
176
|
+
`--resume` continues it once the cause is fixed. A failure worth another try (a 429, a 5xx, a
|
|
177
|
+
dropped connection) is tried three times with a pause, every attempt a `spend` record; a step
|
|
178
|
+
that still cannot go on writes no `done`, and `run` waits and resumes it, five times at most,
|
|
179
|
+
before it exits 2. Searches, reads and extracts fan out (four reads or extracts at a time);
|
|
180
|
+
each document's records land as its batch settles, and a step run again skips what its records
|
|
181
|
+
show done. An extract that fails for good (the model's answer would not parse, a 400) stops the
|
|
182
|
+
step with its findings so far on record and exits 2: `extract` again asks only for the
|
|
183
|
+
documents without a verdict. A model answer the output cap cut is not a failure: the whole
|
|
184
|
+
findings before the cut go through the gate and the model is asked again for the rest of the
|
|
185
|
+
document, with those findings known (`./document`'s `Cut`; the `spend` carries both calls' tokens).
|
|
186
|
+
|
|
187
|
+
Between rounds the person reads `brief` (or the agent tells them), keeps or drops findings (`add
|
|
188
|
+
call`), and steers the run with free text (`add steer`). Every `steer` record shows first in the
|
|
189
|
+
brief and in the extract's prompt of the rounds after it. `budget.rounds` is a guard
|
|
190
|
+
rail, not a stop rule: past it, the edge's why says why one more round is worth it.
|
|
191
|
+
|
|
192
|
+
The steps talk in English records. A query every searcher answered with no new hit, and a kept
|
|
193
|
+
hit no source could open (a 403, a block the ladder could not pass), each write a `gap`; so does
|
|
194
|
+
`add gap`. The round's review reads the gaps and a person's keep or drop with its why; the next
|
|
195
|
+
round's plan reads them, and the review's why on `again`, under "The last round left open".
|
|
196
|
+
`add edge back --to <step>` writes a person's edge: the fold takes it like the agent's. A `pattern`
|
|
197
|
+
(`add pattern`) is what several kept findings say together, in the agent's words; its count of
|
|
198
|
+
distinct sites is computed from the findings' proofs, never claimed, and `brief.patterns` lists
|
|
199
|
+
them for the report. A run can be a child (`--parent <run> --why`): its snapshot names the parent, its plan reads
|
|
200
|
+
what the parent and siblings found (recall over those runs), and its budget is the parent's at
|
|
201
|
+
most. The plan file's `runs.depth` (2) is a guard rail: a deeper child warns. Two caps stop: the
|
|
202
|
+
children per parent round (`runs.children`, 6) and the whole tree's reads (`runs.reads`, pinned
|
|
203
|
+
on the root's snapshot as `treeReads`; by default the root's reads × rounds × (children + 1)).
|
|
204
|
+
`open` stops or cuts its hits at the tree's cap; `brief.lineage` and `brief.treeRoom` show the
|
|
205
|
+
ancestors and the room left. A
|
|
206
|
+
stopped child's stop, patterns and dead ends (`add dead`) reach the parent's next plan as
|
|
207
|
+
known lines; a running child is open. A `class` (`add class`) says what kind of source a document is (primary data, survey,
|
|
208
|
+
practitioner, vendor, forum, news, other), with why; its findings take it, and a class on one
|
|
209
|
+
finding overrides it. `brief.patterns[].classes` counts sources per class: distinct sites,
|
|
210
|
+
but each forum thread on its own; `echo` flags a pattern whose classed sources are all vendors,
|
|
211
|
+
and `unchecked` lists its findings no check has read. Only the agent or a person writes a
|
|
212
|
+
class. A tree facet's `needs` names the classes that can prove it; `tree.facets[].lacks` says
|
|
213
|
+
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
|
|
214
|
+
findings and patterns: the study's draft gate drops an item that cites nothing kept or has a number
|
|
215
|
+
no cited quote has. `brief.draft` shows the latest. It is never the final write.
|
|
216
|
+
|
|
217
|
+
The check step reads a sample of the round's kept findings (`budget.checks`, 3 by default, or a
|
|
218
|
+
tenth of the kept findings, whichever is more; 0 is none; spread across documents);
|
|
219
|
+
`check --patterns` reads every unchecked finding a pattern rests on, against the text around each quote. The extract gate proves a quote is
|
|
220
|
+
on the page; the check asks whether it backs the sentence. A finding that fails gets a `check`
|
|
221
|
+
record and a drop call; `add call <finding> keep` puts it back. A fail comes with a fix when one
|
|
222
|
+
fits: a narrower sentence the same quote backs. The fix passes the extract gate and a second
|
|
223
|
+
check, then lands as a new finding with `fixes` naming the old one. The review sees every check, and a finding
|
|
224
|
+
carries its document's date, so the review weighs age as the plan file's `age` line says.
|
|
225
|
+
|
|
226
|
+
The judge (Jev, from `@wrenautomation/models/judge`) answers typed questions with probabilities
|
|
227
|
+
and no text, so it never writes a call or a check. `search` asks it of each new hit: does it
|
|
228
|
+
serve the ask, whose side wrote it (the people the ask is about, or a seller to them), what
|
|
229
|
+
class. `open` asks each new page its class. `check` asks each kept finding with a quote: does the
|
|
230
|
+
quote back it, does it credit another source. Each answer is a `judge` record and each call a
|
|
231
|
+
spend. They hint and route: `brief.hits.uncalled` sorts by facets then the judge's serves, and
|
|
232
|
+
shows its answers; `documents.unclassed[].guess` shows its class; a facet with `needs` is not
|
|
233
|
+
enough while its kept hits have a class (the agent's, else the judge's) and none is needed;
|
|
234
|
+
`patterns[].cites` lists findings that credit another source, and the vendor echo warning names
|
|
235
|
+
them. `check` reads the sample plus each finding the judge backs under 0.8: a judge's yes never
|
|
236
|
+
takes one out. `accept` turns the hints into records at the agent's own bar, each with the
|
|
237
|
+
judge's number in its why. `brief.judge` counts its calls, cost, and how often the agent and the
|
|
238
|
+
check agreed with it. A trial (0.22.0): `open` also asks each page its `worth` (would a careful
|
|
239
|
+
researcher cite it for the ask). `documents.toExtract[].worth` shows it; `brief.judge.worth`
|
|
240
|
+
gives its AUC against "a finding on it was kept", and what a gate at a half would skip. Nothing
|
|
241
|
+
gates on it yet.
|
|
242
|
+
|
|
243
|
+
From 0.25.0 the judge reads a page whole: the body to its references (an appendix stays), in
|
|
244
|
+
parts of 64,000 characters, 4 at most, each answer the most yes of any part. Jev's input is
|
|
245
|
+
cheap (about $0.04 per million tokens), so a paper costs about a tenth of a cent. A run's
|
|
246
|
+
criteria (`--criterion name="question"`, on `plan`) are the person's own yes-or-no tests of a
|
|
247
|
+
good source: the judge asks each one on every hit and every page, and the brief shows its odds
|
|
248
|
+
on each hit (serves and each test) as a hint. Their mean sorts `hits.uncalled` and `documents.toExtract`; `brief.judge.criteria` gives each one's AUC against
|
|
249
|
+
"a finding on it was kept", on hits and on pages. A sort and a hint, never a gate.
|
|
250
|
+
|
|
251
|
+
From 0.27.0 the extractor reads the body too (the reference list holds no finding). Each review
|
|
252
|
+
sees every earlier round's new hits and kept findings, and decides when to stop. A paper's
|
|
253
|
+
`follow --as refs` and `--as citers` list what it cites and what cites it. A check (25 papers, Sonnet as reference):
|
|
254
|
+
on the whole page, Jev's criteria odds rank Sonnet's yes over its no with AUC 0.94 to 1.00; on the
|
|
255
|
+
abstract, 0.77 to 1.00.
|
|
256
|
+
|
|
257
|
+
A follow is a query record with `follow: {url, as, document?}` and terms `<as> of <url>`. The
|
|
258
|
+
fetch step sends it to the sources whose `follows` takes it, `follow` lists up to 30, and the
|
|
259
|
+
hits go through triage, the read and the caps like a search's. Each query goes to the allow-list
|
|
260
|
+
as it stood when it was written: a source `follow --with` lets in joins `opens`, so it runs
|
|
261
|
+
follows, never searches. A
|
|
262
|
+
`person` names one author across sites (profile URLs, names they post under); a document is
|
|
263
|
+
theirs when a posts follow from their profile found it, when its URL sits under a profile, or
|
|
264
|
+
when its channel (or its hit's) is one of their names. `brief.people` counts each person's
|
|
265
|
+
documents, kept findings and sites; `brief.unfollowed` lists kept pages no follow started from
|
|
266
|
+
and no person owns.
|
|
267
|
+
|
|
268
|
+
`serve` puts the ledger on 127.0.0.1 for a viewer (the Wren portal's Research page). Read only:
|
|
269
|
+
the CLI stays the one writer. `GET /runs` gives a row per run (ask, parent, round, next step,
|
|
270
|
+
counts, spend). `GET /runs/:run` gives a `RunView` (`v: 1`): the brief, every query with the hits
|
|
271
|
+
it wrote and found again, every hit with its sources, queries, call and judge, every page with its
|
|
272
|
+
class, verdict and person, every finding with its checks, the timeline of decisions and step ends
|
|
273
|
+
with their whys, and spend per source. `GET /runs/:run/documents/:doc` gives a page's stored text.
|
|
274
|
+
`GET /events` sends `event: run` with `{run}` each time a run's files change, and a ping each 25 s;
|
|
275
|
+
the viewer then gets that run again. CORS answers `--origin` (the portal by default) and any
|
|
276
|
+
localhost, and says yes to Chrome's private-network preflight. The types live in
|
|
277
|
+
`src/engine/view.ts`; a host copies them and checks `v`, never imports them.
|
|
278
|
+
|
|
279
|
+
## Library
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { PoliteFetcher, userAgent } from "@wrenautomation/read/fetch";
|
|
283
|
+
import { readPage } from "@wrenautomation/read/text";
|
|
284
|
+
|
|
285
|
+
const fetcher = new PoliteFetcher(userAgent("you@example.com"));
|
|
286
|
+
const res = await fetcher.get(url);
|
|
287
|
+
const page = readPage(res.text, res.url);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The engine, with your own model and sources:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { claimExtractor } from "@wrenautomation/birdwatch/document";
|
|
294
|
+
import { extract, open, planRound, registry, search } from "@wrenautomation/birdwatch/engine";
|
|
295
|
+
import { FilesLedger, ulids } from "@wrenautomation/birdwatch/ledger";
|
|
296
|
+
import { exa, fetcherSource } from "@wrenautomation/birdwatch/sources";
|
|
297
|
+
|
|
298
|
+
const ledger = new FilesLedger("./ledger", "will");
|
|
299
|
+
const sources = registry(exa({ keys }), fetcherSource({ fetcher }));
|
|
300
|
+
const deps = { sources, ledger, model, extractor: claimExtractor, clock: () => new Date(), ids: ulids(), writer: "will" };
|
|
301
|
+
const agent = { from: "ai" as const, by: "my-agent" };
|
|
302
|
+
const { run } = await planRound(deps, { ask: { text: "how do staffing firms win repeat clients", kind: "web" }, planFile, model: model.id });
|
|
303
|
+
await search(deps, run, [{ terms: "staffing repeat client rate", why: "the ask's words" }], agent);
|
|
304
|
+
await open(deps, run, { urls: ["https://a.example/report"], why: "a survey" }, agent);
|
|
305
|
+
await extract(deps, run); // findings, each with its proof: a quote on a page the run read
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
A host with its own ledger, sources or model mounts
|
|
309
|
+
the same commands: `addEngineCommands(program, { ledger, sources, model, extractor })` from
|
|
310
|
+
`@wrenautomation/birdwatch` (wren's `research` command is these over Postgres).
|
|
311
|
+
|
|
312
|
+
## Develop
|
|
313
|
+
|
|
314
|
+
```sh
|
|
315
|
+
pnpm install
|
|
316
|
+
pnpm lint && pnpm typecheck && pnpm test
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A `v*` tag publishes to npm (`.github/workflows/release.yml`, trusted publishing: no stored token).
|
|
320
|
+
|
|
321
|
+
License: PolyForm Strict 1.0.0 (`LICENSE.md`).
|
package/dist/app/cli.js
ADDED
|
@@ -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 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 "../engine/plan.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":";AACA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EACL,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,SAAS,EACT,WAAW,EACX,aAAa,EAEb,SAAS,GACV,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE;KAC1B,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,6EAA6E,CAAC,CAAC;AAE9F,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,8EAA8E,CAAC;KAC3F,MAAM,CACL,iBAAiB,EACjB,qFAAqF,EACrF,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAC9B;KACA,MAAM,CAAC,QAAQ,EAAE,8CAA8C,CAAC;KAChE,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,CAAsC,EAAE,EAAE;IACpE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;;QAC3F,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7E,CAAC,CAAC,CAAC;AAEL,6FAA6F;AAC7F,MAAM,UAAU,UAAU,CAAC,CAAY;IACrC,MAAM,KAAK,GAA4C;QACrD,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC;QAC/B,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;QAClB,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC;QACd,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC;QACtB,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;QACxB,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;QACxB,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC;KAC3B,CAAC;IACF,MAAM,KAAK,GAAG,KAAK;SAChB,MAAM,CAAC,CAAC,CAAC,EAAkC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;SACjE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SACzE,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3D,CAAC;AAED,OAAO;KACJ,OAAO,CAAC,YAAY,CAAC;KACrB,WAAW,CACV,oJAAoJ,CACrJ;KACA,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC;KACpC,MAAM,CAAC,iBAAiB,EAAE,mBAAmB,EAAE,QAAQ,CAAC;KACxD,MAAM,CAAC,gBAAgB,EAAE,oBAAoB,EAAE,QAAQ,CAAC;KACxD,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,CAAoD,EAAE,EAAE;IAClF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACrE,IAAI,IAAe,CAAC;IACpB,IAAI,oDAAoD,CAAC,IAAI,CAAC,GAAG,CAAC;QAChE,IAAI,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;SACtD,CAAC;QACJ,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QACvD,IAAI,CAAC,UAAU,CAAC,MAAM;YACpB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;QAC5E,IAAI,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YAC1B,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,GAAG,EAAE,SAAS,CAAC,UAAU,CAAC;YAC1B,GAAG,EAAE,YAAY,CAAC,UAAU,CAAC;YAC7B,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CACV,qFAAqF,CACtF;KACA,MAAM,CAAC,YAAY,EAAE,sBAAsB,EAAE,MAAM,CAAC;KACpD,MAAM,CAAC,UAAU,EAAE,uBAAuB,EAAE,MAAM,CAAC;KACnD,MAAM,CAAC,gBAAgB,EAAE,oBAAoB,EAAE,QAAQ,CAAC;KACxD,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,CAAgD,EAAE,EAAE;IACjF,MAAM,KAAK,GACT,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5F,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,6BAA6B,CAAC;KAC1C,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;IAC7B,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3F,IAAI,GAAG,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,0CAA0C,CAAC,CAAC;IACrF,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACnB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,6DAA6D,CAAC;KAC1E,MAAM,CAAC,aAAa,EAAE,0CAA0C,CAAC;KACjE,MAAM,CAAC,KAAK,EAAE,IAAY,EAAE,CAAmB,EAAE,EAAE;IAClD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,UAAU,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC,CAAC,CAAC;AAUL,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,gDAAgD,CAAC;KAC7D,MAAM,CAAC,QAAQ,EAAE,oBAAoB,CAAC;KACtC,MAAM,CAAC,aAAa,EAAE,wBAAwB,CAAC;KAC/C,MAAM,CAAC,mBAAmB,EAAE,4BAA4B,EAAE,IAAI,CAAC;KAC/D,MAAM,CAAC,aAAa,EAAE,wBAAwB,EAAE,MAAM,EAAE,CAAC,CAAC;KAC1D,MAAM,CAAC,iBAAiB,EAAE,mBAAmB,EAAE,QAAQ,CAAC;KACxD,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,CAAa,EAAE,EAAE;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IACvD,IAAI,CAAC,UAAU,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE;QACrC,GAAG,EAAE,SAAS,CAAC,UAAU,CAAC;QAC1B,GAAG,EAAE,YAAY,CAAC,UAAU,CAAC;QAC7B,KAAK,EAAE,MAAM,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QAC7D,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,KAAK;KAChB,CAAC,CAAC;IACH,IAAI,CAAC,CAAC,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;;QACrD,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACzC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,CAAC,KAAK,CACX,GAAG,OAAO,CAAC,SAAS,MAAM,IAAI,iBAAiB,MAAM,cAAc,OAAO,CAAC,IAAI,CAAC,MAAM,UAAU,OAAO,CAAC,MAAM,CAAC,MAAM,kBAAkB,CACxI,CAAC;AACJ,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,cAAc,CAAC;KACvB,WAAW,CAAC,4DAA4D,CAAC;KACzE,MAAM,CAAC,CAAC,IAAa,EAAE,EAAE;IACxB,MAAM,KAAK,GAAG,CAAC,CAAU,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtE,IAAI,IAAI;QAAE,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;QACjD,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC;AAEL,iBAAiB,CAAC,OAAO,CAAC,CAAC;AAE3B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACtD,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { type AskKind, Criterion } from "../engine/plan.js";
|
|
7
|
+
import { FilesLedger, type Ledger, type Problem } from "../ledger/index.js";
|
|
8
|
+
export { costOf };
|
|
9
|
+
/** `NAME`, then `NAME_1`, `NAME_2`, ... in order: a fleet of keys for one vendor. */
|
|
10
|
+
export declare function keys(printed: NodeJS.ProcessEnv, name: string): string[];
|
|
11
|
+
/** `name=question` lines as criteria; a name the judge already asks, or a repeat, fails. */
|
|
12
|
+
export declare function criteriaOf(lines: readonly string[] | undefined): Criterion[];
|
|
13
|
+
export interface Common {
|
|
14
|
+
ledger: string;
|
|
15
|
+
writer: string;
|
|
16
|
+
}
|
|
17
|
+
/** A ledger a host hands the commands: the contract, and what the files backend adds when it has it. */
|
|
18
|
+
export type HostLedger = Ledger & {
|
|
19
|
+
problems?: Problem[];
|
|
20
|
+
};
|
|
21
|
+
/** What a host swaps in: the files backend, this machine's sources and `--model` are the defaults. */
|
|
22
|
+
export interface EngineCliDeps {
|
|
23
|
+
ledger?(o: Common): HostLedger;
|
|
24
|
+
sources?(o: Common, ledger: HostLedger): Sources;
|
|
25
|
+
/** The model for a `--model` spec; the default is models' `modelFromSpec`. */
|
|
26
|
+
model?(spec: string, printed: NodeJS.ProcessEnv): Model;
|
|
27
|
+
/** The judge for a `--judge` spec; the default is models' `judgeFromSpec`. */
|
|
28
|
+
judge?(spec: string, printed: NodeJS.ProcessEnv): Judge | null;
|
|
29
|
+
extractor?: Extractor;
|
|
30
|
+
}
|
|
31
|
+
/** The sources this machine can run, from the environment. */
|
|
32
|
+
export declare function sourcesHere(printed: NodeJS.ProcessEnv, ledger: FilesLedger, ytDlp: string, warn?: (line: string) => void): Sources;
|
|
33
|
+
/**
|
|
34
|
+
* The run's allow-list: the names wanted that exist here. `opens`: unless the list was given by
|
|
35
|
+
* hand, every other opener here, let in to open and follow, never to search (arxiv searched every
|
|
36
|
+
* web run when it joined the list). A name wanted that this machine lacks is dropped with a line on stderr.
|
|
37
|
+
*/
|
|
38
|
+
export declare function allowList(sources: Sources, wanted: readonly string[], byHand: boolean, warn?: (line: string) => void): {
|
|
39
|
+
sources: string[];
|
|
40
|
+
opens: string[];
|
|
41
|
+
};
|
|
42
|
+
/** A kind the engine plans for; anything else is the caller's mistake, said in words. */
|
|
43
|
+
export declare const kindOf: (kind: string) => AskKind;
|
|
44
|
+
export declare function addEngineCommands(program: Command, deps?: EngineCliDeps): void;
|