@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1019 -0
  3. package/SKILL.md +203 -0
  4. package/dist/api/client.d.ts +105 -0
  5. package/dist/api/client.js +305 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/errors.d.ts +92 -0
  8. package/dist/api/errors.js +195 -0
  9. package/dist/api/errors.js.map +1 -0
  10. package/dist/api/identity.d.ts +33 -0
  11. package/dist/api/identity.js +52 -0
  12. package/dist/api/identity.js.map +1 -0
  13. package/dist/auth/login.d.ts +32 -0
  14. package/dist/auth/login.js +204 -0
  15. package/dist/auth/login.js.map +1 -0
  16. package/dist/auth/store.d.ts +37 -0
  17. package/dist/auth/store.js +88 -0
  18. package/dist/auth/store.js.map +1 -0
  19. package/dist/auth/tokens.d.ts +54 -0
  20. package/dist/auth/tokens.js +96 -0
  21. package/dist/auth/tokens.js.map +1 -0
  22. package/dist/cli.d.ts +59 -0
  23. package/dist/cli.js +444 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/config.d.ts +98 -0
  26. package/dist/config.js +185 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/content/containers.d.ts +89 -0
  29. package/dist/content/containers.js +210 -0
  30. package/dist/content/containers.js.map +1 -0
  31. package/dist/content/media.d.ts +61 -0
  32. package/dist/content/media.js +125 -0
  33. package/dist/content/media.js.map +1 -0
  34. package/dist/content/text.d.ts +68 -0
  35. package/dist/content/text.js +106 -0
  36. package/dist/content/text.js.map +1 -0
  37. package/dist/doctor.d.ts +14 -0
  38. package/dist/doctor.js +218 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/format/posts.d.ts +41 -0
  41. package/dist/format/posts.js +153 -0
  42. package/dist/format/posts.js.map +1 -0
  43. package/dist/index.d.ts +13 -0
  44. package/dist/index.js +167 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/safety.d.ts +52 -0
  47. package/dist/safety.js +85 -0
  48. package/dist/safety.js.map +1 -0
  49. package/dist/server.d.ts +20 -0
  50. package/dist/server.js +232 -0
  51. package/dist/server.js.map +1 -0
  52. package/dist/tools/accounts.d.ts +27 -0
  53. package/dist/tools/accounts.js +162 -0
  54. package/dist/tools/accounts.js.map +1 -0
  55. package/dist/tools/discover.d.ts +56 -0
  56. package/dist/tools/discover.js +146 -0
  57. package/dist/tools/discover.js.map +1 -0
  58. package/dist/tools/index.d.ts +3 -0
  59. package/dist/tools/index.js +16 -0
  60. package/dist/tools/index.js.map +1 -0
  61. package/dist/tools/insights.d.ts +55 -0
  62. package/dist/tools/insights.js +223 -0
  63. package/dist/tools/insights.js.map +1 -0
  64. package/dist/tools/kit.d.ts +90 -0
  65. package/dist/tools/kit.js +119 -0
  66. package/dist/tools/kit.js.map +1 -0
  67. package/dist/tools/posts.d.ts +170 -0
  68. package/dist/tools/posts.js +312 -0
  69. package/dist/tools/posts.js.map +1 -0
  70. package/dist/tools/read.d.ts +31 -0
  71. package/dist/tools/read.js +95 -0
  72. package/dist/tools/read.js.map +1 -0
  73. package/dist/tools/replies.d.ts +92 -0
  74. package/dist/tools/replies.js +218 -0
  75. package/dist/tools/replies.js.map +1 -0
  76. package/dist/transport/http.d.ts +28 -0
  77. package/dist/transport/http.js +103 -0
  78. package/dist/transport/http.js.map +1 -0
  79. package/package.json +65 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Typed errors for every way a Threads API call can fail.
3
+ *
4
+ * Meta returns a consistent envelope, and the interesting part is not the HTTP
5
+ * status:
6
+ *
7
+ * {"error":{"message":"...","type":"OAuthException","code":190,
8
+ * "error_subcode":463,"fbtrace_id":"..."}}
9
+ *
10
+ * `code` and `error_subcode` are the fields that actually distinguish "your
11
+ * token expired" (190/463) from "you never had this permission" (190/458) from
12
+ * "you are posting too fast" (4 or 32), and all three arrive as HTTP 400. Throw
13
+ * that away and hand a model a bare string, and a model that could have
14
+ * refreshed a token or waited out a quota instead just gives up.
15
+ *
16
+ * Every error here keeps the code, the subcode and the endpoint, and carries a
17
+ * message naming the actual fix, including which OAuth scope was missing when
18
+ * that is what happened.
19
+ */
20
+ export declare class ThreadsError extends Error {
21
+ readonly status: number;
22
+ readonly endpoint: string;
23
+ /** Meta's numeric error code. 0 when absent. */
24
+ readonly code: number;
25
+ readonly subcode: number;
26
+ /** Meta's error type, e.g. "OAuthException". */
27
+ readonly type: string;
28
+ readonly detail: string;
29
+ /** Meta's trace id. The only useful thing to quote in a bug report. */
30
+ readonly traceId: string;
31
+ constructor(message: string, status: number, endpoint: string, parts?: Partial<{
32
+ code: number;
33
+ subcode: number;
34
+ type: string;
35
+ detail: string;
36
+ traceId: string;
37
+ }>);
38
+ toJSON(): Record<string, unknown>;
39
+ }
40
+ /** The token is expired, revoked, or was never valid. A refresh may fix it. */
41
+ export declare class AuthenticationError extends ThreadsError {
42
+ }
43
+ /** Authenticated, but the app lacks the scope, or App Review has not granted it. */
44
+ export declare class PermissionError extends ThreadsError {
45
+ }
46
+ /** Bad arguments: a container that will not build, an unreachable media URL. */
47
+ export declare class ValidationError extends ThreadsError {
48
+ }
49
+ /** The post, container or profile does not exist, or is not visible to you. */
50
+ export declare class NotFoundError extends ThreadsError {
51
+ }
52
+ /** A published or reply quota is exhausted for the rolling 24-hour window. */
53
+ export declare class RateLimitError extends ThreadsError {
54
+ }
55
+ /** 5xx. Upstream, usually transient, worth retrying. */
56
+ export declare class ServerError extends ThreadsError {
57
+ }
58
+ /** Nothing arrived before our own deadline. */
59
+ export declare class TimeoutError extends ThreadsError {
60
+ }
61
+ /** A media container failed to process, expired, or never finished. */
62
+ export declare class ContainerError extends ThreadsError {
63
+ }
64
+ /** Writes are disabled, or a destructive tool was called without `confirm`. */
65
+ export declare class WriteBlockedError extends ThreadsError {
66
+ constructor(message: string);
67
+ }
68
+ /** A post is longer than Threads will accept. Raised before anything is sent. */
69
+ export declare class TextTooLongError extends ThreadsError {
70
+ constructor(message: string);
71
+ }
72
+ export type MetaErrorBody = {
73
+ code: number;
74
+ subcode: number;
75
+ type: string;
76
+ message: string;
77
+ traceId: string;
78
+ };
79
+ /**
80
+ * Pull Meta's error envelope out of a response body.
81
+ *
82
+ * Falls back to raw text capped at 500 characters, so an HTML error page from
83
+ * a load balancer in front of the Graph API does not become the whole message.
84
+ */
85
+ export declare function parseErrorBody(body: string): MetaErrorBody;
86
+ /** True when this failure means a token refresh is worth attempting. */
87
+ export declare function isRefreshable(code: number, subcode: number): boolean;
88
+ /** True when the token is dead and only a fresh authorisation will help. */
89
+ export declare function isUnrecoverableToken(code: number, subcode: number): boolean;
90
+ export declare function scopeFor(endpoint: string): string | undefined;
91
+ /** Map a status plus Meta's error code onto the right class. */
92
+ export declare function errorFor(status: number, endpoint: string, body: string): ThreadsError;
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Typed errors for every way a Threads API call can fail.
3
+ *
4
+ * Meta returns a consistent envelope, and the interesting part is not the HTTP
5
+ * status:
6
+ *
7
+ * {"error":{"message":"...","type":"OAuthException","code":190,
8
+ * "error_subcode":463,"fbtrace_id":"..."}}
9
+ *
10
+ * `code` and `error_subcode` are the fields that actually distinguish "your
11
+ * token expired" (190/463) from "you never had this permission" (190/458) from
12
+ * "you are posting too fast" (4 or 32), and all three arrive as HTTP 400. Throw
13
+ * that away and hand a model a bare string, and a model that could have
14
+ * refreshed a token or waited out a quota instead just gives up.
15
+ *
16
+ * Every error here keeps the code, the subcode and the endpoint, and carries a
17
+ * message naming the actual fix, including which OAuth scope was missing when
18
+ * that is what happened.
19
+ */
20
+ export class ThreadsError extends Error {
21
+ status;
22
+ endpoint;
23
+ /** Meta's numeric error code. 0 when absent. */
24
+ code;
25
+ subcode;
26
+ /** Meta's error type, e.g. "OAuthException". */
27
+ type;
28
+ detail;
29
+ /** Meta's trace id. The only useful thing to quote in a bug report. */
30
+ traceId;
31
+ constructor(message, status, endpoint, parts = {}) {
32
+ super(message);
33
+ this.name = new.target.name;
34
+ this.status = status;
35
+ this.endpoint = endpoint;
36
+ this.code = parts.code ?? 0;
37
+ this.subcode = parts.subcode ?? 0;
38
+ this.type = parts.type ?? "";
39
+ this.detail = parts.detail ?? "";
40
+ this.traceId = parts.traceId ?? "";
41
+ }
42
+ toJSON() {
43
+ return {
44
+ error: this.message,
45
+ type: this.name,
46
+ status: this.status,
47
+ endpoint: this.endpoint,
48
+ ...(this.code ? { code: this.code } : {}),
49
+ ...(this.subcode ? { subcode: this.subcode } : {}),
50
+ ...(this.type ? { meta_type: this.type } : {}),
51
+ ...(this.detail ? { detail: this.detail } : {}),
52
+ ...(this.traceId ? { trace_id: this.traceId } : {}),
53
+ };
54
+ }
55
+ }
56
+ /** The token is expired, revoked, or was never valid. A refresh may fix it. */
57
+ export class AuthenticationError extends ThreadsError {
58
+ }
59
+ /** Authenticated, but the app lacks the scope, or App Review has not granted it. */
60
+ export class PermissionError extends ThreadsError {
61
+ }
62
+ /** Bad arguments: a container that will not build, an unreachable media URL. */
63
+ export class ValidationError extends ThreadsError {
64
+ }
65
+ /** The post, container or profile does not exist, or is not visible to you. */
66
+ export class NotFoundError extends ThreadsError {
67
+ }
68
+ /** A published or reply quota is exhausted for the rolling 24-hour window. */
69
+ export class RateLimitError extends ThreadsError {
70
+ }
71
+ /** 5xx. Upstream, usually transient, worth retrying. */
72
+ export class ServerError extends ThreadsError {
73
+ }
74
+ /** Nothing arrived before our own deadline. */
75
+ export class TimeoutError extends ThreadsError {
76
+ }
77
+ /** A media container failed to process, expired, or never finished. */
78
+ export class ContainerError extends ThreadsError {
79
+ }
80
+ /** Writes are disabled, or a destructive tool was called without `confirm`. */
81
+ export class WriteBlockedError extends ThreadsError {
82
+ constructor(message) {
83
+ super(message, 0, "(local)", { type: "WriteBlocked" });
84
+ }
85
+ }
86
+ /** A post is longer than Threads will accept. Raised before anything is sent. */
87
+ export class TextTooLongError extends ThreadsError {
88
+ constructor(message) {
89
+ super(message, 0, "(local)", { type: "TextTooLong" });
90
+ }
91
+ }
92
+ /**
93
+ * Pull Meta's error envelope out of a response body.
94
+ *
95
+ * Falls back to raw text capped at 500 characters, so an HTML error page from
96
+ * a load balancer in front of the Graph API does not become the whole message.
97
+ */
98
+ export function parseErrorBody(body) {
99
+ const empty = { code: 0, subcode: 0, type: "", message: "", traceId: "" };
100
+ const text = body.trim();
101
+ if (!text)
102
+ return empty;
103
+ try {
104
+ const parsed = JSON.parse(text);
105
+ if (parsed && typeof parsed === "object") {
106
+ const err = parsed.error;
107
+ if (err && typeof err === "object") {
108
+ const e = err;
109
+ return {
110
+ code: typeof e.code === "number" ? e.code : 0,
111
+ subcode: typeof e.error_subcode === "number" ? e.error_subcode : 0,
112
+ type: typeof e.type === "string" ? e.type : "",
113
+ message: typeof e.message === "string" ? e.message.slice(0, 500) : "",
114
+ traceId: typeof e.fbtrace_id === "string" ? e.fbtrace_id : "",
115
+ };
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // Not JSON. Fall through to the raw text.
121
+ }
122
+ return { ...empty, message: text.replace(/\s+/g, " ").slice(0, 500) };
123
+ }
124
+ /**
125
+ * Meta's OAuth error codes, as far as they are documented and observed.
126
+ *
127
+ * 190 is every token problem at once, and the subcode is what separates them.
128
+ * 463 means expired, which a refresh fixes. 467 means invalidated, which it
129
+ * does not: the person has to authorise again.
130
+ */
131
+ const OAUTH_CODE = 190;
132
+ const SUBCODE_EXPIRED = 463;
133
+ const SUBCODE_INVALIDATED = 467;
134
+ const SUBCODE_CHANGED_PASSWORD = 460;
135
+ /** True when this failure means a token refresh is worth attempting. */
136
+ export function isRefreshable(code, subcode) {
137
+ return code === OAUTH_CODE && subcode === SUBCODE_EXPIRED;
138
+ }
139
+ /** True when the token is dead and only a fresh authorisation will help. */
140
+ export function isUnrecoverableToken(code, subcode) {
141
+ return code === OAUTH_CODE && (subcode === SUBCODE_INVALIDATED || subcode === SUBCODE_CHANGED_PASSWORD);
142
+ }
143
+ /**
144
+ * The scope each endpoint needs, so a permission failure can name it.
145
+ *
146
+ * Threads splits its permissions far more finely than most APIs, and the error
147
+ * Meta returns for a missing one does not say which is missing. Being told
148
+ * "add threads_manage_insights" is the difference between a two-minute fix in
149
+ * the App Dashboard and an afternoon of guessing.
150
+ */
151
+ const SCOPE_FOR = [
152
+ [/threads_publish|\/threads$/, "threads_content_publish"],
153
+ [/manage_reply|pending_replies/, "threads_manage_replies"],
154
+ [/\/replies|\/conversation/, "threads_read_replies"],
155
+ [/insights/, "threads_manage_insights"],
156
+ [/keyword_search/, "threads_keyword_search"],
157
+ [/profile_lookup/, "threads_profile_discovery"],
158
+ [/location_search/, "threads_location_tagging"],
159
+ ];
160
+ export function scopeFor(endpoint) {
161
+ for (const [pattern, scope] of SCOPE_FOR) {
162
+ if (pattern.test(endpoint))
163
+ return scope;
164
+ }
165
+ return undefined;
166
+ }
167
+ /** Map a status plus Meta's error code onto the right class. */
168
+ export function errorFor(status, endpoint, body) {
169
+ const { code, subcode, type, message, traceId } = parseErrorBody(body);
170
+ const parts = { code, subcode, type, detail: message, traceId };
171
+ // Meta signals a spent quota with codes 4, 17 and 32 rather than HTTP 429.
172
+ if (status === 429 || code === 4 || code === 17 || code === 32 || code === 613) {
173
+ return new RateLimitError(`Threads rate limited ${endpoint}. Posting is capped at 250 posts and 1,000 replies per rolling 24 hours, and deletes at 100. Call get_publishing_limit to see what is left.`, status, endpoint, parts);
174
+ }
175
+ if (code === OAUTH_CODE || status === 401) {
176
+ if (isUnrecoverableToken(code, subcode)) {
177
+ return new AuthenticationError(`The Threads token was invalidated and cannot be refreshed. Run \`threads-mcp login\` to authorise again.`, status, endpoint, parts);
178
+ }
179
+ return new AuthenticationError(`Threads rejected the token for ${endpoint}. A long-lived token lasts 60 days and dies permanently if it is never refreshed. Try \`threads-mcp refresh\`, then \`threads-mcp login\` if that fails.`, status, endpoint, parts);
180
+ }
181
+ if (status === 403 || code === 10 || (code >= 200 && code <= 299)) {
182
+ const scope = scopeFor(endpoint);
183
+ return new PermissionError(scope
184
+ ? `Threads refused ${endpoint}. This endpoint needs the \`${scope}\` scope, granted both when the profile authorises the app and by App Review for anyone other than a tester on your own app.`
185
+ : `Threads refused ${endpoint} for this profile. This is usually a missing OAuth scope or an App Review permission that has not been granted.`, status, endpoint, parts);
186
+ }
187
+ if (status === 404 || code === 803) {
188
+ return new NotFoundError(`Not found via ${endpoint}. Check the post or container id. A deleted post, a container that expired after 24 hours, and an id that never existed all look the same here.`, status, endpoint, parts);
189
+ }
190
+ if (status >= 500) {
191
+ return new ServerError(`Threads returned ${status} for ${endpoint}. This is upstream and usually transient.`, status, endpoint, parts);
192
+ }
193
+ return new ValidationError(`Threads rejected the request to ${endpoint}${code ? ` (code ${code}${subcode ? `/${subcode}` : ""})` : ""}.`, status, endpoint, parts);
194
+ }
195
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/api/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Post ids, container ids, and the two hostnames Threads answers on.
3
+ *
4
+ * A Threads post has a numeric media id, which is what every endpoint wants,
5
+ * and a permalink containing a shortcode, which is what a person copies out of
6
+ * the app. They are not the same string and there is no public endpoint that
7
+ * converts a shortcode back into an id.
8
+ *
9
+ * So: anywhere a tool takes a post, it accepts a numeric id, or a permalink
10
+ * whose numeric id we can read. A permalink that carries only a shortcode is
11
+ * refused with an explanation rather than being silently sent to the API as a
12
+ * malformed id, which fails several layers later with a message about nothing.
13
+ */
14
+ export type PostRef = {
15
+ /** The numeric media id, when it could be determined. */
16
+ id?: string;
17
+ /** The permalink shortcode, when the input was a URL. */
18
+ shortcode?: string;
19
+ username?: string;
20
+ };
21
+ /** True for a bare numeric Threads media or container id. */
22
+ export declare function isMediaId(value: string): boolean;
23
+ export declare function parsePostRef(raw: string): PostRef;
24
+ /**
25
+ * Resolve whatever the caller passed into the numeric id the API needs.
26
+ *
27
+ * Threads publishes no shortcode-to-id endpoint, so a permalink alone cannot be
28
+ * resolved. Saying that plainly is better than sending the shortcode and
29
+ * letting Meta return a generic "Unsupported get request".
30
+ */
31
+ export declare function requireMediaId(raw: string): string;
32
+ /** Best-effort permalink, for display when the API did not return one. */
33
+ export declare function webUrl(username: string | undefined, shortcode: string | undefined): string | undefined;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Post ids, container ids, and the two hostnames Threads answers on.
3
+ *
4
+ * A Threads post has a numeric media id, which is what every endpoint wants,
5
+ * and a permalink containing a shortcode, which is what a person copies out of
6
+ * the app. They are not the same string and there is no public endpoint that
7
+ * converts a shortcode back into an id.
8
+ *
9
+ * So: anywhere a tool takes a post, it accepts a numeric id, or a permalink
10
+ * whose numeric id we can read. A permalink that carries only a shortcode is
11
+ * refused with an explanation rather than being silently sent to the API as a
12
+ * malformed id, which fails several layers later with a message about nothing.
13
+ */
14
+ /** Threads moved to threads.com; threads.net still resolves. Both are accepted. */
15
+ const PERMALINK = /^https?:\/\/(?:www\.)?threads\.(?:net|com)\/(?:@([\w.]+)\/)?post\/([A-Za-z0-9_-]+)/i;
16
+ /** True for a bare numeric Threads media or container id. */
17
+ export function isMediaId(value) {
18
+ return /^\d{5,}$/.test(value.trim());
19
+ }
20
+ export function parsePostRef(raw) {
21
+ const value = raw.trim();
22
+ if (isMediaId(value))
23
+ return { id: value };
24
+ const match = PERMALINK.exec(value);
25
+ if (match) {
26
+ return { shortcode: match[2], username: match[1]?.toLowerCase() };
27
+ }
28
+ return {};
29
+ }
30
+ /**
31
+ * Resolve whatever the caller passed into the numeric id the API needs.
32
+ *
33
+ * Threads publishes no shortcode-to-id endpoint, so a permalink alone cannot be
34
+ * resolved. Saying that plainly is better than sending the shortcode and
35
+ * letting Meta return a generic "Unsupported get request".
36
+ */
37
+ export function requireMediaId(raw) {
38
+ const ref = parsePostRef(raw);
39
+ if (ref.id)
40
+ return ref.id;
41
+ if (ref.shortcode) {
42
+ throw new Error(`"${raw}" is a Threads permalink. Threads has no public endpoint that converts a permalink shortcode back into the numeric post id the API needs. Use the numeric id, which get_posts returns for every post alongside its permalink.`);
43
+ }
44
+ throw new Error(`"${raw}" is not a Threads post id. Pass the numeric id, which get_posts and get_replies return on every result.`);
45
+ }
46
+ /** Best-effort permalink, for display when the API did not return one. */
47
+ export function webUrl(username, shortcode) {
48
+ if (!username || !shortcode)
49
+ return undefined;
50
+ return `https://www.threads.com/@${username}/post/${shortcode}`;
51
+ }
52
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/api/identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,mFAAmF;AACnF,MAAM,SAAS,GAAG,qFAAqF,CAAC;AAUxG,6DAA6D;AAC7D,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACzB,IAAI,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;IAE3C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,GAAG,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC,EAAE,CAAC;IAC1B,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,IAAI,GAAG,+NAA+N,CACvO,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,KAAK,CACb,IAAI,GAAG,0GAA0G,CAClH,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,MAAM,CAAC,QAA4B,EAAE,SAA6B;IAChF,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,4BAA4B,QAAQ,SAAS,SAAS,EAAE,CAAC;AAClE,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `threads-mcp login`: the whole OAuth flow, on the command line.
3
+ *
4
+ * Threads is not a platform where you can paste a credential and be done. You
5
+ * need a Meta app, and the token that app's Graph Explorer hands out is
6
+ * short-lived, so the usual "paste a token into the config" route produces
7
+ * something that stops working in an hour. That is why so many Threads
8
+ * integrations are described as flaky: they are not flaky, they were set up
9
+ * with the wrong kind of token.
10
+ *
11
+ * This command does the sequence properly: opens the authorisation URL, catches
12
+ * the redirect on a loopback listener, exchanges the code, exchanges the
13
+ * short-lived token for a 60-day one, verifies it against `GET /me`, and writes
14
+ * it to a store the server can refresh on its own afterwards.
15
+ *
16
+ * The loopback redirect binds 127.0.0.1 on a port you choose, and the same URL
17
+ * has to be listed as a valid redirect URI in the app's settings. That is the
18
+ * one manual step nothing can remove.
19
+ */
20
+ /** Every scope this server can use. Anything not granted simply stays unused. */
21
+ export declare const ALL_SCOPES: string[];
22
+ /** The subset that needs nothing from App Review while you are a tester. */
23
+ export declare const CORE_SCOPES: string[];
24
+ export declare function authorizeUrl(appId: string, redirectUri: string, scopes: string[]): string;
25
+ export type LoginOptions = {
26
+ port: number;
27
+ scopes: string[];
28
+ /** Skip the browser and take a short-lived token pasted in by hand. */
29
+ manual: boolean;
30
+ };
31
+ export declare function loginOptionsFrom(argv: string[]): LoginOptions;
32
+ export declare function runLogin(argv?: string[]): Promise<number>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * `threads-mcp login`: the whole OAuth flow, on the command line.
3
+ *
4
+ * Threads is not a platform where you can paste a credential and be done. You
5
+ * need a Meta app, and the token that app's Graph Explorer hands out is
6
+ * short-lived, so the usual "paste a token into the config" route produces
7
+ * something that stops working in an hour. That is why so many Threads
8
+ * integrations are described as flaky: they are not flaky, they were set up
9
+ * with the wrong kind of token.
10
+ *
11
+ * This command does the sequence properly: opens the authorisation URL, catches
12
+ * the redirect on a loopback listener, exchanges the code, exchanges the
13
+ * short-lived token for a 60-day one, verifies it against `GET /me`, and writes
14
+ * it to a store the server can refresh on its own afterwards.
15
+ *
16
+ * The loopback redirect binds 127.0.0.1 on a port you choose, and the same URL
17
+ * has to be listed as a valid redirect URI in the app's settings. That is the
18
+ * one manual step nothing can remove.
19
+ */
20
+ import { createServer } from "node:http";
21
+ import { spawn } from "node:child_process";
22
+ import { createInterface } from "node:readline/promises";
23
+ import { loadConfig, normalizeUsername } from "../config.js";
24
+ import { exchangeForLongLived, expiryFrom } from "./tokens.js";
25
+ import { upsertToken } from "./store.js";
26
+ import { ThreadsClient } from "../api/client.js";
27
+ const AUTH_HOST = "https://threads.net";
28
+ /** Every scope this server can use. Anything not granted simply stays unused. */
29
+ export const ALL_SCOPES = [
30
+ "threads_basic",
31
+ "threads_content_publish",
32
+ "threads_manage_replies",
33
+ "threads_read_replies",
34
+ "threads_manage_insights",
35
+ "threads_delete",
36
+ "threads_keyword_search",
37
+ "threads_profile_discovery",
38
+ "threads_location_tagging",
39
+ ];
40
+ /** The subset that needs nothing from App Review while you are a tester. */
41
+ export const CORE_SCOPES = [
42
+ "threads_basic",
43
+ "threads_content_publish",
44
+ "threads_manage_replies",
45
+ "threads_read_replies",
46
+ "threads_manage_insights",
47
+ "threads_delete",
48
+ ];
49
+ export function authorizeUrl(appId, redirectUri, scopes) {
50
+ const url = new URL(`${AUTH_HOST}/oauth/authorize`);
51
+ url.searchParams.set("client_id", appId);
52
+ url.searchParams.set("redirect_uri", redirectUri);
53
+ url.searchParams.set("scope", scopes.join(","));
54
+ url.searchParams.set("response_type", "code");
55
+ return url.toString();
56
+ }
57
+ function open(url) {
58
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
59
+ try {
60
+ spawn(command, [url], { detached: true, stdio: "ignore", shell: process.platform === "win32" }).unref();
61
+ }
62
+ catch {
63
+ // No browser is fine. The URL is printed either way.
64
+ }
65
+ }
66
+ /** Wait for Meta to redirect back with `?code=`. Resolves with the code. */
67
+ function awaitCode(port, timeoutMs) {
68
+ return new Promise((resolve, reject) => {
69
+ const server = createServer((req, res) => {
70
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
71
+ const code = url.searchParams.get("code");
72
+ const error = url.searchParams.get("error_description") ?? url.searchParams.get("error");
73
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
74
+ res.end(`<!doctype html><meta charset="utf-8"><title>threads-mcp</title>` +
75
+ `<body style="font:16px system-ui;padding:3rem;max-width:34rem">` +
76
+ (code
77
+ ? `<h1>Authorised</h1><p>You can close this tab and go back to the terminal.</p>`
78
+ : `<h1>Authorisation failed</h1><p>${error ?? "No code was returned."}</p>`) +
79
+ `</body>`);
80
+ server.close();
81
+ clearTimeout(timer);
82
+ if (code)
83
+ resolve(code);
84
+ else
85
+ reject(new Error(error ?? "Threads returned no authorisation code."));
86
+ });
87
+ const timer = setTimeout(() => {
88
+ server.close();
89
+ reject(new Error(`No redirect arrived within ${Math.round(timeoutMs / 1000)}s.`));
90
+ }, timeoutMs);
91
+ server.on("error", (error) => {
92
+ clearTimeout(timer);
93
+ reject(new Error(`Could not listen on 127.0.0.1:${port}: ${error.message}. Pass --port to use another one, and add the matching redirect URI to the app.`));
94
+ });
95
+ // Only loopback. This listener briefly holds an authorisation code.
96
+ server.listen(port, "127.0.0.1");
97
+ });
98
+ }
99
+ /** Exchange the authorisation code for a short-lived token. */
100
+ async function exchangeCode(appId, appSecret, redirectUri, code, graphHost) {
101
+ const body = new URLSearchParams({
102
+ client_id: appId,
103
+ client_secret: appSecret,
104
+ grant_type: "authorization_code",
105
+ redirect_uri: redirectUri,
106
+ code,
107
+ });
108
+ const res = await fetch(`${graphHost}/oauth/access_token`, { method: "POST", body });
109
+ const text = await res.text();
110
+ if (!res.ok)
111
+ throw new Error(`Code exchange failed (${res.status}): ${text.slice(0, 300)}`);
112
+ const parsed = JSON.parse(text);
113
+ if (!parsed.access_token)
114
+ throw new Error("Code exchange returned no access_token.");
115
+ return parsed.access_token;
116
+ }
117
+ export function loginOptionsFrom(argv) {
118
+ const portFlag = argv.find((a) => a.startsWith("--port="));
119
+ const port = Number(portFlag?.split("=")[1] ?? 8788);
120
+ return {
121
+ port: Number.isFinite(port) && port > 0 ? port : 8788,
122
+ scopes: argv.includes("--all-scopes") ? ALL_SCOPES : CORE_SCOPES,
123
+ manual: argv.includes("--manual"),
124
+ };
125
+ }
126
+ export async function runLogin(argv = []) {
127
+ const options = loginOptionsFrom(argv);
128
+ const config = loadConfig();
129
+ const out = (line) => process.stdout.write(`${line}\n`);
130
+ if (!config.appId || !config.appSecret) {
131
+ out("threads-mcp login needs a Meta app.");
132
+ out("");
133
+ out(" 1. Create one at developers.facebook.com, add the Threads use case,");
134
+ out(" and copy the Threads App ID and App Secret from its settings.");
135
+ out(` 2. Add http://127.0.0.1:${options.port}/callback as a valid`);
136
+ out(" OAuth redirect URI on that app.");
137
+ out(" 3. Export both and run this again:");
138
+ out("");
139
+ out(" export THREADS_APP_ID=...");
140
+ out(" export THREADS_APP_SECRET=...");
141
+ out(" threads-mcp login");
142
+ out("");
143
+ return 1;
144
+ }
145
+ const redirectUri = `http://127.0.0.1:${options.port}/callback`;
146
+ const url = authorizeUrl(config.appId, redirectUri, options.scopes);
147
+ let shortLived;
148
+ if (options.manual) {
149
+ out("Open this URL, authorise, then paste the short-lived token:");
150
+ out("");
151
+ out(` ${url}`);
152
+ out("");
153
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
154
+ shortLived = (await rl.question("Short-lived token: ")).trim();
155
+ rl.close();
156
+ if (!shortLived) {
157
+ out("Nothing pasted.");
158
+ return 1;
159
+ }
160
+ }
161
+ else {
162
+ out(`Redirect URI: ${redirectUri}`);
163
+ out(`Scopes: ${options.scopes.join(", ")}`);
164
+ out("");
165
+ out("Opening the authorisation page. If nothing opens, use this URL:");
166
+ out("");
167
+ out(` ${url}`);
168
+ out("");
169
+ out("Waiting for the redirect…");
170
+ const waiting = awaitCode(options.port, 300_000);
171
+ open(url);
172
+ const code = await waiting;
173
+ out("Got the code. Exchanging it…");
174
+ shortLived = await exchangeCode(config.appId, config.appSecret, redirectUri, code, config.graphHost);
175
+ }
176
+ out("Exchanging for a 60-day token…");
177
+ const longLived = await exchangeForLongLived(shortLived, config.appSecret, config.graphHost);
178
+ const expiresAt = expiryFrom(longLived);
179
+ // Verify before storing. A token that cannot read /me is not worth keeping,
180
+ // and finding that out now is better than on the first tool call.
181
+ const account = { accessToken: longLived.access_token, source: "store" };
182
+ const client = new ThreadsClient({ ...config, accounts: [account] });
183
+ const profile = await client.profile(account);
184
+ upsertToken(config.storePath, {
185
+ user_id: profile.id,
186
+ username: profile.username ? normalizeUsername(profile.username) : undefined,
187
+ access_token: longLived.access_token,
188
+ expires_at: expiresAt,
189
+ obtained_at: Date.now(),
190
+ scopes: options.scopes,
191
+ });
192
+ const days = expiresAt ? Math.round((expiresAt - Date.now()) / 86_400_000) : 60;
193
+ out("");
194
+ out(`Authorised as @${profile.username ?? profile.id} (${profile.id}).`);
195
+ out(`Token valid for ${days} days, saved to ${config.storePath}`);
196
+ out("");
197
+ out("The server refreshes it automatically while it is running. If nothing runs");
198
+ out("for 60 days the token expires for good, so run `threads-mcp refresh` if you");
199
+ out("are going away, or just leave the MCP client connected.");
200
+ out("");
201
+ out("Check everything with: threads-mcp doctor");
202
+ return 0;
203
+ }
204
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/auth/login.ts"],"names":[],"mappings":"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"}