katman-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/cli.js +71 -0
- package/dist/context.js +43 -0
- package/dist/crawlers.js +86 -0
- package/dist/index.js +25 -0
- package/dist/license.js +448 -0
- package/dist/meta.js +28 -0
- package/dist/net.js +128 -0
- package/dist/server.js +17 -0
- package/dist/state.js +81 -0
- package/dist/tools/account/account.js +64 -0
- package/dist/tools/account/cli.js +21 -0
- package/dist/tools/account/index.js +48 -0
- package/dist/tools/account/sync.js +142 -0
- package/dist/tools/account/upsell.js +290 -0
- package/dist/tools/crawl/audit/analyze.js +719 -0
- package/dist/tools/crawl/audit/bots.js +114 -0
- package/dist/tools/crawl/audit/checks.js +126 -0
- package/dist/tools/crawl/audit/html.js +382 -0
- package/dist/tools/crawl/audit/index.js +364 -0
- package/dist/tools/crawl/audit/model.js +1 -0
- package/dist/tools/crawl/audit/report.js +199 -0
- package/dist/tools/crawl/audit/robots.js +199 -0
- package/dist/tools/crawl/audit/sitemap.js +147 -0
- package/dist/tools/crawl/cli.js +106 -0
- package/dist/tools/crawl/common/advice.js +61 -0
- package/dist/tools/crawl/common/findings.js +57 -0
- package/dist/tools/crawl/common/routes.js +80 -0
- package/dist/tools/crawl/common/text.js +89 -0
- package/dist/tools/crawl/common/urls.js +64 -0
- package/dist/tools/crawl/index.js +226 -0
- package/dist/tools/crawl/indexnow.js +271 -0
- package/dist/tools/crawl/keywords.js +178 -0
- package/dist/tools/crawl/scan/framework.js +645 -0
- package/dist/tools/crawl/scan/index.js +175 -0
- package/dist/tools/crawl/scan/project.js +180 -0
- package/dist/tools/crawl/scan/routes.js +354 -0
- package/dist/tools/crawl/scan/signals.js +414 -0
- package/dist/tools/method/cli.js +42 -0
- package/dist/tools/method/core/brief.js +302 -0
- package/dist/tools/method/core/extract.js +435 -0
- package/dist/tools/method/core/generate.js +504 -0
- package/dist/tools/method/core/lint.js +556 -0
- package/dist/tools/method/core/plan.js +405 -0
- package/dist/tools/method/core/prompt.js +150 -0
- package/dist/tools/method/core/questions.js +196 -0
- package/dist/tools/method/core/report.js +182 -0
- package/dist/tools/method/core/research-run.js +148 -0
- package/dist/tools/method/core/research.js +448 -0
- package/dist/tools/method/core/snippets.js +292 -0
- package/dist/tools/method/core/start.js +277 -0
- package/dist/tools/method/data/anatomy.js +141 -0
- package/dist/tools/method/data/appendices.js +202 -0
- package/dist/tools/method/data/cloudflare.js +192 -0
- package/dist/tools/method/data/evidence.js +137 -0
- package/dist/tools/method/data/geo-jobs.js +128 -0
- package/dist/tools/method/data/index.js +19 -0
- package/dist/tools/method/data/landing.js +67 -0
- package/dist/tools/method/data/lessons.js +127 -0
- package/dist/tools/method/data/measurement.js +142 -0
- package/dist/tools/method/data/method.js +141 -0
- package/dist/tools/method/data/offsite.js +148 -0
- package/dist/tools/method/data/pages.js +167 -0
- package/dist/tools/method/data/prompts.js +633 -0
- package/dist/tools/method/data/publish.js +113 -0
- package/dist/tools/method/data/render.js +117 -0
- package/dist/tools/method/data/research.js +131 -0
- package/dist/tools/method/data/roadmap.js +105 -0
- package/dist/tools/method/data/traps.js +74 -0
- package/dist/tools/method/data/verify.js +88 -0
- package/dist/tools/method/data/viral-list.js +39 -0
- package/dist/tools/method/index.js +209 -0
- package/dist/tools/method/lib/calls.js +53 -0
- package/dist/tools/method/lib/framework.js +525 -0
- package/dist/tools/method/lib/l10n.js +17 -0
- package/dist/tools/method/lib/routes.js +118 -0
- package/dist/tools/method/lib/state.js +454 -0
- package/dist/tools/method/lib/text.js +165 -0
- package/dist/tools/method/mcp/prompts.js +79 -0
- package/dist/tools/method/mcp/resources.js +50 -0
- package/dist/tools/visibility/analyze.js +463 -0
- package/dist/tools/visibility/cli.js +86 -0
- package/dist/tools/visibility/estimate.js +105 -0
- package/dist/tools/visibility/index.js +108 -0
- package/dist/tools/visibility/judge.js +156 -0
- package/dist/tools/visibility/manual.js +275 -0
- package/dist/tools/visibility/models.js +277 -0
- package/dist/tools/visibility/monitoring.js +165 -0
- package/dist/tools/visibility/openrouter.js +449 -0
- package/dist/tools/visibility/render.js +295 -0
- package/dist/tools/visibility/run.js +684 -0
- package/dist/tools/visibility/summary.js +164 -0
- package/dist/types.js +3 -0
- package/dist/util.js +92 -0
- package/package.json +54 -0
- package/server.json +34 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { httpGet, sleep as realSleep } from "../../net.js";
|
|
2
|
+
import { mdTable } from "../../util.js";
|
|
3
|
+
import { collapse, fold } from "./common/text.js";
|
|
4
|
+
export const MAX_REQUESTS = 40;
|
|
5
|
+
export const DELAY_MS = 350;
|
|
6
|
+
const LETTERS = {
|
|
7
|
+
tr: "abcçdefgğhıijklmnoöprsştuüvyz",
|
|
8
|
+
de: "abcdefghijklmnopqrstuvwxyzäöü",
|
|
9
|
+
es: "abcdefghijklmnñopqrstuvwxyz",
|
|
10
|
+
};
|
|
11
|
+
export function suggestUrl(q, lang, country) {
|
|
12
|
+
return `https://suggestqueries.google.com/complete/search?client=firefox&hl=${encodeURIComponent(lang)}&gl=${encodeURIComponent(country)}&ie=utf-8&oe=utf-8&q=${encodeURIComponent(q)}`;
|
|
13
|
+
}
|
|
14
|
+
export function parseSuggestions(body) {
|
|
15
|
+
try {
|
|
16
|
+
const j = JSON.parse(body);
|
|
17
|
+
if (Array.isArray(j) && Array.isArray(j[1]))
|
|
18
|
+
return j[1].filter((x) => typeof x === "string");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* not JSON */
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
export function queriesFor(seed, lang, expand) {
|
|
26
|
+
const s = collapse(seed);
|
|
27
|
+
if (!expand)
|
|
28
|
+
return [s];
|
|
29
|
+
const letters = [...(LETTERS[lang] ?? "abcdefghijklmnopqrstuvwxyz")];
|
|
30
|
+
const qs = [s, ...letters.map((l) => `${s} ${l}`), ..."0123456789".split("").map((d) => `${s} ${d}`)];
|
|
31
|
+
return qs.slice(0, MAX_REQUESTS);
|
|
32
|
+
}
|
|
33
|
+
const Q_EN = /^(how|what|why|can|is|are|does|do|which|where|when|should|who)\b|\b(how to|what is|why does|can i|can you)\b/;
|
|
34
|
+
const Q_TR = /\b(nasil|nedir|neden|nicin|hangi|kac|ne zaman)\b|\s(mi|mu)(\s|$)/;
|
|
35
|
+
const CMP_EN = /\b(best|vs|versus|alternative|alternatives|compare|comparison|top|review|reviews|like)\b/;
|
|
36
|
+
const CMP_TR = /\b(en iyi|alternatif|alternatifleri|alternatifi|karsilastirma|yorum|yorumlari|benzeri)\b/;
|
|
37
|
+
const SIT_EN = /\b(free|without|for|with|no|instead|from)\b/;
|
|
38
|
+
const SIT_TR = /\b(ucretsiz|bedava|olmadan|icin|ile|yerine)\b/;
|
|
39
|
+
// First-person phrasing is how customers describe their situation to an assistant (book ch. 7).
|
|
40
|
+
const FIRST_PERSON = /\b(i|i'm|im|my|me|mine)\b|\b(istiyorum|yapamiyorum|bilmiyorum|olmuyor|calismiyor|benim|var ama)\b/;
|
|
41
|
+
/** Classify a suggestion by what it adds to the seed (the seed's own words don't count). */
|
|
42
|
+
export function classifyTerm(term, seed = "") {
|
|
43
|
+
const t = fold(term);
|
|
44
|
+
const seedTokens = new Set(fold(seed).split(/[^\p{L}\p{N}']+/u).filter(Boolean));
|
|
45
|
+
const extra = t.split(/[^\p{L}\p{N}']+/u).filter((w) => w && !seedTokens.has(w)).join(" ");
|
|
46
|
+
if (Q_EN.test(t) || Q_TR.test(t))
|
|
47
|
+
return "single-question";
|
|
48
|
+
if (CMP_EN.test(extra) || CMP_TR.test(extra))
|
|
49
|
+
return "comparison";
|
|
50
|
+
if (FIRST_PERSON.test(extra) || SIT_EN.test(extra) || SIT_TR.test(extra))
|
|
51
|
+
return "situation";
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const PAGE = {
|
|
55
|
+
"single-question": "single-question page (answer in the first 1–3 sentences, P6)",
|
|
56
|
+
situation: "situation page (P6)",
|
|
57
|
+
comparison: "honest comparison guide (P7)",
|
|
58
|
+
};
|
|
59
|
+
const STOP = new Set(["the", "a", "an", "and", "or", "of", "in", "on", "for", "to", "with", "ve", "ile", "bir", "icin", "da", "de"]);
|
|
60
|
+
export function cluster(seed, suggestions) {
|
|
61
|
+
const seedTokens = new Set(fold(seed).split(/[^\p{L}\p{N}]+/u).filter(Boolean));
|
|
62
|
+
const groups = new Map();
|
|
63
|
+
const label = new Map();
|
|
64
|
+
for (const s of suggestions) {
|
|
65
|
+
const words = s.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
66
|
+
const first = words.find((w) => !seedTokens.has(fold(w)) && !STOP.has(fold(w)));
|
|
67
|
+
let key = first ? fold(first) : "(seed wording)";
|
|
68
|
+
if (key.length > 4 && key.endsWith("s"))
|
|
69
|
+
key = key.slice(0, -1);
|
|
70
|
+
if (first && !label.has(key))
|
|
71
|
+
label.set(key, first);
|
|
72
|
+
groups.set(key, [...(groups.get(key) ?? []), s]);
|
|
73
|
+
}
|
|
74
|
+
let list = [...groups.entries()].map(([key, items]) => ({ key: label.get(key) ?? key, items })).sort((a, b) => b.items.length - a.items.length || a.key.localeCompare(b.key));
|
|
75
|
+
if (list.length > 14) {
|
|
76
|
+
const big = list.filter((c) => c.items.length > 1);
|
|
77
|
+
const small = list.filter((c) => c.items.length === 1).flatMap((c) => c.items);
|
|
78
|
+
list = small.length ? [...big, { key: "other", items: small }] : big;
|
|
79
|
+
}
|
|
80
|
+
return list;
|
|
81
|
+
}
|
|
82
|
+
export async function runKeywords(o) {
|
|
83
|
+
const lang = (o.lang ?? "en").toLowerCase();
|
|
84
|
+
const country = (o.country ?? "us").toLowerCase();
|
|
85
|
+
const fetcher = o.fetcher ?? httpGet;
|
|
86
|
+
const pause = o.sleep ?? realSleep;
|
|
87
|
+
const delay = o.delayMs ?? DELAY_MS;
|
|
88
|
+
const qs = queriesFor(o.seed, lang, o.expand !== false);
|
|
89
|
+
const errors = [];
|
|
90
|
+
const seen = new Map();
|
|
91
|
+
const seedKey = fold(collapse(o.seed));
|
|
92
|
+
let requests = 0;
|
|
93
|
+
for (let i = 0; i < qs.length && requests < MAX_REQUESTS; i++) {
|
|
94
|
+
if (i > 0)
|
|
95
|
+
await pause(delay);
|
|
96
|
+
requests++;
|
|
97
|
+
const res = await fetcher(suggestUrl(qs[i], lang, country), { redirect: "follow", headers: { accept: "application/json,text/javascript,*/*" }, timeoutMs: 10_000 });
|
|
98
|
+
if (res.status === 429 || res.status === 403) {
|
|
99
|
+
errors.push(`HTTP ${res.status} on "${qs[i]}": Google is rate-limiting; stopped early.`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (res.status !== 200) {
|
|
103
|
+
errors.push(`HTTP ${res.status || res.error} on "${qs[i]}"`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const s of parseSuggestions(res.body)) {
|
|
107
|
+
const c = collapse(s);
|
|
108
|
+
const k = fold(c);
|
|
109
|
+
if (!c || k === seedKey || seen.has(k))
|
|
110
|
+
continue;
|
|
111
|
+
seen.set(k, c);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const suggestions = [...seen.values()];
|
|
115
|
+
const candidates = [];
|
|
116
|
+
for (const s of suggestions) {
|
|
117
|
+
const kind = classifyTerm(s, o.seed);
|
|
118
|
+
if (kind)
|
|
119
|
+
candidates.push({ term: s, kind, page: PAGE[kind] });
|
|
120
|
+
}
|
|
121
|
+
return { seed: collapse(o.seed), lang, country, requests, errors, suggestions, clusters: cluster(o.seed, suggestions), candidates };
|
|
122
|
+
}
|
|
123
|
+
/** Terms worth saving: every candidate, then the top of each cluster (max 60). */
|
|
124
|
+
export function termsToSave(r) {
|
|
125
|
+
const out = new Map();
|
|
126
|
+
for (const c of r.candidates)
|
|
127
|
+
out.set(fold(c.term), c.term);
|
|
128
|
+
for (const c of r.clusters)
|
|
129
|
+
for (const t of c.items.slice(0, 3))
|
|
130
|
+
out.set(fold(t), t);
|
|
131
|
+
return [...out.values()].slice(0, 60);
|
|
132
|
+
}
|
|
133
|
+
/** Merge into research.keywords (source "autocomplete"); returns the number added. */
|
|
134
|
+
export function mergeKeywords(research, terms) {
|
|
135
|
+
const have = new Set((research.keywords ?? []).map((k) => fold(k.term)));
|
|
136
|
+
let added = 0;
|
|
137
|
+
research.keywords ??= [];
|
|
138
|
+
for (const t of terms) {
|
|
139
|
+
if (have.has(fold(t)))
|
|
140
|
+
continue;
|
|
141
|
+
const k = { term: t, source: "autocomplete" };
|
|
142
|
+
research.keywords.push(k);
|
|
143
|
+
have.add(fold(t));
|
|
144
|
+
added++;
|
|
145
|
+
}
|
|
146
|
+
return added;
|
|
147
|
+
}
|
|
148
|
+
export function keywordsMarkdown(r, saved) {
|
|
149
|
+
const L = [];
|
|
150
|
+
L.push(`# Google autocomplete — "${r.seed}" (${r.lang}-${r.country.toUpperCase()})`);
|
|
151
|
+
L.push(`${r.suggestions.length} unique suggestions from ${r.requests} requests (seed + letters/digits, ${DELAY_MS} ms apart; suggestqueries.google.com only).`);
|
|
152
|
+
for (const e of r.errors.slice(0, 5))
|
|
153
|
+
L.push(`- ${e}`);
|
|
154
|
+
L.push("");
|
|
155
|
+
if (!r.suggestions.length) {
|
|
156
|
+
L.push("No suggestions: try a shorter seed in the customer's own words, or another language/country.");
|
|
157
|
+
return L.join("\n");
|
|
158
|
+
}
|
|
159
|
+
const byKind = (k) => r.candidates.filter((c) => c.kind === k);
|
|
160
|
+
for (const [kind, title] of [["single-question", "Single-question page candidates"], ["situation", "Situation page candidates"], ["comparison", "Comparison / alternatives candidates"]]) {
|
|
161
|
+
const list = byKind(kind);
|
|
162
|
+
if (!list.length)
|
|
163
|
+
continue;
|
|
164
|
+
L.push(`## ${title} (${list.length}) → ${PAGE[kind]}`);
|
|
165
|
+
for (const c of list.slice(0, 20))
|
|
166
|
+
L.push(`- ${c.term}`);
|
|
167
|
+
if (list.length > 20)
|
|
168
|
+
L.push(`- …${list.length - 20} more`);
|
|
169
|
+
L.push("");
|
|
170
|
+
}
|
|
171
|
+
L.push("## Clusters");
|
|
172
|
+
L.push(mdTable(["Cluster", "Count", "Examples"], r.clusters.slice(0, 20).map((c) => [c.key, c.items.length, c.items.slice(0, 4).join(" · ")])));
|
|
173
|
+
L.push("");
|
|
174
|
+
L.push("Autocomplete shows what people type into Google. People describe situations to ChatGPT in longer sentences: combine these with the situations in research.json (unbranded, in the customer's own words).");
|
|
175
|
+
if (saved)
|
|
176
|
+
L.push(`\nSaved: ${saved.added} new keyword(s) merged into .katman/research.json (source: autocomplete).${saved.note ? ` ${saved.note}` : ""}`);
|
|
177
|
+
return L.join("\n");
|
|
178
|
+
}
|