@thenavidm/threads-mcp-cli 1.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 +21 -0
- package/README.md +1019 -0
- package/SKILL.md +203 -0
- package/dist/api/client.d.ts +105 -0
- package/dist/api/client.js +305 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/errors.d.ts +92 -0
- package/dist/api/errors.js +195 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/identity.d.ts +33 -0
- package/dist/api/identity.js +52 -0
- package/dist/api/identity.js.map +1 -0
- package/dist/auth/login.d.ts +32 -0
- package/dist/auth/login.js +204 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/store.d.ts +37 -0
- package/dist/auth/store.js +88 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/auth/tokens.d.ts +54 -0
- package/dist/auth/tokens.js +96 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +444 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +98 -0
- package/dist/config.js +185 -0
- package/dist/config.js.map +1 -0
- package/dist/content/containers.d.ts +89 -0
- package/dist/content/containers.js +210 -0
- package/dist/content/containers.js.map +1 -0
- package/dist/content/media.d.ts +61 -0
- package/dist/content/media.js +125 -0
- package/dist/content/media.js.map +1 -0
- package/dist/content/text.d.ts +68 -0
- package/dist/content/text.js +106 -0
- package/dist/content/text.js.map +1 -0
- package/dist/doctor.d.ts +14 -0
- package/dist/doctor.js +218 -0
- package/dist/doctor.js.map +1 -0
- package/dist/format/posts.d.ts +41 -0
- package/dist/format/posts.js +153 -0
- package/dist/format/posts.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/dist/safety.d.ts +52 -0
- package/dist/safety.js +85 -0
- package/dist/safety.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +232 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/accounts.d.ts +27 -0
- package/dist/tools/accounts.js +162 -0
- package/dist/tools/accounts.js.map +1 -0
- package/dist/tools/discover.d.ts +56 -0
- package/dist/tools/discover.js +146 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/insights.d.ts +55 -0
- package/dist/tools/insights.js +223 -0
- package/dist/tools/insights.js.map +1 -0
- package/dist/tools/kit.d.ts +90 -0
- package/dist/tools/kit.js +119 -0
- package/dist/tools/kit.js.map +1 -0
- package/dist/tools/posts.d.ts +170 -0
- package/dist/tools/posts.js +312 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/read.d.ts +31 -0
- package/dist/tools/read.js +95 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replies.d.ts +92 -0
- package/dist/tools/replies.js +218 -0
- package/dist/tools/replies.js.map +1 -0
- package/dist/transport/http.d.ts +28 -0
- package/dist/transport/http.js +103 -0
- package/dist/transport/http.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where tokens live between runs.
|
|
3
|
+
*
|
|
4
|
+
* A Threads long-lived token is valid for 60 days and can be refreshed for
|
|
5
|
+
* another 60 at any point after it is 24 hours old. Miss that window and it is
|
|
6
|
+
* gone permanently: there is no grace period and no way to refresh an expired
|
|
7
|
+
* one. The whole point of writing tokens to a file the server owns is that the
|
|
8
|
+
* server can then refresh them on its own, which an environment variable pasted
|
|
9
|
+
* into a client config can never do.
|
|
10
|
+
*
|
|
11
|
+
* The file is written 0600, and the directory 0700, because it holds
|
|
12
|
+
* credentials that can post as you. Writes go to a temporary file in the same
|
|
13
|
+
* directory and are then renamed, so a crash mid-write leaves the previous
|
|
14
|
+
* tokens intact rather than a truncated file that locks you out.
|
|
15
|
+
*/
|
|
16
|
+
import type { Account } from "../config.js";
|
|
17
|
+
export type StoredToken = {
|
|
18
|
+
user_id: string;
|
|
19
|
+
username?: string;
|
|
20
|
+
access_token: string;
|
|
21
|
+
/** Unix ms. */
|
|
22
|
+
expires_at?: number;
|
|
23
|
+
/** Unix ms this token was last minted or refreshed. */
|
|
24
|
+
obtained_at?: number;
|
|
25
|
+
scopes?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type TokenFile = {
|
|
28
|
+
version: 1;
|
|
29
|
+
accounts: StoredToken[];
|
|
30
|
+
};
|
|
31
|
+
export declare function readStore(path: string): TokenFile;
|
|
32
|
+
export declare function writeStore(path: string, file: TokenFile): void;
|
|
33
|
+
/** Insert or replace one profile's token, keyed by profile id. */
|
|
34
|
+
export declare function upsertToken(path: string, token: StoredToken): void;
|
|
35
|
+
export declare function removeToken(path: string, userIdOrUsername: string): boolean;
|
|
36
|
+
/** The stored tokens, as accounts the rest of the server understands. */
|
|
37
|
+
export declare function accountsFromStore(path: string): Account[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where tokens live between runs.
|
|
3
|
+
*
|
|
4
|
+
* A Threads long-lived token is valid for 60 days and can be refreshed for
|
|
5
|
+
* another 60 at any point after it is 24 hours old. Miss that window and it is
|
|
6
|
+
* gone permanently: there is no grace period and no way to refresh an expired
|
|
7
|
+
* one. The whole point of writing tokens to a file the server owns is that the
|
|
8
|
+
* server can then refresh them on its own, which an environment variable pasted
|
|
9
|
+
* into a client config can never do.
|
|
10
|
+
*
|
|
11
|
+
* The file is written 0600, and the directory 0700, because it holds
|
|
12
|
+
* credentials that can post as you. Writes go to a temporary file in the same
|
|
13
|
+
* directory and are then renamed, so a crash mid-write leaves the previous
|
|
14
|
+
* tokens intact rather than a truncated file that locks you out.
|
|
15
|
+
*/
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { normalizeUsername } from "../config.js";
|
|
19
|
+
const EMPTY = { version: 1, accounts: [] };
|
|
20
|
+
export function readStore(path) {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return { ...EMPTY, accounts: [] };
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
if (!parsed || typeof parsed !== "object")
|
|
26
|
+
return { ...EMPTY, accounts: [] };
|
|
27
|
+
const accounts = parsed.accounts;
|
|
28
|
+
if (!Array.isArray(accounts))
|
|
29
|
+
return { ...EMPTY, accounts: [] };
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
accounts: accounts.filter((a) => Boolean(a && typeof a.access_token === "string" && typeof a.user_id === "string")),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// A corrupt store must not take the server down. It behaves as empty, and
|
|
37
|
+
// `doctor` reports it, because silently overwriting someone's only working
|
|
38
|
+
// token would be worse than refusing to read it.
|
|
39
|
+
process.stderr.write(`[threads-mcp] Token store at ${path} is unreadable. Ignoring it.\n`);
|
|
40
|
+
return { ...EMPTY, accounts: [] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function writeStore(path, file) {
|
|
44
|
+
const dir = dirname(path);
|
|
45
|
+
if (!existsSync(dir))
|
|
46
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
|
+
const tmp = join(dir, `.tokens.${process.pid}.tmp`);
|
|
48
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
49
|
+
try {
|
|
50
|
+
renameSync(tmp, path);
|
|
51
|
+
chmodSync(path, 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
try {
|
|
55
|
+
unlinkSync(tmp);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Nothing useful to do; the rename failure is the real error.
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Insert or replace one profile's token, keyed by profile id. */
|
|
64
|
+
export function upsertToken(path, token) {
|
|
65
|
+
const file = readStore(path);
|
|
66
|
+
const rest = file.accounts.filter((a) => a.user_id !== token.user_id);
|
|
67
|
+
writeStore(path, { version: 1, accounts: [...rest, token] });
|
|
68
|
+
}
|
|
69
|
+
export function removeToken(path, userIdOrUsername) {
|
|
70
|
+
const file = readStore(path);
|
|
71
|
+
const needle = normalizeUsername(userIdOrUsername);
|
|
72
|
+
const kept = file.accounts.filter((a) => a.user_id !== needle && normalizeUsername(a.username ?? "") !== needle);
|
|
73
|
+
if (kept.length === file.accounts.length)
|
|
74
|
+
return false;
|
|
75
|
+
writeStore(path, { version: 1, accounts: kept });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/** The stored tokens, as accounts the rest of the server understands. */
|
|
79
|
+
export function accountsFromStore(path) {
|
|
80
|
+
return readStore(path).accounts.map((a) => ({
|
|
81
|
+
accessToken: a.access_token,
|
|
82
|
+
userId: a.user_id,
|
|
83
|
+
username: a.username ? normalizeUsername(a.username) : undefined,
|
|
84
|
+
expiresAt: a.expires_at,
|
|
85
|
+
source: "store",
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minting and refreshing Threads tokens.
|
|
3
|
+
*
|
|
4
|
+
* Three token states exist and they are easy to confuse:
|
|
5
|
+
*
|
|
6
|
+
* short-lived 1 hour. What the OAuth code exchange returns.
|
|
7
|
+
* long-lived 60 days. What you get by exchanging a short-lived token,
|
|
8
|
+
* and the only kind worth storing.
|
|
9
|
+
* refreshed 60 days from the refresh. Allowed once the token is at
|
|
10
|
+
* least 24 hours old, and impossible once it has expired.
|
|
11
|
+
*
|
|
12
|
+
* That last sentence is the entire reason this file exists. A token that is
|
|
13
|
+
* never refreshed dies on day 60 and cannot be revived; the person has to walk
|
|
14
|
+
* the whole OAuth flow again. A server that refreshes on a schedule turns a
|
|
15
|
+
* recurring 60-day outage into something nobody has to think about.
|
|
16
|
+
*
|
|
17
|
+
* `maybeRefresh` is called before every request rather than on a timer, because
|
|
18
|
+
* an MCP server is not a daemon. It runs when a client launches it and stops
|
|
19
|
+
* when the client quits, and a timer that fires on day 59 fires in a process
|
|
20
|
+
* that has not existed for weeks.
|
|
21
|
+
*/
|
|
22
|
+
import type { Account, Config } from "../config.js";
|
|
23
|
+
export type TokenResponse = {
|
|
24
|
+
access_token: string;
|
|
25
|
+
token_type?: string;
|
|
26
|
+
/** Seconds until expiry. About 5,183,944 for a fresh long-lived token. */
|
|
27
|
+
expires_in?: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Exchange the short-lived token from the OAuth code flow for a 60-day one.
|
|
31
|
+
*
|
|
32
|
+
* This needs the app secret, which is why `login` needs THREADS_APP_SECRET and
|
|
33
|
+
* pasting a token from Meta's Graph API Explorer does not: that explorer hands
|
|
34
|
+
* out short-lived tokens, and a short-lived token pasted into a config stops
|
|
35
|
+
* working in an hour. `login` is the path that produces something durable.
|
|
36
|
+
*/
|
|
37
|
+
export declare function exchangeForLongLived(shortLivedToken: string, appSecret: string, host: string, timeoutMs?: number): Promise<TokenResponse>;
|
|
38
|
+
/** Extend a long-lived token by another 60 days. No app secret required. */
|
|
39
|
+
export declare function refreshLongLived(token: string, host: string, timeoutMs?: number): Promise<TokenResponse>;
|
|
40
|
+
/** Days until this token expires, or undefined when the expiry is unknown. */
|
|
41
|
+
export declare function daysRemaining(account: Account, now?: number): number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Whether this token should be refreshed now.
|
|
44
|
+
*
|
|
45
|
+
* Two conditions, both from Meta's rules. It has to be at least 24 hours old,
|
|
46
|
+
* and it has to still be alive. An expiry we do not know about is left alone:
|
|
47
|
+
* a token pasted in from the environment has no recorded expiry, and refreshing
|
|
48
|
+
* something on a guess would burn a call and could not be written anywhere
|
|
49
|
+
* useful anyway.
|
|
50
|
+
*/
|
|
51
|
+
export declare function shouldRefresh(account: Account, windowDays: number, now?: number): boolean;
|
|
52
|
+
/** Turn Meta's `expires_in` seconds into the absolute expiry we store. */
|
|
53
|
+
export declare function expiryFrom(response: TokenResponse, now?: number): number | undefined;
|
|
54
|
+
export declare function refreshWindowOf(config: Config): number;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minting and refreshing Threads tokens.
|
|
3
|
+
*
|
|
4
|
+
* Three token states exist and they are easy to confuse:
|
|
5
|
+
*
|
|
6
|
+
* short-lived 1 hour. What the OAuth code exchange returns.
|
|
7
|
+
* long-lived 60 days. What you get by exchanging a short-lived token,
|
|
8
|
+
* and the only kind worth storing.
|
|
9
|
+
* refreshed 60 days from the refresh. Allowed once the token is at
|
|
10
|
+
* least 24 hours old, and impossible once it has expired.
|
|
11
|
+
*
|
|
12
|
+
* That last sentence is the entire reason this file exists. A token that is
|
|
13
|
+
* never refreshed dies on day 60 and cannot be revived; the person has to walk
|
|
14
|
+
* the whole OAuth flow again. A server that refreshes on a schedule turns a
|
|
15
|
+
* recurring 60-day outage into something nobody has to think about.
|
|
16
|
+
*
|
|
17
|
+
* `maybeRefresh` is called before every request rather than on a timer, because
|
|
18
|
+
* an MCP server is not a daemon. It runs when a client launches it and stops
|
|
19
|
+
* when the client quits, and a timer that fires on day 59 fires in a process
|
|
20
|
+
* that has not existed for weeks.
|
|
21
|
+
*/
|
|
22
|
+
import { AuthenticationError, errorFor } from "../api/errors.js";
|
|
23
|
+
const DAY_MS = 86_400_000;
|
|
24
|
+
async function tokenCall(url, endpoint, timeoutMs) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw errorFor(res.status, endpoint, text);
|
|
32
|
+
const parsed = JSON.parse(text);
|
|
33
|
+
if (!parsed.access_token) {
|
|
34
|
+
throw new AuthenticationError(`${endpoint} returned no access_token.`, res.status, endpoint);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Exchange the short-lived token from the OAuth code flow for a 60-day one.
|
|
44
|
+
*
|
|
45
|
+
* This needs the app secret, which is why `login` needs THREADS_APP_SECRET and
|
|
46
|
+
* pasting a token from Meta's Graph API Explorer does not: that explorer hands
|
|
47
|
+
* out short-lived tokens, and a short-lived token pasted into a config stops
|
|
48
|
+
* working in an hour. `login` is the path that produces something durable.
|
|
49
|
+
*/
|
|
50
|
+
export async function exchangeForLongLived(shortLivedToken, appSecret, host, timeoutMs = 30_000) {
|
|
51
|
+
const url = new URL(`${host}/access_token`);
|
|
52
|
+
url.searchParams.set("grant_type", "th_exchange_token");
|
|
53
|
+
url.searchParams.set("client_secret", appSecret);
|
|
54
|
+
url.searchParams.set("access_token", shortLivedToken);
|
|
55
|
+
return tokenCall(url.toString(), "/access_token", timeoutMs);
|
|
56
|
+
}
|
|
57
|
+
/** Extend a long-lived token by another 60 days. No app secret required. */
|
|
58
|
+
export async function refreshLongLived(token, host, timeoutMs = 30_000) {
|
|
59
|
+
const url = new URL(`${host}/refresh_access_token`);
|
|
60
|
+
url.searchParams.set("grant_type", "th_refresh_token");
|
|
61
|
+
url.searchParams.set("access_token", token);
|
|
62
|
+
return tokenCall(url.toString(), "/refresh_access_token", timeoutMs);
|
|
63
|
+
}
|
|
64
|
+
/** Days until this token expires, or undefined when the expiry is unknown. */
|
|
65
|
+
export function daysRemaining(account, now = Date.now()) {
|
|
66
|
+
if (!account.expiresAt)
|
|
67
|
+
return undefined;
|
|
68
|
+
return Math.floor((account.expiresAt - now) / DAY_MS);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether this token should be refreshed now.
|
|
72
|
+
*
|
|
73
|
+
* Two conditions, both from Meta's rules. It has to be at least 24 hours old,
|
|
74
|
+
* and it has to still be alive. An expiry we do not know about is left alone:
|
|
75
|
+
* a token pasted in from the environment has no recorded expiry, and refreshing
|
|
76
|
+
* something on a guess would burn a call and could not be written anywhere
|
|
77
|
+
* useful anyway.
|
|
78
|
+
*/
|
|
79
|
+
export function shouldRefresh(account, windowDays, now = Date.now()) {
|
|
80
|
+
const remaining = daysRemaining(account, now);
|
|
81
|
+
if (remaining === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (remaining <= 0)
|
|
84
|
+
return false;
|
|
85
|
+
if (account.expiresAt && now < account.expiresAt - 59 * DAY_MS)
|
|
86
|
+
return false;
|
|
87
|
+
return remaining <= windowDays;
|
|
88
|
+
}
|
|
89
|
+
/** Turn Meta's `expires_in` seconds into the absolute expiry we store. */
|
|
90
|
+
export function expiryFrom(response, now = Date.now()) {
|
|
91
|
+
return typeof response.expires_in === "number" ? now + response.expires_in * 1000 : undefined;
|
|
92
|
+
}
|
|
93
|
+
export function refreshWindowOf(config) {
|
|
94
|
+
return config.refreshWindowDays;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* `register()` in tools/kit.ts turns a `ToolSpec` into an MCP tool. This turns
|
|
5
|
+
* the same spec into a shell command, from the same `ALL_TOOLS` array, through
|
|
6
|
+
* the same handler and the same `WriteGuard`. Nothing is described twice, so a
|
|
7
|
+
* tool added tomorrow is a command tomorrow and the two surfaces cannot drift.
|
|
8
|
+
*
|
|
9
|
+
* The command IS the tool name. `create_post` runs as `create-post`, and the
|
|
10
|
+
* underscore form works too. Inventing a prettier command tree would mean a
|
|
11
|
+
* hand-written mapping, which is exactly the drift this avoids, and it would
|
|
12
|
+
* force anyone reading the SKILL.md to learn two vocabularies for one action.
|
|
13
|
+
*
|
|
14
|
+
* Zod is the only schema: every flag, its placeholder, its help text and its
|
|
15
|
+
* validation come from the shape the MCP tool already declares.
|
|
16
|
+
*/
|
|
17
|
+
import { type ZodRawShape } from "zod";
|
|
18
|
+
/** How a value reaches the parser, once the Zod wrappers are peeled off. */
|
|
19
|
+
type FlagKind = "string" | "number" | "boolean" | "enum" | "json";
|
|
20
|
+
type Flag = {
|
|
21
|
+
/** The schema key, e.g. `reply_control`. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** The long flag, e.g. `--reply-control`. */
|
|
24
|
+
flag: string;
|
|
25
|
+
kind: FlagKind;
|
|
26
|
+
required: boolean;
|
|
27
|
+
repeatable: boolean;
|
|
28
|
+
choices?: string[];
|
|
29
|
+
help: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function flagsFor(shape: ZodRawShape): Flag[];
|
|
32
|
+
/**
|
|
33
|
+
* Parse argv against a tool's flags.
|
|
34
|
+
*
|
|
35
|
+
* Zod does the real validation afterwards, so this only has to get the values
|
|
36
|
+
* into the right JavaScript types and catch the mistakes Zod would report in
|
|
37
|
+
* terms of a schema the person at the terminal never sees.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseArgs(argv: string[], flags: Flag[]): Record<string, unknown>;
|
|
40
|
+
/** Exit codes, so a script can branch without parsing the message. */
|
|
41
|
+
export declare const EXIT: {
|
|
42
|
+
readonly ok: 0;
|
|
43
|
+
readonly usage: 2;
|
|
44
|
+
readonly notFound: 3;
|
|
45
|
+
readonly auth: 4;
|
|
46
|
+
readonly api: 5;
|
|
47
|
+
readonly rateLimited: 7;
|
|
48
|
+
readonly config: 10;
|
|
49
|
+
};
|
|
50
|
+
/** Map a thrown error onto one of those, from the shape the API gave back. */
|
|
51
|
+
export declare function exitCodeFor(error: unknown): number;
|
|
52
|
+
/**
|
|
53
|
+
* `--select id,post.title` keeps only the named fields. Dotted paths descend,
|
|
54
|
+
* arrays are traversed element-wise. This is what makes a long feed affordable.
|
|
55
|
+
*/
|
|
56
|
+
export declare function selectFields(data: unknown, paths: string[]): unknown;
|
|
57
|
+
export declare function isCliCommand(argv: string[]): boolean;
|
|
58
|
+
export declare function runCli(argv: string[]): Promise<number>;
|
|
59
|
+
export {};
|