@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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media rules, checked before a container is created.
|
|
3
|
+
*
|
|
4
|
+
* Threads does not upload bytes. You hand it a public URL and it fetches the
|
|
5
|
+
* file itself, asynchronously, and reports the result minutes later as a
|
|
6
|
+
* container in state ERROR with a message like "Media download failed". By
|
|
7
|
+
* then the useful context is gone.
|
|
8
|
+
*
|
|
9
|
+
* So the checks that can be made locally are made locally, in the one place the
|
|
10
|
+
* numbers are written down. Everything here comes from Meta's published specs.
|
|
11
|
+
*/
|
|
12
|
+
export const IMAGE_SPEC = {
|
|
13
|
+
formats: ["jpeg", "jpg", "png"],
|
|
14
|
+
maxBytes: 8 * 1024 * 1024,
|
|
15
|
+
minWidth: 320,
|
|
16
|
+
maxWidth: 1440,
|
|
17
|
+
/** Widest and tallest Threads will accept, as width:height. */
|
|
18
|
+
maxAspect: 10,
|
|
19
|
+
};
|
|
20
|
+
export const VIDEO_SPEC = {
|
|
21
|
+
formats: ["mp4", "mov"],
|
|
22
|
+
maxBytes: 1024 * 1024 * 1024,
|
|
23
|
+
maxSeconds: 300,
|
|
24
|
+
codecs: ["h264", "hevc"],
|
|
25
|
+
recommendedAspect: "9:16",
|
|
26
|
+
};
|
|
27
|
+
export const CAROUSEL = { min: 2, max: 20 };
|
|
28
|
+
/**
|
|
29
|
+
* Reject a media URL that cannot possibly work, before spending a container on
|
|
30
|
+
* it.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately narrow. It refuses what is certainly wrong (not a URL, not
|
|
33
|
+
* HTTPS, a local path, a `data:` URI) and lets everything else through, because
|
|
34
|
+
* a URL with no file extension is completely normal for a CDN and refusing it
|
|
35
|
+
* would break more than it fixed.
|
|
36
|
+
*/
|
|
37
|
+
export function assertMediaUrl(url, kind) {
|
|
38
|
+
const value = url.trim();
|
|
39
|
+
if (!value)
|
|
40
|
+
throw new Error(`A ${kind} URL is required.`);
|
|
41
|
+
if (value.startsWith("data:")) {
|
|
42
|
+
throw new Error(`Threads cannot take a data: URI. It fetches media from a public URL itself, so the ${kind} has to be hosted somewhere it can reach.`);
|
|
43
|
+
}
|
|
44
|
+
if (/^(file:|\/|\.\/|~\/|[A-Za-z]:\\)/.test(value)) {
|
|
45
|
+
throw new Error(`"${value}" is a local path. Threads has no upload endpoint: it fetches media from a public URL, so upload the ${kind} somewhere reachable first and pass that URL.`);
|
|
46
|
+
}
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = new URL(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error(`"${value}" is not a valid URL.`);
|
|
53
|
+
}
|
|
54
|
+
if (parsed.protocol !== "https:") {
|
|
55
|
+
if (parsed.protocol === "http:") {
|
|
56
|
+
throw new Error(`Threads requires HTTPS for media URLs. "${value}" is plain HTTP.`);
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Media URLs must be HTTPS. "${value}" uses ${parsed.protocol}`);
|
|
59
|
+
}
|
|
60
|
+
if (/^(localhost|127\.|0\.0\.0\.0|\[::1\]|192\.168\.|10\.)/i.test(parsed.hostname)) {
|
|
61
|
+
throw new Error(`"${parsed.hostname}" is not reachable from Meta's servers. Threads fetches the ${kind} itself, so a URL that only works on your machine will fail as a container error several minutes later.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The extension, lowercased, when the URL has one. */
|
|
65
|
+
export function extensionOf(url) {
|
|
66
|
+
try {
|
|
67
|
+
const path = new URL(url).pathname;
|
|
68
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(path);
|
|
69
|
+
return match?.[1]?.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Warn about an extension Threads does not support.
|
|
77
|
+
*
|
|
78
|
+
* A warning rather than an error: the extension in a URL is a hint, not a
|
|
79
|
+
* content type, and plenty of perfectly good CDN URLs end in `.webp?format=jpg`
|
|
80
|
+
* or nothing at all. Being told up front that a `.webp` is likely to fail is
|
|
81
|
+
* worth more than being stopped by it.
|
|
82
|
+
*/
|
|
83
|
+
export function formatWarning(url, kind) {
|
|
84
|
+
const ext = extensionOf(url);
|
|
85
|
+
if (!ext)
|
|
86
|
+
return undefined;
|
|
87
|
+
const allowed = kind === "image" ? IMAGE_SPEC.formats : VIDEO_SPEC.formats;
|
|
88
|
+
if (allowed.includes(ext))
|
|
89
|
+
return undefined;
|
|
90
|
+
return `The URL ends in .${ext}. Threads accepts ${allowed.join(", ")} for ${kind}s; anything else usually comes back as a container error a few minutes after posting.`;
|
|
91
|
+
}
|
|
92
|
+
/** Which container type a set of arguments implies. */
|
|
93
|
+
export function mediaTypeFor(item) {
|
|
94
|
+
if (item.video_url)
|
|
95
|
+
return "VIDEO";
|
|
96
|
+
if (item.image_url)
|
|
97
|
+
return "IMAGE";
|
|
98
|
+
return "TEXT";
|
|
99
|
+
}
|
|
100
|
+
/** Validate one carousel item or post attachment. Returns any warnings. */
|
|
101
|
+
export function checkMedia(item) {
|
|
102
|
+
const warnings = [];
|
|
103
|
+
if (item.image_url && item.video_url) {
|
|
104
|
+
throw new Error("A Threads post carries an image or a video, not both. For several pieces of media use create_carousel.");
|
|
105
|
+
}
|
|
106
|
+
if (item.image_url) {
|
|
107
|
+
assertMediaUrl(item.image_url, "image");
|
|
108
|
+
const warning = formatWarning(item.image_url, "image");
|
|
109
|
+
if (warning)
|
|
110
|
+
warnings.push(warning);
|
|
111
|
+
}
|
|
112
|
+
if (item.video_url) {
|
|
113
|
+
assertMediaUrl(item.video_url, "video");
|
|
114
|
+
const warning = formatWarning(item.video_url, "video");
|
|
115
|
+
if (warning)
|
|
116
|
+
warnings.push(warning);
|
|
117
|
+
}
|
|
118
|
+
return warnings;
|
|
119
|
+
}
|
|
120
|
+
export function assertCarouselSize(count) {
|
|
121
|
+
if (count < CAROUSEL.min || count > CAROUSEL.max) {
|
|
122
|
+
throw new Error(`A Threads carousel holds ${CAROUSEL.min} to ${CAROUSEL.max} items. You passed ${count}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/content/media.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measuring a Threads post, and escaping text for the output format.
|
|
3
|
+
*
|
|
4
|
+
* Threads caps a post at 500 characters, and Meta's own documentation adds that
|
|
5
|
+
* emoji "count as UTF-8 bytes". Those are two different limits, and neither is
|
|
6
|
+
* JavaScript's `String.length`:
|
|
7
|
+
*
|
|
8
|
+
* "👨👩👧👦" 1 character to a reader, 11 UTF-16 code units to `.length`,
|
|
9
|
+
* 25 bytes in UTF-8.
|
|
10
|
+
* "é" 1 character, but 1 or 2 code units depending on whether it
|
|
11
|
+
* arrived composed or decomposed, and 2 or 3 bytes.
|
|
12
|
+
*
|
|
13
|
+
* So a post of 480 emoji passes a naive `.length < 500` check and is refused by
|
|
14
|
+
* Threads, and a post of 300 family emoji fails a naive byte check that Threads
|
|
15
|
+
* would have accepted. Both limits are measured here, separately, and the error
|
|
16
|
+
* says which one was crossed and by how much. That matters most for
|
|
17
|
+
* `create_thread`, which checks every part before it posts any of them: a
|
|
18
|
+
* thread that half-publishes and then fails on part four cannot be un-posted.
|
|
19
|
+
*
|
|
20
|
+
* Graphemes are counted with `Intl.Segmenter`, which is in every Node 20
|
|
21
|
+
* runtime, so a flag or a family emoji counts as the one character a person
|
|
22
|
+
* sees rather than the several code points it is made of.
|
|
23
|
+
*/
|
|
24
|
+
/** Threads' published limit for post and reply text. */
|
|
25
|
+
export declare const MAX_POST_CHARS = 500;
|
|
26
|
+
/**
|
|
27
|
+
* The byte ceiling. Meta documents the character limit and notes that emoji are
|
|
28
|
+
* counted as UTF-8 bytes, without publishing a separate byte number. 500 is
|
|
29
|
+
* used for both, which is the conservative reading: a post inside both limits
|
|
30
|
+
* is accepted under either interpretation.
|
|
31
|
+
*/
|
|
32
|
+
export declare const MAX_POST_BYTES = 500;
|
|
33
|
+
/** Threads' limit on a topic tag. */
|
|
34
|
+
export declare const MAX_TOPIC_TAG_CHARS = 50;
|
|
35
|
+
/** Threads accepts at most this many distinct URLs in one post's text. */
|
|
36
|
+
export declare const MAX_LINKS_PER_POST = 5;
|
|
37
|
+
/** Count what a reader would call characters, not UTF-16 code units. */
|
|
38
|
+
export declare function graphemeLength(text: string): number;
|
|
39
|
+
/** UTF-8 byte length, which is what Meta counts emoji against. */
|
|
40
|
+
export declare function byteLength(text: string): number;
|
|
41
|
+
export type Measurement = {
|
|
42
|
+
graphemes: number;
|
|
43
|
+
bytes: number;
|
|
44
|
+
/** True when the text is within both limits. */
|
|
45
|
+
ok: boolean;
|
|
46
|
+
/** Which limit was crossed, when one was. */
|
|
47
|
+
over?: "characters" | "bytes";
|
|
48
|
+
};
|
|
49
|
+
export declare function measure(text: string): Measurement;
|
|
50
|
+
/**
|
|
51
|
+
* A one-line explanation of why a piece of text will not post.
|
|
52
|
+
*
|
|
53
|
+
* `label` names the part, so a thread failure says "part 4 of 6" rather than
|
|
54
|
+
* making the reader work out which string was the problem.
|
|
55
|
+
*/
|
|
56
|
+
export declare function overLimitMessage(text: string, label?: string): string | undefined;
|
|
57
|
+
/** Count distinct URLs in post text, against Threads' five-link ceiling. */
|
|
58
|
+
export declare function countLinks(text: string): number;
|
|
59
|
+
/** Escape for an XML attribute or text node in the output format. */
|
|
60
|
+
export declare function escapeXml(value: unknown): string;
|
|
61
|
+
/**
|
|
62
|
+
* Validate a topic tag.
|
|
63
|
+
*
|
|
64
|
+
* Threads allows one per post, 1 to 50 characters, and rejects periods and
|
|
65
|
+
* ampersands. It is also written without a leading `#`, which is the mistake
|
|
66
|
+
* everyone makes once, so a leading hash is stripped rather than refused.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeTopicTag(raw: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measuring a Threads post, and escaping text for the output format.
|
|
3
|
+
*
|
|
4
|
+
* Threads caps a post at 500 characters, and Meta's own documentation adds that
|
|
5
|
+
* emoji "count as UTF-8 bytes". Those are two different limits, and neither is
|
|
6
|
+
* JavaScript's `String.length`:
|
|
7
|
+
*
|
|
8
|
+
* "👨👩👧👦" 1 character to a reader, 11 UTF-16 code units to `.length`,
|
|
9
|
+
* 25 bytes in UTF-8.
|
|
10
|
+
* "é" 1 character, but 1 or 2 code units depending on whether it
|
|
11
|
+
* arrived composed or decomposed, and 2 or 3 bytes.
|
|
12
|
+
*
|
|
13
|
+
* So a post of 480 emoji passes a naive `.length < 500` check and is refused by
|
|
14
|
+
* Threads, and a post of 300 family emoji fails a naive byte check that Threads
|
|
15
|
+
* would have accepted. Both limits are measured here, separately, and the error
|
|
16
|
+
* says which one was crossed and by how much. That matters most for
|
|
17
|
+
* `create_thread`, which checks every part before it posts any of them: a
|
|
18
|
+
* thread that half-publishes and then fails on part four cannot be un-posted.
|
|
19
|
+
*
|
|
20
|
+
* Graphemes are counted with `Intl.Segmenter`, which is in every Node 20
|
|
21
|
+
* runtime, so a flag or a family emoji counts as the one character a person
|
|
22
|
+
* sees rather than the several code points it is made of.
|
|
23
|
+
*/
|
|
24
|
+
/** Threads' published limit for post and reply text. */
|
|
25
|
+
export const MAX_POST_CHARS = 500;
|
|
26
|
+
/**
|
|
27
|
+
* The byte ceiling. Meta documents the character limit and notes that emoji are
|
|
28
|
+
* counted as UTF-8 bytes, without publishing a separate byte number. 500 is
|
|
29
|
+
* used for both, which is the conservative reading: a post inside both limits
|
|
30
|
+
* is accepted under either interpretation.
|
|
31
|
+
*/
|
|
32
|
+
export const MAX_POST_BYTES = 500;
|
|
33
|
+
/** Threads' limit on a topic tag. */
|
|
34
|
+
export const MAX_TOPIC_TAG_CHARS = 50;
|
|
35
|
+
/** Threads accepts at most this many distinct URLs in one post's text. */
|
|
36
|
+
export const MAX_LINKS_PER_POST = 5;
|
|
37
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
38
|
+
/** Count what a reader would call characters, not UTF-16 code units. */
|
|
39
|
+
export function graphemeLength(text) {
|
|
40
|
+
let count = 0;
|
|
41
|
+
for (const _ of segmenter.segment(text))
|
|
42
|
+
count++;
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
/** UTF-8 byte length, which is what Meta counts emoji against. */
|
|
46
|
+
export function byteLength(text) {
|
|
47
|
+
return Buffer.byteLength(text, "utf8");
|
|
48
|
+
}
|
|
49
|
+
export function measure(text) {
|
|
50
|
+
const graphemes = graphemeLength(text);
|
|
51
|
+
const bytes = byteLength(text);
|
|
52
|
+
if (graphemes > MAX_POST_CHARS)
|
|
53
|
+
return { graphemes, bytes, ok: false, over: "characters" };
|
|
54
|
+
if (bytes > MAX_POST_BYTES)
|
|
55
|
+
return { graphemes, bytes, ok: false, over: "bytes" };
|
|
56
|
+
return { graphemes, bytes, ok: true };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A one-line explanation of why a piece of text will not post.
|
|
60
|
+
*
|
|
61
|
+
* `label` names the part, so a thread failure says "part 4 of 6" rather than
|
|
62
|
+
* making the reader work out which string was the problem.
|
|
63
|
+
*/
|
|
64
|
+
export function overLimitMessage(text, label = "This post") {
|
|
65
|
+
const m = measure(text);
|
|
66
|
+
if (m.ok)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (m.over === "characters") {
|
|
69
|
+
return `${label} is ${m.graphemes} characters. Threads allows ${MAX_POST_CHARS}. Trim ${m.graphemes - MAX_POST_CHARS}, or split it across a thread.`;
|
|
70
|
+
}
|
|
71
|
+
return `${label} is ${m.bytes} UTF-8 bytes (${m.graphemes} characters). Threads allows ${MAX_POST_BYTES} bytes, and counts emoji as bytes rather than characters, so an emoji-heavy post runs out of room before it looks full. Remove about ${m.bytes - MAX_POST_BYTES} bytes.`;
|
|
72
|
+
}
|
|
73
|
+
/** Count distinct URLs in post text, against Threads' five-link ceiling. */
|
|
74
|
+
export function countLinks(text) {
|
|
75
|
+
const matches = text.match(/https?:\/\/[^\s<>"']+/gi) ?? [];
|
|
76
|
+
return new Set(matches.map((u) => u.replace(/[.,;:!?)\]]+$/, ""))).size;
|
|
77
|
+
}
|
|
78
|
+
/** Escape for an XML attribute or text node in the output format. */
|
|
79
|
+
export function escapeXml(value) {
|
|
80
|
+
return String(value)
|
|
81
|
+
.replace(/&/g, "&")
|
|
82
|
+
.replace(/</g, "<")
|
|
83
|
+
.replace(/>/g, ">")
|
|
84
|
+
.replace(/"/g, """)
|
|
85
|
+
.replace(/'/g, "'");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate a topic tag.
|
|
89
|
+
*
|
|
90
|
+
* Threads allows one per post, 1 to 50 characters, and rejects periods and
|
|
91
|
+
* ampersands. It is also written without a leading `#`, which is the mistake
|
|
92
|
+
* everyone makes once, so a leading hash is stripped rather than refused.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeTopicTag(raw) {
|
|
95
|
+
const tag = raw.trim().replace(/^#/, "");
|
|
96
|
+
if (!tag)
|
|
97
|
+
throw new Error("A topic tag cannot be empty.");
|
|
98
|
+
if (tag.length > MAX_TOPIC_TAG_CHARS) {
|
|
99
|
+
throw new Error(`Topic tag is ${tag.length} characters. Threads allows ${MAX_TOPIC_TAG_CHARS}.`);
|
|
100
|
+
}
|
|
101
|
+
if (/[.&]/.test(tag)) {
|
|
102
|
+
throw new Error(`Topic tag "${tag}" contains a period or an ampersand. Threads rejects both.`);
|
|
103
|
+
}
|
|
104
|
+
return tag;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/content/text.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `threads-mcp doctor`: find out what is actually wrong.
|
|
3
|
+
*
|
|
4
|
+
* Threads fails in ways that look identical from a tool call. A missing scope
|
|
5
|
+
* usually returns an empty result rather than an error. An expired token and a
|
|
6
|
+
* token that was never long-lived both read as "rejected". A profile with 99
|
|
7
|
+
* followers gets nothing back from demographics and no explanation.
|
|
8
|
+
*
|
|
9
|
+
* So this probes each capability separately with the cheapest call that proves
|
|
10
|
+
* it, and reports what is granted, what is missing, and what to do about each.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runDoctor(): Promise<number>;
|
|
13
|
+
/** `threads-mcp refresh`: extend every stored token now. */
|
|
14
|
+
export declare function runRefresh(): Promise<number>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `threads-mcp doctor`: find out what is actually wrong.
|
|
3
|
+
*
|
|
4
|
+
* Threads fails in ways that look identical from a tool call. A missing scope
|
|
5
|
+
* usually returns an empty result rather than an error. An expired token and a
|
|
6
|
+
* token that was never long-lived both read as "rejected". A profile with 99
|
|
7
|
+
* followers gets nothing back from demographics and no explanation.
|
|
8
|
+
*
|
|
9
|
+
* So this probes each capability separately with the cheapest call that proves
|
|
10
|
+
* it, and reports what is granted, what is missing, and what to do about each.
|
|
11
|
+
*/
|
|
12
|
+
import { accountsFromStore } from "./auth/store.js";
|
|
13
|
+
import { ThreadsClient } from "./api/client.js";
|
|
14
|
+
import { loadConfig } from "./config.js";
|
|
15
|
+
import { daysRemaining } from "./auth/tokens.js";
|
|
16
|
+
import { ThreadsError } from "./api/errors.js";
|
|
17
|
+
const PASS = " ok ";
|
|
18
|
+
const FAIL = " fail ";
|
|
19
|
+
const WARN = " warn ";
|
|
20
|
+
function line(check) {
|
|
21
|
+
const mark = check.ok ? PASS : check.detail.startsWith("not granted") ? WARN : FAIL;
|
|
22
|
+
const fix = check.fix && !check.ok ? `\n ${check.fix}` : "";
|
|
23
|
+
return `[${mark}] ${check.name}: ${check.detail}${fix}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Probe one capability.
|
|
27
|
+
*
|
|
28
|
+
* A permission failure is reported as a warning rather than an error: an app
|
|
29
|
+
* that never asked for keyword search is not broken, it just cannot search.
|
|
30
|
+
*/
|
|
31
|
+
async function probe(name, fix, run) {
|
|
32
|
+
try {
|
|
33
|
+
return { name, ok: true, detail: await run() };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof ThreadsError && error.name === "PermissionError") {
|
|
37
|
+
return { name, ok: false, detail: "not granted", fix };
|
|
38
|
+
}
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return { name, ok: false, detail: message.slice(0, 160), fix };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function checkAccount(client, account, config) {
|
|
44
|
+
const label = account.username ? `@${account.username}` : (account.userId ?? "unresolved");
|
|
45
|
+
const lines = [`\n${label} (credential from ${account.source})`];
|
|
46
|
+
let userId;
|
|
47
|
+
try {
|
|
48
|
+
const profile = await client.profile(account);
|
|
49
|
+
userId = profile.id;
|
|
50
|
+
lines.push(line({
|
|
51
|
+
name: "token",
|
|
52
|
+
ok: true,
|
|
53
|
+
detail: `valid, acting as @${profile.username ?? profile.id}${profile.is_verified ? ", verified" : ""}`,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
lines.push(line({
|
|
58
|
+
name: "token",
|
|
59
|
+
ok: false,
|
|
60
|
+
detail: error instanceof Error ? error.message.slice(0, 200) : String(error),
|
|
61
|
+
fix: "Run `threads-mcp login` to authorise again. An expired Threads token cannot be refreshed, only replaced.",
|
|
62
|
+
}));
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
const days = daysRemaining(account);
|
|
66
|
+
if (days === undefined) {
|
|
67
|
+
lines.push(line({
|
|
68
|
+
name: "expiry",
|
|
69
|
+
ok: true,
|
|
70
|
+
detail: "unknown — this token came from the environment, so its expiry was never recorded",
|
|
71
|
+
fix: "Run `threads-mcp login` so the server owns the token and can refresh it before it lapses.",
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
else if (days <= 7) {
|
|
75
|
+
lines.push(line({
|
|
76
|
+
name: "expiry",
|
|
77
|
+
ok: false,
|
|
78
|
+
detail: `${days} days left`,
|
|
79
|
+
fix: "Run `threads-mcp refresh` now. A token that lapses cannot be recovered.",
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push(line({ name: "expiry", ok: true, detail: `${days} days left` }));
|
|
84
|
+
}
|
|
85
|
+
const checks = await Promise.all([
|
|
86
|
+
probe("read own posts", "threads_basic is missing, which is unusual — re-authorise.", async () => {
|
|
87
|
+
const response = (await client.call(account, `/${userId}/threads`, { params: { fields: "id", limit: 1 } }));
|
|
88
|
+
return `${response.data?.length ?? 0} post(s) readable`;
|
|
89
|
+
}),
|
|
90
|
+
probe("publishing quota", "Add threads_content_publish and re-authorise.", async () => {
|
|
91
|
+
const response = (await client.call(account, `/${userId}/threads_publishing_limit`, {
|
|
92
|
+
params: { fields: "quota_usage,reply_quota_usage" },
|
|
93
|
+
}));
|
|
94
|
+
const row = response.data?.[0] ?? {};
|
|
95
|
+
return `${row.quota_usage ?? 0}/250 posts and ${row.reply_quota_usage ?? 0}/1000 replies used today`;
|
|
96
|
+
}),
|
|
97
|
+
probe("replies", "Add threads_read_replies and re-authorise.", async () => {
|
|
98
|
+
const response = (await client.call(account, `/${userId}/replies`, { params: { fields: "id", limit: 1 } }));
|
|
99
|
+
return `readable (${response.data?.length ?? 0} recent)`;
|
|
100
|
+
}),
|
|
101
|
+
probe("insights", "Add threads_manage_insights and re-authorise.", async () => {
|
|
102
|
+
const response = (await client.call(account, `/${userId}/threads_insights`, {
|
|
103
|
+
params: { metric: "followers_count" },
|
|
104
|
+
}));
|
|
105
|
+
const followers = response.data?.[0]?.total_value?.value;
|
|
106
|
+
return followers === undefined ? "readable" : `${followers} followers`;
|
|
107
|
+
}),
|
|
108
|
+
probe("keyword search", "Needs threads_keyword_search, which requires App Review. Without it, searches quietly return only your own posts.", async () => {
|
|
109
|
+
const response = (await client.call(account, "/keyword_search", {
|
|
110
|
+
params: { q: "threads", search_type: "TOP", fields: "id,username", limit: 2 },
|
|
111
|
+
}));
|
|
112
|
+
const rows = response.data ?? [];
|
|
113
|
+
const mine = account.username;
|
|
114
|
+
const onlyMine = rows.length > 0 && rows.every((r) => r.username?.toLowerCase() === mine);
|
|
115
|
+
return onlyMine ? "granted, but every result was your own — likely unapproved" : `${rows.length} result(s)`;
|
|
116
|
+
}),
|
|
117
|
+
probe("profile discovery", "Needs threads_profile_discovery and expanded access. Without it, only Meta's own accounts resolve.", async () => {
|
|
118
|
+
const response = (await client.call(account, "/profile_lookup", {
|
|
119
|
+
params: { username: "threads", fields: "id,username" },
|
|
120
|
+
}));
|
|
121
|
+
return response.username ? `resolved @${response.username}` : "reachable";
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
lines.push(...checks.map(line));
|
|
125
|
+
const geo = (await client.profile(account)).is_eligible_for_geo_gating;
|
|
126
|
+
lines.push(line({
|
|
127
|
+
name: "geo-gating",
|
|
128
|
+
ok: geo === true,
|
|
129
|
+
detail: geo === true ? "eligible" : "not enabled for this profile",
|
|
130
|
+
fix: "Meta enables geo-gating per profile. There is no way to request it through the API.",
|
|
131
|
+
}));
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
export async function runDoctor() {
|
|
135
|
+
const stored = accountsFromStore(loadConfig().storePath);
|
|
136
|
+
const config = loadConfig(stored);
|
|
137
|
+
const out = (text) => process.stdout.write(`${text}\n`);
|
|
138
|
+
out("threads-mcp doctor");
|
|
139
|
+
out("");
|
|
140
|
+
// Network first. Every check below is meaningless if this fails, and the
|
|
141
|
+
// errors it produces would all blame the token instead.
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(`${config.graphHost}/v1.0/me`, { method: "GET" });
|
|
144
|
+
out(line({ name: "network", ok: true, detail: `${config.graphHost} reachable (HTTP ${res.status} without a token, as expected)` }));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
out(line({
|
|
148
|
+
name: "network",
|
|
149
|
+
ok: false,
|
|
150
|
+
detail: `cannot reach ${config.graphHost}: ${error.message}`,
|
|
151
|
+
fix: "Check the connection, a proxy, or THREADS_GRAPH_HOST.",
|
|
152
|
+
}));
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
out(line({
|
|
156
|
+
name: "app credentials",
|
|
157
|
+
ok: Boolean(config.appId && config.appSecret),
|
|
158
|
+
detail: config.appId && config.appSecret ? "THREADS_APP_ID and THREADS_APP_SECRET are set" : "not set",
|
|
159
|
+
fix: "Only `threads-mcp login` needs these. Refreshing an existing token does not.",
|
|
160
|
+
}));
|
|
161
|
+
out(line({
|
|
162
|
+
name: "token store",
|
|
163
|
+
ok: true,
|
|
164
|
+
detail: `${stored.length} token(s) at ${config.storePath}`,
|
|
165
|
+
}));
|
|
166
|
+
if (config.readOnly)
|
|
167
|
+
out(`[${WARN}] mode: THREADS_READ_ONLY=1, every write is hidden from the tool list`);
|
|
168
|
+
if (!config.allowDestructive)
|
|
169
|
+
out(`[${WARN}] mode: THREADS_ALLOW_DESTRUCTIVE=0, posting and deleting are blocked`);
|
|
170
|
+
if (!config.accounts.length) {
|
|
171
|
+
out("");
|
|
172
|
+
out("No Threads profile is connected.");
|
|
173
|
+
out(" Run `threads-mcp login`, or set THREADS_ACCESS_TOKEN to a long-lived token.");
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const client = new ThreadsClient(config);
|
|
177
|
+
let failed = false;
|
|
178
|
+
for (const account of config.accounts) {
|
|
179
|
+
const lines = await checkAccount(client, account, config);
|
|
180
|
+
for (const l of lines)
|
|
181
|
+
out(l);
|
|
182
|
+
if (lines.some((l) => l.includes(FAIL)))
|
|
183
|
+
failed = true;
|
|
184
|
+
}
|
|
185
|
+
out("");
|
|
186
|
+
out(failed ? "Something above needs fixing." : "Everything checks out.");
|
|
187
|
+
return failed ? 1 : 0;
|
|
188
|
+
}
|
|
189
|
+
/** `threads-mcp refresh`: extend every stored token now. */
|
|
190
|
+
export async function runRefresh() {
|
|
191
|
+
const stored = accountsFromStore(loadConfig().storePath);
|
|
192
|
+
const config = loadConfig(stored);
|
|
193
|
+
const out = (text) => process.stdout.write(`${text}\n`);
|
|
194
|
+
if (!config.accounts.length) {
|
|
195
|
+
out("No Threads profile is connected. Run `threads-mcp login`.");
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const client = new ThreadsClient(config);
|
|
199
|
+
let failed = false;
|
|
200
|
+
for (const account of config.accounts) {
|
|
201
|
+
const label = account.username ? `@${account.username}` : (account.userId ?? "unresolved");
|
|
202
|
+
if (account.source !== "store") {
|
|
203
|
+
out(`[${WARN}] ${label}: token came from the environment, so a refreshed value cannot be saved anywhere.`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const before = daysRemaining(account);
|
|
207
|
+
const ok = await client.refresh(account);
|
|
208
|
+
if (ok) {
|
|
209
|
+
out(`[${PASS}] ${label}: ${before ?? "?"} → ${daysRemaining(account) ?? "?"} days`);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
failed = true;
|
|
213
|
+
out(`[${FAIL}] ${label}: refresh refused. A token must be at least 24 hours old and not yet expired. Run \`threads-mcp login\`.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return failed ? 1 : 0;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering posts for a model to read.
|
|
3
|
+
*
|
|
4
|
+
* The Threads Graph API answers with an object per post whose useful content is
|
|
5
|
+
* a `text` field surrounded by ids, media URLs, permalinks, paging cursors and
|
|
6
|
+
* a `quoted_post` that repeats the whole shape one level down. Handed straight
|
|
7
|
+
* to a model, a 50-post listing is mostly punctuation, and the model spends its
|
|
8
|
+
* attention finding the text rather than reading it.
|
|
9
|
+
*
|
|
10
|
+
* The tagged format below runs roughly a tenth the size and puts the text where
|
|
11
|
+
* a model expects it. Rules that matter:
|
|
12
|
+
*
|
|
13
|
+
* - **Timestamps are ISO-8601 UTC.** Threads returns an offset format
|
|
14
|
+
* (`2026-08-31T09:14:02+0000`) that `new Date()` parses but that two
|
|
15
|
+
* different posts can express differently. Normalized, so they compare.
|
|
16
|
+
* - **Every attribute is escaped.** A display name containing a quote must
|
|
17
|
+
* not be able to produce malformed output, or worse, close a tag.
|
|
18
|
+
* - **One renderer.** Posts, replies, quoted posts and search results all go
|
|
19
|
+
* through `renderPost`, so their handling cannot drift apart.
|
|
20
|
+
* - **Hidden and deleted replies render as themselves** rather than
|
|
21
|
+
* vanishing, so a gap in a conversation is visible instead of implied.
|
|
22
|
+
* - **`quoted_post` and `reposted_post` are nested, not flattened.** A repost
|
|
23
|
+
* with no text of its own is otherwise indistinguishable from an empty post.
|
|
24
|
+
*/
|
|
25
|
+
type Any = Record<string, any>;
|
|
26
|
+
export declare function renderPost(post: Any, depth?: number, tag?: string): string;
|
|
27
|
+
export type ListOptions = {
|
|
28
|
+
/** What produced this list: "posts", "replies", "search", "conversation". */
|
|
29
|
+
source: string;
|
|
30
|
+
cursor?: string;
|
|
31
|
+
/** Extra attributes on the root element, e.g. the account or query. */
|
|
32
|
+
meta?: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
export declare function renderList(posts: Any[], options: ListOptions, tag?: string): string;
|
|
35
|
+
/** A profile, for whoami and lookup_profile. */
|
|
36
|
+
export declare function renderProfile(profile: Any, extra?: Record<string, unknown>): string;
|
|
37
|
+
/** Pull the paging cursor out of a Graph API listing. */
|
|
38
|
+
export declare function cursorOf(response: Any): string | undefined;
|
|
39
|
+
/** The `data` array of a Graph API listing, whatever shape it arrived in. */
|
|
40
|
+
export declare function dataOf(response: Any): Any[];
|
|
41
|
+
export {};
|