ants-move 0.0.1
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 +21 -0
- package/README.md +114 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +2214 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# ants-move
|
|
2
|
+
|
|
3
|
+
`ants-move` is an open-source TypeScript CLI for moving data between systems. Its commands are small workers: collectors bring data in, and future automation commands can move that data into forms and other systems. The first release collects data from 36Kr, Toutiao, and Hacker News.
|
|
4
|
+
|
|
5
|
+
## Installation and requirements
|
|
6
|
+
|
|
7
|
+
Requirements:
|
|
8
|
+
|
|
9
|
+
- Node.js 22 or later
|
|
10
|
+
- npm
|
|
11
|
+
- `curl` on `PATH` for 36Kr collection
|
|
12
|
+
- Chromium installed through Playwright for Toutiao collection
|
|
13
|
+
|
|
14
|
+
Install the package globally, then install the browser used by Toutiao:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g ants-move
|
|
18
|
+
npx playwright@1.59.1 install chromium
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package installs two equivalent executables: `ants` and the short alias `amv`.
|
|
22
|
+
|
|
23
|
+
## Output conventions
|
|
24
|
+
|
|
25
|
+
Commands write successful results to stdout and structured errors to stderr. JSON is the default format. List and search commands accept `--format table` or the `-t` shortcut for table output. Article detail commands support JSON only.
|
|
26
|
+
|
|
27
|
+
Successful JSON uses an `{ "ok": true, "data": ... }` envelope. Errors use an `{ "ok": false, "error": ... }` envelope and return a nonzero exit code.
|
|
28
|
+
|
|
29
|
+
## 36Kr commands
|
|
30
|
+
|
|
31
|
+
Fetch an article by numeric ID:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ants 36kr article <article-id> [--format json]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Fetch one to 20 information pages from the `AI` or `technology` channel:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
ants 36kr list <AI|technology> [--pages 1..20] [--format json|table] [-t]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Pagination is serial and bounded at 20 pages per invocation. Each curl response and mapped article result is limited to 20 MB, each list page to 30 items, and the mapped list to 5 MB, so a list retains at most 600 items without accumulating oversized fields across pages.
|
|
44
|
+
|
|
45
|
+
## Toutiao commands and verification limitations
|
|
46
|
+
|
|
47
|
+
Fetch an article by numeric ID or a Toutiao article, group, or legacy URL:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ants toutiao article <article-id-or-url> [--format json]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Fetch one to five pages from the technology channel or a supported search keyword:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ants toutiao list <tech|AI|光刻机|芯片|半导体> [--pages 1..5] [--format json|table] [-t]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Fetch an author feed by token or `/c/user/token/<token>/` profile URL:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ants toutiao author <token-or-url> [--pages 1..5] [--with-content] [--format json|table] [-t]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Feed collection keeps at most the requested number of response parses active, starts at most twice that many bounded parse attempts to replace malformed responses, and accepts at most five successful responses. Each feed body is limited to 5 MB before JSON parsing and 100 raw items after parsing.
|
|
66
|
+
|
|
67
|
+
Article extraction limits title, body, and paragraph text to 1 MB each. Search fallback inspection serializes at most 300 bounded links and rejects body text larger than 1 MB. `--with-content` fetches article details serially in the same browser session, accepts at most 100 unique articles, and retains at most 10 MB of article results per invocation. Toutiao can require interactive browser verification; when verification prevents usable search results, the command returns `TOUTIAO_VERIFICATION_REQUIRED`. The CLI does not attempt to bypass site verification.
|
|
68
|
+
|
|
69
|
+
## Hacker News commands and aliases
|
|
70
|
+
|
|
71
|
+
Fetch one to 100 stories from each Firebase list:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ants hn top [--limit 1..100] [--format json|table] [-t]
|
|
75
|
+
ants hn new [--limit 1..100] [--format json|table] [-t]
|
|
76
|
+
ants hn best [--limit 1..100] [--format json|table] [-t]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Search through Algolia, ordered by relevance or date:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ants hn search <query> [--limit 1..100] [--sort relevance|date] [--format json|table] [-t]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`hackernews` is an alias for `hn`, so `ants hackernews top` is equivalent to `ants hn top`. The `amv` executable accepts every command shown above, for example `amv hackernews search typescript`.
|
|
86
|
+
|
|
87
|
+
Hacker News uses at most eight concurrent detail requests. ID-list and search responses are limited to 5 MB, while each Firebase story detail is limited to 256 KB.
|
|
88
|
+
|
|
89
|
+
## High-concurrency usage warning
|
|
90
|
+
|
|
91
|
+
Resource use is bounded within one process, but identical commands running in separate processes are not globally deduplicated or rate limited. At `Q` concurrent invocations, upstream work can approach `20Q` requests for a 36Kr list, `201Q` requests for a Hacker News list, or `105Q` browser navigations plus page subresources for a Toutiao author collection.
|
|
92
|
+
|
|
93
|
+
Retained collector data is also bounded per invocation: 5 MB for a 36Kr list, approximately 50 MB across 200 Hacker News candidate details before result filtering, and 10 MB for Toutiao author article results. Toutiao may additionally hold up to five active 5 MB feed buffers inside one browser session; browser process overhead and required page subresources are separate from these payload limits.
|
|
94
|
+
|
|
95
|
+
Do not place the CLI directly on a high-QPS request path. Online services must use an external bounded queue and a shared rate limiter to apply backpressure across processes and instances. This project has no distributed lock, shared cache, retry queue, or multi-instance single-flight mechanism.
|
|
96
|
+
|
|
97
|
+
## Development and verification
|
|
98
|
+
|
|
99
|
+
Install dependencies and run the deterministic checks:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install
|
|
103
|
+
npm run check
|
|
104
|
+
npm test
|
|
105
|
+
npm run coverage
|
|
106
|
+
npm run build
|
|
107
|
+
npm pack --json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Tests use injected network and browser boundaries and do not contact live websites.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
Licensed under the [MIT License](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
type HackerNewsStorySource = 'top' | 'new' | 'best';
|
|
2
|
+
type HackerNewsSearchSort = 'relevance' | 'date';
|
|
3
|
+
interface HackerNewsItem {
|
|
4
|
+
author?: string;
|
|
5
|
+
commentCount?: number;
|
|
6
|
+
id: number;
|
|
7
|
+
score?: number;
|
|
8
|
+
text?: string;
|
|
9
|
+
time?: {
|
|
10
|
+
iso: string;
|
|
11
|
+
seconds: number;
|
|
12
|
+
};
|
|
13
|
+
title: string;
|
|
14
|
+
type: 'story';
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
interface HackerNewsStoriesRuntimeResult {
|
|
18
|
+
items: HackerNewsItem[];
|
|
19
|
+
source: HackerNewsStorySource;
|
|
20
|
+
}
|
|
21
|
+
interface HackerNewsSearchRuntimeResult {
|
|
22
|
+
items: HackerNewsItem[];
|
|
23
|
+
query: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HackerNewsRuntime {
|
|
27
|
+
fetchSearch: (options: {
|
|
28
|
+
limit: number;
|
|
29
|
+
query: string;
|
|
30
|
+
sort: HackerNewsSearchSort;
|
|
31
|
+
}) => Promise<HackerNewsSearchRuntimeResult>;
|
|
32
|
+
fetchStories: (options: {
|
|
33
|
+
limit: number;
|
|
34
|
+
source: HackerNewsStorySource;
|
|
35
|
+
}) => Promise<HackerNewsStoriesRuntimeResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Kr36Request {
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
url: string;
|
|
41
|
+
}
|
|
42
|
+
interface Kr36JsonRequest {
|
|
43
|
+
body: unknown;
|
|
44
|
+
headers: Record<string, string>;
|
|
45
|
+
url: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Kr36Runtime {
|
|
49
|
+
fetchArticleHtml: (request: Kr36Request) => Promise<string>;
|
|
50
|
+
fetchJson: (request: Kr36JsonRequest) => Promise<string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ToutiaoSource = 'tech' | 'AI' | '光刻机' | '芯片' | '半导体';
|
|
54
|
+
interface ToutiaoItem {
|
|
55
|
+
abstract?: string;
|
|
56
|
+
authorName?: string;
|
|
57
|
+
commentCount?: number;
|
|
58
|
+
id: string;
|
|
59
|
+
image?: string;
|
|
60
|
+
publishTime?: {
|
|
61
|
+
iso: string;
|
|
62
|
+
local: string;
|
|
63
|
+
seconds: number;
|
|
64
|
+
};
|
|
65
|
+
sourceUrl?: string;
|
|
66
|
+
title: string;
|
|
67
|
+
url: string;
|
|
68
|
+
}
|
|
69
|
+
interface ToutiaoAuthorRuntimeResult {
|
|
70
|
+
authorToken: string;
|
|
71
|
+
hasMore: boolean;
|
|
72
|
+
items: ToutiaoItem[];
|
|
73
|
+
next?: {
|
|
74
|
+
maxBehotTime?: number;
|
|
75
|
+
offset?: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
interface ToutiaoArticle {
|
|
79
|
+
authorName?: string;
|
|
80
|
+
content: {
|
|
81
|
+
paragraphs: string[];
|
|
82
|
+
text: string;
|
|
83
|
+
};
|
|
84
|
+
id: string;
|
|
85
|
+
publishTimeText?: string;
|
|
86
|
+
request: {
|
|
87
|
+
input: string;
|
|
88
|
+
url: string;
|
|
89
|
+
};
|
|
90
|
+
title: string;
|
|
91
|
+
url: string;
|
|
92
|
+
}
|
|
93
|
+
interface ToutiaoRuntimeListResult {
|
|
94
|
+
hasMore: boolean;
|
|
95
|
+
items: ToutiaoItem[];
|
|
96
|
+
keyword?: string;
|
|
97
|
+
next?: {
|
|
98
|
+
maxBehotTime?: number;
|
|
99
|
+
offset?: number;
|
|
100
|
+
};
|
|
101
|
+
source: ToutiaoSource;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ToutiaoSession {
|
|
105
|
+
fetchArticle: (request: {
|
|
106
|
+
input: string;
|
|
107
|
+
url: string;
|
|
108
|
+
}) => Promise<ToutiaoArticle>;
|
|
109
|
+
fetchAuthorArticles: (options: {
|
|
110
|
+
authorToken: string;
|
|
111
|
+
pages: number;
|
|
112
|
+
url: string;
|
|
113
|
+
}) => Promise<ToutiaoAuthorRuntimeResult>;
|
|
114
|
+
fetchKeywordInformation: (options: {
|
|
115
|
+
keyword: ToutiaoSource;
|
|
116
|
+
pages: number;
|
|
117
|
+
source: ToutiaoSource;
|
|
118
|
+
}) => Promise<ToutiaoRuntimeListResult>;
|
|
119
|
+
fetchTechnologyChannel: (options: {
|
|
120
|
+
pages: number;
|
|
121
|
+
}) => Promise<ToutiaoRuntimeListResult>;
|
|
122
|
+
}
|
|
123
|
+
interface ToutiaoRuntime {
|
|
124
|
+
withSession: <T>(operation: (session: ToutiaoSession) => Promise<T>) => Promise<T>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface CliIo {
|
|
128
|
+
stderr: (value: string) => void;
|
|
129
|
+
stdout: (value: string) => void;
|
|
130
|
+
}
|
|
131
|
+
interface CliRunner {
|
|
132
|
+
run: (args: string[]) => Promise<number>;
|
|
133
|
+
}
|
|
134
|
+
interface CreateCliOptions extends Partial<CliIo> {
|
|
135
|
+
hackerNewsRuntime?: HackerNewsRuntime;
|
|
136
|
+
kr36Runtime?: Kr36Runtime;
|
|
137
|
+
toutiaoRuntime?: ToutiaoRuntime;
|
|
138
|
+
}
|
|
139
|
+
declare function createCli(options?: CreateCliOptions): CliRunner;
|
|
140
|
+
|
|
141
|
+
declare function isMainEntrypoint(moduleUrl: string, argv: string[]): boolean;
|
|
142
|
+
|
|
143
|
+
export { type CliIo, type CliRunner, type CreateCliOptions, createCli, isMainEntrypoint };
|