fanfou 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leaking
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,122 @@
1
+ # 饭否 CLI (`fanfou`)
2
+
3
+ An **LLM-friendly** command line for the [Fanfou (饭否)](https://fanfou.com) API,
4
+ plus a bundled **agent skill** that works with Claude Code, Codex, Cursor, and
5
+ 50+ other AI coding agents. JSON-by-default output, schema introspection,
6
+ dry-run previews, and a three-layer command surface (shortcuts / resource
7
+ commands / raw API).
8
+
9
+ ## Requirements
10
+
11
+ - Node.js **≥ 22.6** (the CLI runs TypeScript directly via type-stripping — no build step needed).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ # 1) The CLI (provides the `fanfou` command):
17
+ npm install -g fanfou # or one-off, no install: npx fanfou <command>
18
+
19
+ # 2) The skill, into your AI agent(s) — via npx skills (GitHub as registry):
20
+ npx skills add Leaking/fanfou-cli -y -g
21
+ # …or target specific agents (Claude Code, Codex, Cursor, 50+ more):
22
+ npx skills add Leaking/fanfou-cli --agent claude-code codex cursor
23
+ ```
24
+
25
+ Run it:
26
+
27
+ ```bash
28
+ fanfou --help
29
+ ```
30
+
31
+ Developing from a clone (Node ≥ 22.6 runs the TypeScript directly — no build):
32
+
33
+ ```bash
34
+ node src/index.ts --help
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ ```bash
40
+ # 1) Log in (XAuth). Use env vars to keep secrets out of argv.
41
+ FANFOU_USERNAME='you@example.com' FANFOU_PASSWORD='••••••' fanfou auth login
42
+
43
+ # 2) Read your timeline
44
+ fanfou +timeline --count 10
45
+ fanfou +timeline --format table
46
+
47
+ # 3) Post / reply / repost
48
+ fanfou +post "Hello 饭否"
49
+ fanfou +reply <status-id> "说得对"
50
+ fanfou +repost <status-id>
51
+
52
+ # 4) Anything else, raw:
53
+ fanfou api GET statuses/mentions.json --query count=5
54
+ ```
55
+
56
+ ## Three layers
57
+
58
+ 1. **Shortcuts** (`+timeline`, `+post`, `+reply`, `+repost`, `+mentions`, `+me`,
59
+ `+search`, `+fav`, `+dm`) — high-frequency operations with smart defaults.
60
+ 2. **Resource commands** — `auth`, `timeline`, `status`, `favorite`, `user`,
61
+ `friendship`, `dm`, `account`, `search` (full Fanfou API coverage).
62
+ 3. **Raw API** — `fanfou api <GET|POST> <path> [--query ...] [--form ...]`.
63
+
64
+ ## Authentication
65
+
66
+ Two flows, stored per-profile under `~/.config/fanfou/config.json` (mode `0600`):
67
+
68
+ - **XAuth** (default): `fanfou auth login -u <name> -p <pass>`
69
+ - **OAuth web**: `fanfou auth oauth-url` → approve in browser →
70
+ `fanfou auth oauth-exchange --token … --secret … [--verifier …]`
71
+
72
+ Env overrides (handy for CI / agents): `FANFOU_USERNAME`, `FANFOU_PASSWORD`,
73
+ `FANFOU_OAUTH_TOKEN`, `FANFOU_OAUTH_TOKEN_SECRET`, `FANFOU_CONSUMER_KEY`,
74
+ `FANFOU_CONSUMER_SECRET`, `FANFOU_PROFILE`, `FANFOU_CONFIG_DIR`.
75
+
76
+ Multiple accounts: pass `--profile <name>` to any command, or
77
+ `fanfou auth use <name>` to change the default.
78
+
79
+ ## LLM / agent friendliness
80
+
81
+ - **JSON by default** on stdout; errors are JSON on stderr with exit codes
82
+ (`2` usage, `3` auth-required, `1` other/HTTP).
83
+ - **Schema introspection**: `fanfou <cmd> --help --format json`.
84
+ - **Dry-run** (`--dry-run` / `-n`) on every state-changing command prints the
85
+ exact signed request without sending it.
86
+ - **Output formats**: `--format json|ndjson|table|raw` (`-o`).
87
+ - Robust arg parsing for Fanfou IDs that begin with `-`.
88
+
89
+ ## The bundled agent skill
90
+
91
+ A single, cross-agent skill lives at
92
+ [`skills/fanfou/SKILL.md`](skills/fanfou/SKILL.md). It is consumable by any
93
+ AI coding agent that supports the [skills](https://github.com/vercel-labs/skills)
94
+ convention — Claude Code, Codex, Cursor, Cline, Aider, Continue, and 50+ more.
95
+
96
+ Install it globally for all of your agents at once, or scope it to a specific
97
+ agent:
98
+
99
+ ```bash
100
+ npx skills add Leaking/fanfou-cli -y -g # all agents
101
+ npx skills add Leaking/fanfou-cli --agent claude-code codex cursor # specific
102
+ ```
103
+
104
+ From a local clone, you can also drop the skill into a Claude Code skills
105
+ directory directly (other agents typically use the `skills` CLI above):
106
+
107
+ ```bash
108
+ npm run install-skill # -> ./.claude/skills/fanfou (project)
109
+ npm run install-skill -- --user # -> ~/.claude/skills/fanfou (global)
110
+ ```
111
+
112
+ ## Develop
113
+
114
+ ```bash
115
+ npm run typecheck # tsc --noEmit
116
+ npm test # node --test (OAuth1 signer vectors)
117
+ npm run build # emit dist/
118
+ ```
119
+
120
+ The OAuth1 HMAC-SHA1 signer matches the canonical RFC 5849 example signature
121
+ (pinned by `test/oauth1.test.ts`). Fanfou signs the base string over `http://`
122
+ even on HTTPS requests — the signer replicates that quirk.
package/dist/args.js ADDED
@@ -0,0 +1,91 @@
1
+ const SHORT_ALIASES = {
2
+ h: "help",
3
+ o: "format",
4
+ n: "dry-run",
5
+ v: "verbose",
6
+ q: "quiet",
7
+ };
8
+ /**
9
+ * Minimal, predictable argument parser.
10
+ * - `--key=value` and `--key value` both bind a value.
11
+ * - `--flag` is boolean true when its name is in `booleanFlags` or no value follows.
12
+ * - `+shortcut` and other bare tokens are positionals.
13
+ * - `--` stops flag parsing; everything after is positional.
14
+ */
15
+ export function parseArgs(argv, booleanFlags) {
16
+ const positionals = [];
17
+ const flags = new Map();
18
+ let onlyPositionals = false;
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const token = argv[i];
21
+ if (onlyPositionals) {
22
+ positionals.push(token);
23
+ continue;
24
+ }
25
+ if (token === "--") {
26
+ onlyPositionals = true;
27
+ continue;
28
+ }
29
+ if (token.startsWith("--")) {
30
+ const body = token.slice(2);
31
+ const eq = body.indexOf("=");
32
+ if (eq >= 0) {
33
+ flags.set(body.slice(0, eq), body.slice(eq + 1));
34
+ continue;
35
+ }
36
+ const name = body;
37
+ if (booleanFlags.has(name)) {
38
+ flags.set(name, true);
39
+ continue;
40
+ }
41
+ const next = argv[i + 1];
42
+ if (next !== undefined && !(next.startsWith("--") || next === "-")) {
43
+ flags.set(name, next);
44
+ i++;
45
+ }
46
+ else {
47
+ flags.set(name, true);
48
+ }
49
+ continue;
50
+ }
51
+ // Only treat an exact single-letter known short (e.g. -h, -u, -o) as a flag.
52
+ // Anything else starting with "-" is a positional — Fanfou ids often begin
53
+ // with "-" (e.g. -A_ycI00_Kc), so we must not misread them as flags.
54
+ const short = token.slice(1);
55
+ if (token.startsWith("-") && short.length === 1 && short in SHORT_ALIASES) {
56
+ const long = SHORT_ALIASES[short];
57
+ if (booleanFlags.has(long)) {
58
+ flags.set(long, true);
59
+ }
60
+ else {
61
+ const next = argv[i + 1];
62
+ if (next !== undefined && !next.startsWith("-")) {
63
+ flags.set(long, next);
64
+ i++;
65
+ }
66
+ else {
67
+ flags.set(long, true);
68
+ }
69
+ }
70
+ continue;
71
+ }
72
+ positionals.push(token);
73
+ }
74
+ return { positionals, flags };
75
+ }
76
+ export function flagString(flags, name) {
77
+ const value = flags.get(name);
78
+ if (value === undefined)
79
+ return undefined;
80
+ return typeof value === "string" ? value : "";
81
+ }
82
+ export function flagBool(flags, name) {
83
+ return flags.has(name) && flags.get(name) !== "false";
84
+ }
85
+ export function flagNumber(flags, name) {
86
+ const value = flagString(flags, name);
87
+ if (value === undefined || value === "")
88
+ return undefined;
89
+ const n = Number(value);
90
+ return Number.isFinite(n) ? n : undefined;
91
+ }