@cyanheads/stackexchange-mcp-server 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +360 -0
- package/CLAUDE.md +360 -0
- package/Dockerfile +99 -0
- package/LICENSE +201 -0
- package/README.md +307 -0
- package/changelog/0.1.x/0.1.1.md +27 -0
- package/changelog/template.md +127 -0
- package/dist/config/server-config.d.ts +11 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +21 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/index.d.ts +176 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/index.js +22 -0
- package/dist/mcp-server/tools/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.d.ts +37 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.js +118 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-tag-faq.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.d.ts +54 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.js +205 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-thread.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.d.ts +48 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.js +151 -0
- package/dist/mcp-server/tools/definitions/stackexchange-get-user.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.d.ts +20 -0
- package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.js +94 -0
- package/dist/mcp-server/tools/definitions/stackexchange-list-sites.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.d.ts +45 -0
- package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.js +145 -0
- package/dist/mcp-server/tools/definitions/stackexchange-search-questions.tool.js.map +1 -0
- package/dist/services/stackexchange/html-normalizer.d.ts +18 -0
- package/dist/services/stackexchange/html-normalizer.d.ts.map +1 -0
- package/dist/services/stackexchange/html-normalizer.js +143 -0
- package/dist/services/stackexchange/html-normalizer.js.map +1 -0
- package/dist/services/stackexchange/stackexchange-service.d.ts +143 -0
- package/dist/services/stackexchange/stackexchange-service.d.ts.map +1 -0
- package/dist/services/stackexchange/stackexchange-service.js +336 -0
- package/dist/services/stackexchange/stackexchange-service.js.map +1 -0
- package/dist/services/stackexchange/types.d.ts +104 -0
- package/dist/services/stackexchange/types.d.ts.map +1 -0
- package/dist/services/stackexchange/types.js +7 -0
- package/dist/services/stackexchange/types.js.map +1 -0
- package/package.json +101 -0
- package/server.json +111 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lightweight HTML→markdown normalizer for Stack Exchange post bodies.
|
|
3
|
+
* Handles SE's known tag set: p, pre/code, strong, em, a, ul, ol, li, h1-h6,
|
|
4
|
+
* blockquote, inline code. No external dependency required.
|
|
5
|
+
* @module services/stackexchange/html-normalizer
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Convert a Stack Exchange HTML post body to clean markdown.
|
|
9
|
+
* Operates on SE's predictable, limited HTML tag set.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeHtml(html) {
|
|
12
|
+
if (!html)
|
|
13
|
+
return '';
|
|
14
|
+
let md = html;
|
|
15
|
+
// NOTE: do NOT call decodeEntities() here. HTML entities must remain encoded
|
|
16
|
+
// until after all HTML tags are processed. Pre-decoding </> converts
|
|
17
|
+
// them to < / > which (a) breaks stripTags() on attribute values containing >
|
|
18
|
+
// (e.g. alt="x >= 128" gets split mid-tag) and (b) makes entity-encoded
|
|
19
|
+
// angle brackets in code blocks look like real HTML tags that then get stripped.
|
|
20
|
+
// Entities are decoded inside code block handlers explicitly, and by the final
|
|
21
|
+
// decodeEntities() call at the end of this function.
|
|
22
|
+
// Fenced code blocks: <pre><code>...</code></pre> → ```\n...\n```
|
|
23
|
+
// SE wraps code blocks in both tags; capture the language hint from class if present.
|
|
24
|
+
md = md.replace(/<pre[^>]*>\s*<code[^>]*class="[^"]*language-([^"\s]+)[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_, lang, code) => {
|
|
25
|
+
const cleaned = stripTags(code)
|
|
26
|
+
.replace(/&/g, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, "'");
|
|
31
|
+
return `\`\`\`${lang}\n${cleaned.trim()}\n\`\`\``;
|
|
32
|
+
});
|
|
33
|
+
// Fenced code blocks without language class
|
|
34
|
+
md = md.replace(/<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_, code) => {
|
|
35
|
+
const cleaned = stripTags(code)
|
|
36
|
+
.replace(/&/g, '&')
|
|
37
|
+
.replace(/</g, '<')
|
|
38
|
+
.replace(/>/g, '>')
|
|
39
|
+
.replace(/"/g, '"')
|
|
40
|
+
.replace(/'/g, "'");
|
|
41
|
+
return `\`\`\`\n${cleaned.trim()}\n\`\`\``;
|
|
42
|
+
});
|
|
43
|
+
// Headings h1–h6
|
|
44
|
+
md = md.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, content) => {
|
|
45
|
+
const hashes = '#'.repeat(parseInt(level, 10));
|
|
46
|
+
return `\n${hashes} ${stripTags(content).trim()}\n`;
|
|
47
|
+
});
|
|
48
|
+
// Blockquotes
|
|
49
|
+
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
50
|
+
const inner = normalizeHtml(content).trim();
|
|
51
|
+
return inner
|
|
52
|
+
.split('\n')
|
|
53
|
+
.map((line) => `> ${line}`)
|
|
54
|
+
.join('\n');
|
|
55
|
+
});
|
|
56
|
+
// Bold
|
|
57
|
+
md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, (_, content) => `**${stripTags(content).trim()}**`);
|
|
58
|
+
md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, (_, content) => `**${stripTags(content).trim()}**`);
|
|
59
|
+
// Italic
|
|
60
|
+
md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, (_, content) => `_${stripTags(content).trim()}_`);
|
|
61
|
+
md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, (_, content) => `_${stripTags(content).trim()}_`);
|
|
62
|
+
// Inline code (not already inside pre blocks)
|
|
63
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, content) => {
|
|
64
|
+
const raw = content
|
|
65
|
+
.replace(/&/g, '&')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>')
|
|
68
|
+
.replace(/"/g, '"')
|
|
69
|
+
.replace(/'/g, "'");
|
|
70
|
+
return `\`${raw}\``;
|
|
71
|
+
});
|
|
72
|
+
// Links
|
|
73
|
+
md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
|
74
|
+
const linkText = stripTags(text).trim() || href;
|
|
75
|
+
return `[${linkText}](${href})`;
|
|
76
|
+
});
|
|
77
|
+
md = md.replace(/<a[^>]+href='([^']*)'[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
|
78
|
+
const linkText = stripTags(text).trim() || href;
|
|
79
|
+
return `[${linkText}](${href})`;
|
|
80
|
+
});
|
|
81
|
+
// Ordered lists
|
|
82
|
+
md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, (_, content) => {
|
|
83
|
+
let counter = 0;
|
|
84
|
+
const items = content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (__, item) => {
|
|
85
|
+
counter++;
|
|
86
|
+
return `${counter}. ${normalizeHtml(item).trim()}\n`;
|
|
87
|
+
});
|
|
88
|
+
return `\n${items}\n`;
|
|
89
|
+
});
|
|
90
|
+
// Unordered lists
|
|
91
|
+
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, content) => {
|
|
92
|
+
const items = content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (__, item) => `- ${normalizeHtml(item).trim()}\n`);
|
|
93
|
+
return `\n${items}\n`;
|
|
94
|
+
});
|
|
95
|
+
// Paragraphs → double newline
|
|
96
|
+
md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, content) => `\n${normalizeHtml(content).trim()}\n`);
|
|
97
|
+
// Line breaks
|
|
98
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
99
|
+
// Strip any remaining HTML tags (img, hr, sub, sup, table, etc.).
|
|
100
|
+
// Protect already-converted fenced code blocks first — their decoded content
|
|
101
|
+
// (e.g. `#include <algorithm>`) contains angle brackets that would otherwise
|
|
102
|
+
// be misidentified as HTML tags and stripped.
|
|
103
|
+
const codeBlocks = [];
|
|
104
|
+
const PLACEHOLDER_PREFIX = 'MDCODEBLOCK';
|
|
105
|
+
const PLACEHOLDER_SUFFIX = 'ENDMDCODEBLOCK';
|
|
106
|
+
md = md.replace(/```[\s\S]*?```/g, (block) => {
|
|
107
|
+
codeBlocks.push(block);
|
|
108
|
+
return `${PLACEHOLDER_PREFIX}${codeBlocks.length - 1}${PLACEHOLDER_SUFFIX}`;
|
|
109
|
+
});
|
|
110
|
+
md = stripTags(md);
|
|
111
|
+
md = md.replace(new RegExp(`${PLACEHOLDER_PREFIX}(\\d+)${PLACEHOLDER_SUFFIX}`, 'g'), (_, idx) => codeBlocks[parseInt(idx, 10)] ?? '');
|
|
112
|
+
// Decode entities in the non-code portions (remaining &, <, etc.)
|
|
113
|
+
md = decodeEntities(md);
|
|
114
|
+
// Normalize excessive blank lines (max 2 consecutive newlines)
|
|
115
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
116
|
+
return md.trim();
|
|
117
|
+
}
|
|
118
|
+
/** Strip all HTML tags from a string. */
|
|
119
|
+
function stripTags(html) {
|
|
120
|
+
return html.replace(/<[^>]+>/g, '');
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Decode basic HTML entities in a plain-text string (not HTML).
|
|
124
|
+
* Use this for SE API fields like site names and audiences that arrive
|
|
125
|
+
* HTML-encoded but contain no markup.
|
|
126
|
+
*/
|
|
127
|
+
export function decodeHtmlEntities(text) {
|
|
128
|
+
return decodeEntities(text);
|
|
129
|
+
}
|
|
130
|
+
/** Decode basic HTML entities. */
|
|
131
|
+
function decodeEntities(text) {
|
|
132
|
+
return text
|
|
133
|
+
.replace(/&/g, '&')
|
|
134
|
+
.replace(/</g, '<')
|
|
135
|
+
.replace(/>/g, '>')
|
|
136
|
+
.replace(/"/g, '"')
|
|
137
|
+
.replace(/'/g, "'")
|
|
138
|
+
.replace(/'/g, "'")
|
|
139
|
+
.replace(/ /g, ' ')
|
|
140
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
|
|
141
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=html-normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-normalizer.js","sourceRoot":"","sources":["../../../src/services/stackexchange/html-normalizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Stack Exchange API v2.3 HTTP client with backoff tracking,
|
|
3
|
+
* quota logging, gzip decompression, and typed domain methods.
|
|
4
|
+
* @module services/stackexchange/stackexchange-service
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@cyanheads/mcp-ts-core';
|
|
7
|
+
import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
|
|
8
|
+
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
|
|
9
|
+
export interface SearchQuestionsOptions {
|
|
10
|
+
acceptedOnly?: boolean;
|
|
11
|
+
/** API key from server config — injected by the service. */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
minScore?: number;
|
|
14
|
+
pageSize?: number;
|
|
15
|
+
query: string;
|
|
16
|
+
site: string;
|
|
17
|
+
sort?: 'relevance' | 'votes' | 'activity' | 'newest';
|
|
18
|
+
tags?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface GetThreadOptions {
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
includeComments?: boolean;
|
|
23
|
+
maxAnswers?: number;
|
|
24
|
+
questionId: number;
|
|
25
|
+
site: string;
|
|
26
|
+
}
|
|
27
|
+
export interface GetUserOptions {
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
site: string;
|
|
30
|
+
userId: number;
|
|
31
|
+
}
|
|
32
|
+
export interface GetTagFaqOptions {
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
pageSize?: number;
|
|
35
|
+
site: string;
|
|
36
|
+
tag: string;
|
|
37
|
+
}
|
|
38
|
+
export interface GetSitesOptions {
|
|
39
|
+
apiKey?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Normalized question for tool output. */
|
|
42
|
+
export interface NormalizedQuestion {
|
|
43
|
+
answerCount: number;
|
|
44
|
+
excerpt?: string;
|
|
45
|
+
isAnswered: boolean;
|
|
46
|
+
link: string;
|
|
47
|
+
questionId: number;
|
|
48
|
+
score: number;
|
|
49
|
+
tags: string[];
|
|
50
|
+
title: string;
|
|
51
|
+
}
|
|
52
|
+
/** Normalized answer for thread output. */
|
|
53
|
+
export interface NormalizedAnswer {
|
|
54
|
+
answerId: number;
|
|
55
|
+
authorLink?: string;
|
|
56
|
+
authorName?: string;
|
|
57
|
+
authorReputation?: number;
|
|
58
|
+
bodyMarkdown: string;
|
|
59
|
+
isAccepted: boolean;
|
|
60
|
+
score: number;
|
|
61
|
+
}
|
|
62
|
+
/** Normalized thread for tool output. */
|
|
63
|
+
export interface NormalizedThread {
|
|
64
|
+
acceptedAnswerId?: number;
|
|
65
|
+
answers: NormalizedAnswer[];
|
|
66
|
+
authorLink?: string;
|
|
67
|
+
authorName?: string;
|
|
68
|
+
bodyMarkdown: string;
|
|
69
|
+
link: string;
|
|
70
|
+
questionId: number;
|
|
71
|
+
score: number;
|
|
72
|
+
tags: string[];
|
|
73
|
+
title: string;
|
|
74
|
+
}
|
|
75
|
+
/** Normalized user profile for tool output. */
|
|
76
|
+
export interface NormalizedUser {
|
|
77
|
+
answerCount?: number;
|
|
78
|
+
badgeCounts?: {
|
|
79
|
+
gold?: number;
|
|
80
|
+
silver?: number;
|
|
81
|
+
bronze?: number;
|
|
82
|
+
};
|
|
83
|
+
displayName: string;
|
|
84
|
+
link: string;
|
|
85
|
+
location?: string;
|
|
86
|
+
questionCount?: number;
|
|
87
|
+
reputation: number;
|
|
88
|
+
topTags: {
|
|
89
|
+
tagName: string;
|
|
90
|
+
answerCount?: number;
|
|
91
|
+
answerScore?: number;
|
|
92
|
+
}[];
|
|
93
|
+
userId: number;
|
|
94
|
+
websiteUrl?: string;
|
|
95
|
+
}
|
|
96
|
+
/** Normalized site for tool output. */
|
|
97
|
+
export interface NormalizedSite {
|
|
98
|
+
apiSiteParameter: string;
|
|
99
|
+
audience?: string;
|
|
100
|
+
name: string;
|
|
101
|
+
siteUrl: string;
|
|
102
|
+
}
|
|
103
|
+
export declare class StackExchangeService {
|
|
104
|
+
private readonly apiKey;
|
|
105
|
+
constructor(_config: AppConfig, _storage: StorageService, apiKey?: string);
|
|
106
|
+
/** Build a URL with common params (key, gzip). */
|
|
107
|
+
private buildUrl;
|
|
108
|
+
/** Fetch, decompress (auto), parse, handle errors. */
|
|
109
|
+
private fetchSe;
|
|
110
|
+
/** Search questions (no bodies). */
|
|
111
|
+
searchQuestions(opts: SearchQuestionsOptions, ctx: Context): Promise<{
|
|
112
|
+
questions: NormalizedQuestion[];
|
|
113
|
+
quotaRemaining: number;
|
|
114
|
+
quotaMax: number;
|
|
115
|
+
}>;
|
|
116
|
+
/** Fetch a complete Q&A thread with HTML→markdown normalization. */
|
|
117
|
+
getThread(opts: GetThreadOptions, ctx: Context): Promise<{
|
|
118
|
+
thread: NormalizedThread;
|
|
119
|
+
quotaRemaining: number;
|
|
120
|
+
quotaMax: number;
|
|
121
|
+
}>;
|
|
122
|
+
/** Fetch the tag FAQ (highest-voted answered questions for a tag). */
|
|
123
|
+
getTagFaq(opts: GetTagFaqOptions, ctx: Context): Promise<{
|
|
124
|
+
questions: NormalizedQuestion[];
|
|
125
|
+
quotaRemaining: number;
|
|
126
|
+
quotaMax: number;
|
|
127
|
+
}>;
|
|
128
|
+
/** Fetch a user profile + top tags. */
|
|
129
|
+
getUser(opts: GetUserOptions, ctx: Context): Promise<{
|
|
130
|
+
user: NormalizedUser;
|
|
131
|
+
quotaRemaining: number;
|
|
132
|
+
quotaMax: number;
|
|
133
|
+
}>;
|
|
134
|
+
/** Fetch all sites in the SE network (paginated, bounded set). */
|
|
135
|
+
getSites(ctx: Context): Promise<{
|
|
136
|
+
sites: NormalizedSite[];
|
|
137
|
+
quotaRemaining: number;
|
|
138
|
+
quotaMax: number;
|
|
139
|
+
}>;
|
|
140
|
+
}
|
|
141
|
+
export declare function initStackExchangeService(config: AppConfig, storage: StorageService, apiKey?: string): void;
|
|
142
|
+
export declare function getStackExchangeService(): StackExchangeService;
|
|
143
|
+
//# sourceMappingURL=stackexchange-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stackexchange-service.d.ts","sourceRoot":"","sources":["../../../src/services/stackexchange/stackexchange-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Stack Exchange API v2.3 HTTP client with backoff tracking,
|
|
3
|
+
* quota logging, gzip decompression, and typed domain methods.
|
|
4
|
+
* @module services/stackexchange/stackexchange-service
|
|
5
|
+
*/
|
|
6
|
+
import { invalidParams, notFound, rateLimited, serviceUnavailable, } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { withRetry } from '@cyanheads/mcp-ts-core/utils';
|
|
8
|
+
import { decodeHtmlEntities, normalizeHtml } from './html-normalizer.js';
|
|
9
|
+
const BASE_URL = 'https://api.stackexchange.com/2.3';
|
|
10
|
+
/** Module-level backoff tracking — per-process, acceptable for server-side use. */
|
|
11
|
+
let backoffUntil = 0;
|
|
12
|
+
/** Honour the SE `backoff` field before the next request. */
|
|
13
|
+
async function waitForBackoff() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
if (now < backoffUntil) {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, backoffUntil - now));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Update the backoff window from a response envelope. */
|
|
20
|
+
function updateBackoff(wrapper) {
|
|
21
|
+
if (wrapper.backoff && wrapper.backoff > 0) {
|
|
22
|
+
backoffUntil = Date.now() + wrapper.backoff * 1000;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class StackExchangeService {
|
|
26
|
+
apiKey;
|
|
27
|
+
constructor(_config, _storage, apiKey) {
|
|
28
|
+
this.apiKey = apiKey;
|
|
29
|
+
}
|
|
30
|
+
/** Build a URL with common params (key, gzip). */
|
|
31
|
+
buildUrl(path, params) {
|
|
32
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
33
|
+
// SE API always returns gzip — accept it explicitly
|
|
34
|
+
// (fetch auto-decompresses with Accept-Encoding: gzip)
|
|
35
|
+
for (const [k, v] of Object.entries(params)) {
|
|
36
|
+
if (v !== undefined && v !== '' && v !== null) {
|
|
37
|
+
url.searchParams.set(k, String(v));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (this.apiKey) {
|
|
41
|
+
url.searchParams.set('key', this.apiKey);
|
|
42
|
+
}
|
|
43
|
+
return url.toString();
|
|
44
|
+
}
|
|
45
|
+
/** Fetch, decompress (auto), parse, handle errors. */
|
|
46
|
+
async fetchSe(url, ctx) {
|
|
47
|
+
await waitForBackoff();
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
headers: {
|
|
50
|
+
'Accept-Encoding': 'gzip',
|
|
51
|
+
Accept: 'application/json',
|
|
52
|
+
},
|
|
53
|
+
signal: ctx.signal,
|
|
54
|
+
});
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
// SE returns HTTP 400 with JSON error envelope for bad params
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
let errObj;
|
|
59
|
+
try {
|
|
60
|
+
errObj = JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ignore parse failure
|
|
64
|
+
}
|
|
65
|
+
if (errObj?.error_name === 'bad_parameter') {
|
|
66
|
+
throw invalidParams(`Stack Exchange API error: ${errObj.error_message}`, {
|
|
67
|
+
reason: 'invalid_site',
|
|
68
|
+
error_name: errObj.error_name,
|
|
69
|
+
error_id: errObj.error_id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
throw serviceUnavailable(`Stack Exchange API returned HTTP ${response.status}`, {
|
|
73
|
+
status: response.status,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let wrapper;
|
|
77
|
+
try {
|
|
78
|
+
wrapper = JSON.parse(text);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw serviceUnavailable('Failed to parse Stack Exchange response', {}, { cause: err });
|
|
82
|
+
}
|
|
83
|
+
updateBackoff(wrapper);
|
|
84
|
+
ctx.log.debug('SE quota', {
|
|
85
|
+
quota_remaining: wrapper.quota_remaining,
|
|
86
|
+
quota_max: wrapper.quota_max,
|
|
87
|
+
});
|
|
88
|
+
return wrapper;
|
|
89
|
+
}
|
|
90
|
+
/** Search questions (no bodies). */
|
|
91
|
+
searchQuestions(opts, ctx) {
|
|
92
|
+
return withRetry(async () => {
|
|
93
|
+
const params = {
|
|
94
|
+
site: opts.site,
|
|
95
|
+
q: opts.query,
|
|
96
|
+
sort: opts.sort ?? 'relevance',
|
|
97
|
+
pagesize: opts.pageSize ?? 10,
|
|
98
|
+
intitle: undefined,
|
|
99
|
+
};
|
|
100
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
101
|
+
params.tagged = opts.tags.join(';');
|
|
102
|
+
}
|
|
103
|
+
if (opts.acceptedOnly) {
|
|
104
|
+
params.accepted = 'True';
|
|
105
|
+
}
|
|
106
|
+
if (opts.minScore !== undefined) {
|
|
107
|
+
params.min = opts.minScore;
|
|
108
|
+
}
|
|
109
|
+
const url = this.buildUrl('/search/advanced', params);
|
|
110
|
+
const wrapper = await this.fetchSe(url, ctx);
|
|
111
|
+
if (wrapper.quota_remaining === 0) {
|
|
112
|
+
throw rateLimited('Stack Exchange API quota exhausted.', {
|
|
113
|
+
reason: 'quota_exceeded',
|
|
114
|
+
quota_remaining: 0,
|
|
115
|
+
quota_max: wrapper.quota_max,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const questions = wrapper.items.map((q) => ({
|
|
119
|
+
questionId: q.question_id,
|
|
120
|
+
title: q.title,
|
|
121
|
+
link: q.link,
|
|
122
|
+
score: q.score,
|
|
123
|
+
answerCount: q.answer_count,
|
|
124
|
+
isAnswered: q.is_answered,
|
|
125
|
+
tags: q.tags,
|
|
126
|
+
...(q.excerpt ? { excerpt: q.excerpt } : {}),
|
|
127
|
+
}));
|
|
128
|
+
return { questions, quotaRemaining: wrapper.quota_remaining, quotaMax: wrapper.quota_max };
|
|
129
|
+
}, {
|
|
130
|
+
operation: 'searchQuestions',
|
|
131
|
+
context: ctx,
|
|
132
|
+
baseDelayMs: 1000,
|
|
133
|
+
signal: ctx.signal,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Fetch a complete Q&A thread with HTML→markdown normalization. */
|
|
137
|
+
getThread(opts, ctx) {
|
|
138
|
+
return withRetry(async () => {
|
|
139
|
+
const questionUrl = this.buildUrl(`/questions/${opts.questionId}`, {
|
|
140
|
+
site: opts.site,
|
|
141
|
+
filter: 'withbody',
|
|
142
|
+
});
|
|
143
|
+
const answersUrl = this.buildUrl(`/questions/${opts.questionId}/answers`, {
|
|
144
|
+
site: opts.site,
|
|
145
|
+
filter: 'withbody',
|
|
146
|
+
sort: 'votes',
|
|
147
|
+
pagesize: opts.maxAnswers ?? 10,
|
|
148
|
+
});
|
|
149
|
+
const [questionWrapper, answersWrapper] = await Promise.all([
|
|
150
|
+
this.fetchSe(questionUrl, ctx),
|
|
151
|
+
this.fetchSe(answersUrl, ctx),
|
|
152
|
+
]);
|
|
153
|
+
if (questionWrapper.quota_remaining === 0) {
|
|
154
|
+
throw rateLimited('Stack Exchange API quota exhausted.', {
|
|
155
|
+
reason: 'quota_exceeded',
|
|
156
|
+
quota_remaining: 0,
|
|
157
|
+
quota_max: questionWrapper.quota_max,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (questionWrapper.items.length === 0) {
|
|
161
|
+
throw notFound(`Question ID ${opts.questionId} not found on site "${opts.site}".`, {
|
|
162
|
+
reason: 'question_not_found',
|
|
163
|
+
questionId: opts.questionId,
|
|
164
|
+
site: opts.site,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const q = questionWrapper.items[0];
|
|
168
|
+
// Sort answers: accepted first, then by score descending
|
|
169
|
+
const answers = answersWrapper.items.slice().sort((a, b) => {
|
|
170
|
+
const aAccepted = a.answer_id === q.accepted_answer_id ? 1 : 0;
|
|
171
|
+
const bAccepted = b.answer_id === q.accepted_answer_id ? 1 : 0;
|
|
172
|
+
if (aAccepted !== bAccepted)
|
|
173
|
+
return bAccepted - aAccepted;
|
|
174
|
+
return b.score - a.score;
|
|
175
|
+
});
|
|
176
|
+
const normalizedAnswers = answers.map((a) => ({
|
|
177
|
+
answerId: a.answer_id,
|
|
178
|
+
score: a.score,
|
|
179
|
+
isAccepted: a.is_accepted,
|
|
180
|
+
bodyMarkdown: normalizeHtml(a.body ?? ''),
|
|
181
|
+
...(a.owner?.display_name ? { authorName: a.owner.display_name } : {}),
|
|
182
|
+
...(a.owner?.link ? { authorLink: a.owner.link } : {}),
|
|
183
|
+
...(a.owner?.reputation !== undefined ? { authorReputation: a.owner.reputation } : {}),
|
|
184
|
+
}));
|
|
185
|
+
const thread = {
|
|
186
|
+
questionId: q.question_id,
|
|
187
|
+
title: q.title,
|
|
188
|
+
link: q.link,
|
|
189
|
+
score: q.score,
|
|
190
|
+
tags: q.tags,
|
|
191
|
+
bodyMarkdown: normalizeHtml(q.body ?? ''),
|
|
192
|
+
...(q.owner?.display_name ? { authorName: q.owner.display_name } : {}),
|
|
193
|
+
...(q.owner?.link ? { authorLink: q.owner.link } : {}),
|
|
194
|
+
answers: normalizedAnswers,
|
|
195
|
+
...(q.accepted_answer_id !== undefined ? { acceptedAnswerId: q.accepted_answer_id } : {}),
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
thread,
|
|
199
|
+
quotaRemaining: questionWrapper.quota_remaining,
|
|
200
|
+
quotaMax: questionWrapper.quota_max,
|
|
201
|
+
};
|
|
202
|
+
}, {
|
|
203
|
+
operation: 'getThread',
|
|
204
|
+
context: ctx,
|
|
205
|
+
baseDelayMs: 1000,
|
|
206
|
+
signal: ctx.signal,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** Fetch the tag FAQ (highest-voted answered questions for a tag). */
|
|
210
|
+
getTagFaq(opts, ctx) {
|
|
211
|
+
return withRetry(async () => {
|
|
212
|
+
const url = this.buildUrl(`/tags/${encodeURIComponent(opts.tag)}/faq`, {
|
|
213
|
+
site: opts.site,
|
|
214
|
+
pagesize: opts.pageSize ?? 10,
|
|
215
|
+
});
|
|
216
|
+
const wrapper = await this.fetchSe(url, ctx);
|
|
217
|
+
if (wrapper.quota_remaining === 0) {
|
|
218
|
+
throw rateLimited('Stack Exchange API quota exhausted.', {
|
|
219
|
+
reason: 'quota_exceeded',
|
|
220
|
+
quota_remaining: 0,
|
|
221
|
+
quota_max: wrapper.quota_max,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const questions = wrapper.items.map((q) => ({
|
|
225
|
+
questionId: q.question_id,
|
|
226
|
+
title: q.title,
|
|
227
|
+
link: q.link,
|
|
228
|
+
score: q.score,
|
|
229
|
+
answerCount: q.answer_count,
|
|
230
|
+
isAnswered: q.is_answered,
|
|
231
|
+
tags: q.tags,
|
|
232
|
+
}));
|
|
233
|
+
return { questions, quotaRemaining: wrapper.quota_remaining, quotaMax: wrapper.quota_max };
|
|
234
|
+
}, {
|
|
235
|
+
operation: 'getTagFaq',
|
|
236
|
+
context: ctx,
|
|
237
|
+
baseDelayMs: 1000,
|
|
238
|
+
signal: ctx.signal,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Fetch a user profile + top tags. */
|
|
242
|
+
getUser(opts, ctx) {
|
|
243
|
+
return withRetry(async () => {
|
|
244
|
+
const profileUrl = this.buildUrl(`/users/${opts.userId}`, { site: opts.site });
|
|
245
|
+
const topTagsUrl = this.buildUrl(`/users/${opts.userId}/top-tags`, {
|
|
246
|
+
site: opts.site,
|
|
247
|
+
pagesize: 10,
|
|
248
|
+
});
|
|
249
|
+
const [profileWrapper, topTagsWrapper] = await Promise.all([
|
|
250
|
+
this.fetchSe(profileUrl, ctx),
|
|
251
|
+
this.fetchSe(topTagsUrl, ctx),
|
|
252
|
+
]);
|
|
253
|
+
if (profileWrapper.quota_remaining === 0) {
|
|
254
|
+
throw rateLimited('Stack Exchange API quota exhausted.', {
|
|
255
|
+
reason: 'quota_exceeded',
|
|
256
|
+
quota_remaining: 0,
|
|
257
|
+
quota_max: profileWrapper.quota_max,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (profileWrapper.items.length === 0) {
|
|
261
|
+
throw notFound(`User ID ${opts.userId} not found on site "${opts.site}".`, {
|
|
262
|
+
reason: 'user_not_found',
|
|
263
|
+
userId: opts.userId,
|
|
264
|
+
site: opts.site,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const u = profileWrapper.items[0];
|
|
268
|
+
const topTags = topTagsWrapper.items.map((t) => ({
|
|
269
|
+
tagName: t.tag_name,
|
|
270
|
+
...(t.answer_count !== undefined ? { answerCount: t.answer_count } : {}),
|
|
271
|
+
...(t.answer_score !== undefined ? { answerScore: t.answer_score } : {}),
|
|
272
|
+
}));
|
|
273
|
+
const user = {
|
|
274
|
+
userId: u.user_id,
|
|
275
|
+
displayName: u.display_name,
|
|
276
|
+
link: u.link,
|
|
277
|
+
reputation: u.reputation,
|
|
278
|
+
...(u.badge_counts ? { badgeCounts: u.badge_counts } : {}),
|
|
279
|
+
...(u.location ? { location: u.location } : {}),
|
|
280
|
+
...(u.website_url ? { websiteUrl: u.website_url } : {}),
|
|
281
|
+
topTags,
|
|
282
|
+
...(u.answer_count !== undefined ? { answerCount: u.answer_count } : {}),
|
|
283
|
+
...(u.question_count !== undefined ? { questionCount: u.question_count } : {}),
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
user,
|
|
287
|
+
quotaRemaining: profileWrapper.quota_remaining,
|
|
288
|
+
quotaMax: profileWrapper.quota_max,
|
|
289
|
+
};
|
|
290
|
+
}, {
|
|
291
|
+
operation: 'getUser',
|
|
292
|
+
context: ctx,
|
|
293
|
+
baseDelayMs: 1000,
|
|
294
|
+
signal: ctx.signal,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** Fetch all sites in the SE network (paginated, bounded set). */
|
|
298
|
+
getSites(ctx) {
|
|
299
|
+
return withRetry(async () => {
|
|
300
|
+
const url = this.buildUrl('/sites', { pagesize: 100 });
|
|
301
|
+
const wrapper = await this.fetchSe(url, ctx);
|
|
302
|
+
// SE API returns site names and audiences HTML-encoded (e.g. "Unix & Linux")
|
|
303
|
+
const normalizeSite = (s) => ({
|
|
304
|
+
name: decodeHtmlEntities(s.name),
|
|
305
|
+
apiSiteParameter: s.api_site_parameter,
|
|
306
|
+
siteUrl: s.site_url,
|
|
307
|
+
...(s.audience ? { audience: decodeHtmlEntities(s.audience) } : {}),
|
|
308
|
+
});
|
|
309
|
+
const sites = wrapper.items.map(normalizeSite);
|
|
310
|
+
// If there are more pages, fetch them (SE has ~190 sites, fits in 2 pages at pagesize=100)
|
|
311
|
+
if (wrapper.has_more) {
|
|
312
|
+
const page2Url = this.buildUrl('/sites', { pagesize: 100, page: 2 });
|
|
313
|
+
const wrapper2 = await this.fetchSe(page2Url, ctx);
|
|
314
|
+
sites.push(...wrapper2.items.map(normalizeSite));
|
|
315
|
+
}
|
|
316
|
+
return { sites, quotaRemaining: wrapper.quota_remaining, quotaMax: wrapper.quota_max };
|
|
317
|
+
}, {
|
|
318
|
+
operation: 'getSites',
|
|
319
|
+
context: ctx,
|
|
320
|
+
baseDelayMs: 1000,
|
|
321
|
+
signal: ctx.signal,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// --- Init/accessor pattern ---
|
|
326
|
+
let _service;
|
|
327
|
+
export function initStackExchangeService(config, storage, apiKey) {
|
|
328
|
+
_service = new StackExchangeService(config, storage, apiKey);
|
|
329
|
+
}
|
|
330
|
+
export function getStackExchangeService() {
|
|
331
|
+
if (!_service) {
|
|
332
|
+
throw new Error('StackExchangeService not initialized — call initStackExchangeService() in setup()');
|
|
333
|
+
}
|
|
334
|
+
return _service;
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=stackexchange-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stackexchange-service.js","sourceRoot":"","sources":["../../../src/services/stackexchange/stackexchange-service.ts"],"names":[],"mappings":"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"}
|