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,88 @@
|
|
|
1
|
+
export const VERIFY_INTRO = {
|
|
2
|
+
en: "Don't trust the coding tool saying “done”. These commands run in a Mac or Linux terminal.",
|
|
3
|
+
tr: "Kod aracının “yaptım” demesine güvenmeyin. Bu komutlar Mac ve Linux terminalinde çalışır.",
|
|
4
|
+
};
|
|
5
|
+
export const NINE_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
n: 1,
|
|
8
|
+
title: { en: "Does the page return 200; does http go to https?", tr: "Sayfa 200 dönüyor mu, http https'e gidiyor mu?" },
|
|
9
|
+
cmd: `curl -sI {site_url}/ | head -1
|
|
10
|
+
curl -sI http://{domain}/ | grep -i -E "^HTTP|^location"`,
|
|
11
|
+
expect: { en: "HTTP/2 200; the http request answers 301/308 with location: https://…", tr: "HTTP/2 200; http isteği 301/308 ve location: https://… ile cevap veriyor" },
|
|
12
|
+
katman: "katman_audit_site → status, site.httpsRedirect",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
n: 2,
|
|
16
|
+
title: { en: "Is a missing URL a real 404?", tr: "Olmayan adres gerçek 404 mü?" },
|
|
17
|
+
cmd: `curl -s -o /dev/null -w "%{http_code}\\n" {site_url}/this-page-does-not-exist`,
|
|
18
|
+
expect: { en: "404", tr: "404" },
|
|
19
|
+
katman: "katman_audit_site → site.soft404",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
n: 3,
|
|
23
|
+
title: { en: "Title, description, canonical, robots, og:image", tr: "Title, açıklama, canonical, robots, og:image" },
|
|
24
|
+
cmd: `curl -s {site_url}/ | grep -o -i -E '<title>[^<]*|name="description" content="[^"]*|rel="canonical" href="[^"]*|name="robots" content="[^"]*|og:image" content="[^"]*'`,
|
|
25
|
+
expect: { en: "All five present; canonical is the page's own full URL; no unwanted noindex", tr: "Beşi de var; canonical sayfanın kendi tam adresi; istenmeyen noindex yok" },
|
|
26
|
+
katman: "katman_audit_site → title, description, canonicalSelf, noindex, ogImage",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
n: 4,
|
|
30
|
+
title: { en: "How many words are visible without JavaScript?", tr: "JavaScript olmadan kaç kelime görünüyor?" },
|
|
31
|
+
cmd: `curl -s {site_url}/ | sed 's/<[^>]*>/ /g' | wc -w`,
|
|
32
|
+
expect: { en: "Hundreds, not a few dozen", tr: "Birkaç düzine değil, yüzlerce" },
|
|
33
|
+
katman: "katman_audit_site → pages[].wordsJsOff",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
n: 5,
|
|
37
|
+
title: { en: "Can AI bots get in? (all should be 200)", tr: "Yapay zekâ botları içeri girebiliyor mu? (hepsi 200 olmalı)" },
|
|
38
|
+
cmd: `for b in GPTBot/1.2 OAI-SearchBot/1.0 ClaudeBot/1.0 PerplexityBot/1.0; do
|
|
39
|
+
echo "$b $(curl -s -o /dev/null -w '%{http_code}' -A "Mozilla/5.0 (compatible; $b)" {site_url}/)"; done`,
|
|
40
|
+
expect: { en: "200 for every bot", tr: "Her bot için 200" },
|
|
41
|
+
katman: "katman_audit_site → site.bots",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
n: 6,
|
|
45
|
+
title: { en: "robots.txt and the sitemap", tr: "robots.txt ve sitemap" },
|
|
46
|
+
cmd: `curl -s {site_url}/robots.txt
|
|
47
|
+
curl -s {site_url}/sitemap.xml | grep -c "<loc>"`,
|
|
48
|
+
expect: { en: "No Disallow: /, a Sitemap line; the number of URLs you expect", tr: "Disallow: / yok, Sitemap satırı var; beklediğiniz sayıda adres" },
|
|
49
|
+
katman: "katman_audit_site → site.robots, site.sitemapUrls",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
n: 7,
|
|
53
|
+
title: { en: "Is the page's JSON-LD valid JSON? (needs python3)", tr: "Sayfadaki JSON-LD geçerli JSON mu? (python3 gerekir)" },
|
|
54
|
+
cmd: `curl -s {site_url}/ | python3 -c "import sys,re,json; b=re.findall(r'<script[^>]*application/ld\\+json[^>]*>(.*?)</script>', sys.stdin.read(), re.S); [json.loads(x) for x in b]; print(len(b), 'valid JSON-LD blocks')"`,
|
|
55
|
+
expect: { en: "N valid JSON-LD blocks, no traceback", tr: "N geçerli JSON-LD bloğu, hata yok" },
|
|
56
|
+
katman: "katman_audit_site → pages[].jsonLdErrors",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
n: 8,
|
|
60
|
+
title: { en: "Mobile speed and SEO score", tr: "Mobil hız ve SEO puanı" },
|
|
61
|
+
cmd: `npx lighthouse {site_url}/ --form-factor=mobile --only-categories=performance,seo --view`,
|
|
62
|
+
expect: { en: "LCP ≤ 2.5 s, CLS ≤ 0.1, SEO 100", tr: "LCP ≤ 2,5 sn, CLS ≤ 0,1, SEO 100" },
|
|
63
|
+
katman: "manual (Katman doesn't run Lighthouse)",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
n: 9,
|
|
67
|
+
title: { en: "Is the HTML compressed? (re-check after every technical fix)", tr: "HTML sıkıştırılıyor mu? (her teknik düzeltmeden sonra tekrar)" },
|
|
68
|
+
cmd: `curl -s -o /dev/null -D - -H 'Accept-Encoding: br, gzip' {site_url}/ | grep -i content-encoding`,
|
|
69
|
+
expect: { en: "content-encoding: br (or gzip)", tr: "content-encoding: br (ya da gzip)" },
|
|
70
|
+
katman: "katman_audit_site → site.compression",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
export const BROWSER_CHECKS = [
|
|
74
|
+
{
|
|
75
|
+
en: "Search Console → URL Inspection → Test live URL → View tested page: the HTML and screenshot Google sees. If your text isn't there, it isn't in Google either.",
|
|
76
|
+
tr: "Search Console → URL denetimi → Canlı URL’yi test et → Taranan sayfayı görüntüle: Google’ın gördüğü HTML ve ekran görüntüsü. Metniniz burada yoksa Google’da da yok.",
|
|
77
|
+
},
|
|
78
|
+
{ en: "Rich Results Test (search.google.com/test/rich-results): JSON-LD errors.", tr: "Zengin Sonuçlar Testi (search.google.com/test/rich-results): JSON-LD hataları." },
|
|
79
|
+
{
|
|
80
|
+
en: "Chrome → Developer tools → Settings → Disable JavaScript: see the page through the bot's eyes.",
|
|
81
|
+
tr: "Chrome → Geliştirici araçları → Ayarlar → JavaScript’i devre dışı bırak: sayfayı botun gözüyle görün.",
|
|
82
|
+
},
|
|
83
|
+
{ en: "Paste a link into a chat app: does the og:image card appear?", tr: "Bir sohbet uygulamasına link yapıştırın: og:image kartı çıkıyor mu?" },
|
|
84
|
+
];
|
|
85
|
+
export const VERIFY_EXERCISE = {
|
|
86
|
+
en: "Run the nine commands on your home page and on your most important situation page. Paste the output into research; run them again in 30 days and compare.",
|
|
87
|
+
tr: "Dokuz komutu kendi sitenizin ana sayfasında ve en önemli durum sayfanızda çalıştırın. Çıktıları research.md’ye yapıştırın; 30 gün sonra tekrar çalıştırıp karşılaştırın.",
|
|
88
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const VERDICT_LABEL = {
|
|
2
|
+
true: { en: "TRUE", tr: "DOĞRU" },
|
|
3
|
+
partly: { en: "PARTLY", tr: "KISMEN" },
|
|
4
|
+
myth: { en: "MYTH", tr: "EFSANE" },
|
|
5
|
+
};
|
|
6
|
+
export const VIRAL_LIST = [
|
|
7
|
+
{ n: 1, item: { en: "noindex tags", tr: "noindex etiketleri" }, verdict: "true", google: { en: "noindex removes a page from the index. If robots.txt closes the same URL, Google never sees the noindex and the URL can still appear.", tr: "noindex sayfayı dizinden çıkarır. robots.txt aynı adresi kapatırsa Google noindex’i hiç göremez ve adres yine çıkabilir." } },
|
|
8
|
+
{ n: 2, item: { en: "Page titles", tr: "Sayfa başlıkları (title)" }, verdict: "true", google: { en: "A separate, descriptive title for every page. Google may write the title it shows itself, from the H1 or og:title.", tr: "Her sayfaya ayrı, içeriği anlatan bir title. Google sonuçta gösterdiği başlığı kendisi yazabilir; H1’i ya da og:title’ı kullanabilir." } },
|
|
9
|
+
{ n: 3, item: { en: "Meta descriptions", tr: "Meta açıklamalar" }, verdict: "partly", google: { en: "The snippet mostly comes from the page text; the meta description is used only sometimes. No ranking effect is stated. Write a separate one for every page.", tr: "Snippet çoğunlukla sayfanın metninden çıkar; meta açıklama yalnızca bazen kullanılır. Sıralama etkisi yazmıyor. Her sayfaya ayrı yazın." } },
|
|
10
|
+
{ n: 4, item: { en: "Image alt text", tr: "Görsellere alt metin" }, verdict: "true", google: { en: "Google understands images through alt text, image recognition and page text. Keyword-stuffed alt text can count as spam.", tr: "Google görseli alt metin, görüntü tanıma ve sayfa metniyle anlar. Anahtar kelime doldurulmuş alt metin spam sayılabilir." } },
|
|
11
|
+
{ n: 5, item: { en: "Core Web Vitals", tr: "Core Web Vitals" }, verdict: "partly", google: { en: "Used in ranking. Good: LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 (at 75% of page loads). Not decisive on its own; relevance comes first.", tr: "Sıralamada kullanılır. İyi: LCP ≤ 2,5 sn, INP ≤ 200 ms, CLS ≤ 0,1 (sayfa yüklemelerinin %75’inde). Tek başına belirleyici değil; alakalılık önce gelir." } },
|
|
12
|
+
{ n: 6, item: { en: "sitemap.xml", tr: "sitemap.xml" }, verdict: "partly", google: { en: "Helps discovery but doesn't guarantee indexing. A well-linked site under ~500 pages may not need one.", tr: "Keşfe yardım eder, dizine almayı garanti etmez. Yaklaşık 500 sayfanın altındaki, iyi linklenmiş bir site ihtiyaç duymayabilir." } },
|
|
13
|
+
{ n: 7, item: { en: "og:image", tr: "og:image" }, verdict: "partly", google: { en: "The Open Graph protocol is for share previews. Since 2 March 2026 Google also uses og:image as a source for the search thumbnail; no effect on indexing or ranking.", tr: "Açık Grafik protokolü paylaşım önizlemesi içindir. Google 2 Mart 2026’dan beri og:image’i arama küçük resmi için bir kaynak olarak da kullanıyor; dizine alma ya da sıralama etkisi yok." } },
|
|
14
|
+
{ n: 8, item: { en: "Broken links", tr: "Kırık linkler" }, verdict: "partly", google: { en: "404s don't penalise the rest of the site on their own; a URL returning 4xx drops out of the index. Fix broken links to important pages; 301 moved URLs.", tr: "404’ler sitenin geri kalanını tek başına cezalandırmaz; 4xx dönen adres dizinden düşer. Önemli sayfalara giden kırık linkleri düzeltin, taşınan adresi 301 ile yönlendirin." } },
|
|
15
|
+
{ n: 9, item: { en: "Heading hierarchy", tr: "Başlık hiyerarşisi" }, verdict: "partly", google: { en: "Heading order doesn't matter to Google; a clean order helps screen readers. Bing wants a logical H1–H6 structure.", tr: "Google için başlıkların sırası önemli değil; düzgün sıra ekran okuyuculara yardım eder. Bing mantıklı bir H1–H6 düzeni istiyor." } },
|
|
16
|
+
{ n: 10, item: { en: "Backlink strategy", tr: "Backlink stratejisi" }, verdict: "partly", google: { en: "Google finds pages mostly through links and expects links to come naturally over time. Buying links and excessive link exchanges are spam.", tr: "Google sayfaları çoğunlukla linklerle bulur ve linklerin zamanla doğal gelmesini bekler. Link satın almak ve aşırı link takası spam." } },
|
|
17
|
+
{ n: 11, item: { en: "Clean URLs", tr: "Temiz URL’ler" }, verdict: "partly", google: { en: "Readable words, hyphens instead of underscores. Words in the URL have very little ranking effect. If you change a live URL, a 301 is a must.", tr: "Okunabilir kelimeler, alt çizgi yerine tire. URL’deki kelimelerin sıralamaya etkisi çok az. Canlı adresi değiştirirseniz 301 şart." } },
|
|
18
|
+
{ n: 12, item: { en: "Internal links", tr: "İç linkler" }, verdict: "true", google: { en: "Link to every page you care about from another page. A link must be an <a> with an href; the link text should describe the target.", tr: "Önem verdiğiniz her sayfaya başka bir sayfadan link verin. Link, href’i olan bir <a> olmalı; link metni hedefi anlatmalı." } },
|
|
19
|
+
{ n: 13, item: { en: "Canonical tags", tr: "Canonical etiketleri" }, verdict: "partly", google: { en: "rel=canonical is a strong signal but a hint, not a rule. None of it is mandatory; Google also picks the canonical itself.", tr: "rel=canonical güçlü bir işaret ama kural değil, ipucu. Hiçbiri zorunlu değil; Google kanonik adresi kendisi de seçer." } },
|
|
20
|
+
{ n: 14, item: { en: "HTTPS everywhere", tr: "HTTPS zorunlu" }, verdict: "true", google: { en: "HTTPS is a small ranking signal and part of page experience. Among equivalent pages Google picks the HTTPS one as canonical.", tr: "HTTPS küçük bir sıralama işareti ve sayfa deneyiminin parçası. Google eşdeğer sayfalarda HTTPS olanı kanonik seçer." } },
|
|
21
|
+
{ n: 15, item: { en: "Compress images", tr: "Görselleri sıkıştır" }, verdict: "true", google: { en: "Images are often the biggest part of page weight. The benefit is indirect: speed and LCP. WebP and AVIF are supported.", tr: "Görseller çoğu zaman sayfa boyutunun en büyük parçası. Faydası dolaylı: hız ve LCP. WebP ve AVIF destekleniyor." } },
|
|
22
|
+
{ n: 16, item: { en: "Schema markup", tr: "Schema işaretlemesi" }, verdict: "partly", google: { en: "Only makes you eligible for rich results, no guarantee; must match the visible content. The HowTo rich result was retired in 2023, the FAQ rich result on 7 May 2026.", tr: "Yalnızca zengin sonuca aday yapar, garanti vermez; görünen içerikle aynı olmalı. HowTo zengin sonucu 2023’te, SSS zengin sonucu 7 Mayıs 2026’da kalktı." } },
|
|
23
|
+
{ n: 17, item: { en: "Verify Search Console", tr: "Search Console doğrula" }, verdict: "partly", google: { en: "Not required to be indexed; most pages are found on their own. It's a monitoring and debugging tool (URL Inspection, sitemap report).", tr: "İndekslenmek için gerekmez; sayfaların çoğu kendiliğinden bulunur. İzleme ve hata ayıklama aracıdır (URL denetimi, sitemap raporu)." } },
|
|
24
|
+
{ n: 18, item: { en: "One H1 per page", tr: "Sayfada tek H1" }, verdict: "myth", google: { en: "According to Google there's no ideal number of headings; more than one H1 is fine.", tr: "Google’a göre ideal bir başlık sayısı yok; birden fazla H1 sorun değil." } },
|
|
25
|
+
{ n: 19, item: { en: "Write a robots.txt", tr: "robots.txt yaz" }, verdict: "partly", google: { en: "Not mandatory: if robots.txt returns 404, Google treats it as no restrictions. It manages crawling, not indexing; it doesn't hide a page from Google.", tr: "Zorunlu değil: robots.txt 404 dönerse Google kısıtlama yok sayar. Taramayı yönetir, dizine almayı değil; bir sayfayı Google’dan saklamaz." } },
|
|
26
|
+
{ n: 20, item: { en: "Mobile friendly", tr: "Mobil uyum" }, verdict: "true", google: { en: "Google indexes and ranks the mobile version of the site. Responsive design is recommended; content, headings and schema must be the same on mobile.", tr: "Google sitenin mobil hâlini dizine alır ve sıralar. Duyarlı tasarım önerilir; içerik, başlıklar ve schema mobilde de aynı olmalı." } },
|
|
27
|
+
];
|
|
28
|
+
export const VIRAL_LIST_MISLEADING = [
|
|
29
|
+
{ en: "One H1: according to Google there's no ideal number of headings. One H1 is a tidy habit, not a ranking rule.", tr: "Tek H1: Google’a göre ideal bir başlık sayısı yok. Tek H1 düzenli bir alışkanlık, sıralama kuralı değil." },
|
|
30
|
+
{ en: "“Write a robots.txt”: you can do without one. Written wrong, it does harm: Disallow: / closes the whole site; closing a noindex page in robots.txt hides the noindex from Google.", tr: "“robots.txt yaz”: Yazmasanız da olur. Yanlış yazarsanız zarar verir: Disallow: / bütün siteyi kapatır; noindex’li bir sayfayı robots.txt’de kapatırsanız Google noindex’i göremez." },
|
|
31
|
+
{ en: "Schema: no rich-result guarantee. HowTo was retired in 2023, the FAQ rich result on 7 May 2026.", tr: "Schema: Zengin sonuç garantisi yok. HowTo 2023’te, SSS zengin sonucu 7 Mayıs 2026’da Google’dan kalktı." },
|
|
32
|
+
{ en: "Search Console: not needed to get indexed; needed to see the errors.", tr: "Search Console: İndekslenmek için gerekmez; hatayı görmek için gerekir." },
|
|
33
|
+
{ en: "Backlink “strategy”: bought links and link exchanges are in Google's spam policy. Links come when you publish something others want to quote.", tr: "Backlink “stratejisi”: Satın alınan link ve link takası Google’ın spam politikasında. Link, başkasının alıntılamak isteyeceği bir şey yayınlayınca gelir." },
|
|
34
|
+
{ en: "The real gap: the three things that most often make a vibe-coded site invisible aren't on the list at all — text in JavaScript, soft 404s, and open preview copies.", tr: "Asıl eksik: Vibe-coded bir siteyi en sık görünmez yapan üç şey listede hiç yok: metnin JavaScript’te olması, sahte 404’ler, açık kalmış önizleme kopyaları." },
|
|
35
|
+
];
|
|
36
|
+
export const VIRAL_LIST_FOUND = {
|
|
37
|
+
en: "Not on the list, found on katman.pro: AI training bots (GPTBot, ClaudeBot, CCBot, Bytespider) were getting 403 because of Cloudflare's legacy “Block AI bots” toggle. The list's biggest gap: if a bot can't get through the door, none of the other 20 items matter to the assistant.",
|
|
38
|
+
tr: "Listede yok, katman.pro’da bulduk: Yapay zekâ eğitim botları (GPTBot, ClaudeBot, CCBot, Bytespider) Cloudflare’deki eski “Block AI bots” düğmesi yüzünden 403 alıyordu. Listenin en büyük eksiği bu: bot kapıdan giremiyorsa diğer 20 maddenin asistan tarafında karşılığı yok.",
|
|
39
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Method module: the Katman book as tools (katman_start, katman_research, katman_plan, katman_prompt,
|
|
2
|
+
// katman_page_brief, katman_generate, katman_lint_content, katman_report), MCP prompts and resources.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { resolveRoot, rootWarning } from "../../context.js";
|
|
5
|
+
import { fail, ok } from "../../util.js";
|
|
6
|
+
import { PRO_NOTE, startLicense, withPro } from "../../license.js";
|
|
7
|
+
import { hasOpenRouterKey } from "./lib/calls.js";
|
|
8
|
+
import { runStart } from "./core/start.js";
|
|
9
|
+
import { runResearch } from "./core/research-run.js";
|
|
10
|
+
import { runPlan } from "./core/plan.js";
|
|
11
|
+
import { runPrompt } from "./core/prompt.js";
|
|
12
|
+
import { BRIEF_TYPES, runBrief } from "./core/brief.js";
|
|
13
|
+
import { ARTIFACTS, runGenerate } from "./core/generate.js";
|
|
14
|
+
import { PAGE_TYPES_LINT, runLint } from "./core/lint.js";
|
|
15
|
+
import { runReport } from "./core/report.js";
|
|
16
|
+
import { BOT_PLATFORMS } from "./core/snippets.js";
|
|
17
|
+
import { registerMethodPrompts } from "./mcp/prompts.js";
|
|
18
|
+
import { registerMethodResources } from "./mcp/resources.js";
|
|
19
|
+
const projectRoot = z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Absolute path of the website's codebase. Default: KATMAN_PROJECT_ROOT, then the client's workspace root, then the server's cwd.");
|
|
23
|
+
const lang = z.enum(["en", "tr"]).optional().describe("Output language: en or tr (default: .katman/config.json lang, else en).");
|
|
24
|
+
const QUESTION_TYPES = ["situation", "tool", "best", "alternative", "market", "problem", "diagnostic"];
|
|
25
|
+
const researchData = z
|
|
26
|
+
.object({
|
|
27
|
+
site: z
|
|
28
|
+
.object({
|
|
29
|
+
url: z.string().optional().describe("Live site URL, e.g. https://example.com"),
|
|
30
|
+
languages: z.array(z.string()).optional().describe("Site languages, primary first, e.g. [\"en\", \"tr\"]"),
|
|
31
|
+
previewUrls: z.array(z.string()).optional().describe("Preview/staging hosts that must redirect or be noindex"),
|
|
32
|
+
})
|
|
33
|
+
.optional(),
|
|
34
|
+
brand: z
|
|
35
|
+
.object({
|
|
36
|
+
name: z.string().describe("One distinctive name"),
|
|
37
|
+
alternateNames: z.array(z.string()),
|
|
38
|
+
category: z.string().describe("What it is, e.g. \"AI invoice scanner\""),
|
|
39
|
+
notX: z.string().describe("What it's NOT confused with (ask ChatGPT \"What is <brand>?\"), or \"none\""),
|
|
40
|
+
oneLiner: z
|
|
41
|
+
.object({
|
|
42
|
+
brings: z.string().describe("What the customer brings"),
|
|
43
|
+
weDo: z.string().describe("What we do"),
|
|
44
|
+
gets: z.string().describe("What they get"),
|
|
45
|
+
fear: z.string().describe("Which fear is unnecessary"),
|
|
46
|
+
})
|
|
47
|
+
.partial(),
|
|
48
|
+
sentence: z.string().describe("Optional hand-written final one sentence (else composed from oneLiner)"),
|
|
49
|
+
pricing: z.string().describe("Exactly as on the pricing page"),
|
|
50
|
+
company: z.string(),
|
|
51
|
+
country: z.string(),
|
|
52
|
+
socials: z.array(z.string()),
|
|
53
|
+
})
|
|
54
|
+
.partial()
|
|
55
|
+
.optional(),
|
|
56
|
+
situations: z
|
|
57
|
+
.array(z.object({
|
|
58
|
+
id: z.string().optional(),
|
|
59
|
+
sentence: z.string().describe("The customer's own sentence, no brand name"),
|
|
60
|
+
url: z.string().optional().describe("Planned or live path, e.g. /import-pdf-invoices"),
|
|
61
|
+
status: z.enum(["planned", "live"]).optional(),
|
|
62
|
+
}))
|
|
63
|
+
.optional(),
|
|
64
|
+
competitors: z
|
|
65
|
+
.array(z.object({
|
|
66
|
+
name: z.string(),
|
|
67
|
+
url: z.string().optional().describe("The competitor's own page the facts come from"),
|
|
68
|
+
what: z.string().optional(),
|
|
69
|
+
price: z.string().optional(),
|
|
70
|
+
forWhom: z.string().optional(),
|
|
71
|
+
source: z.string().optional(),
|
|
72
|
+
checkedAt: z.string().optional().describe("YYYY-MM-DD"),
|
|
73
|
+
}))
|
|
74
|
+
.optional(),
|
|
75
|
+
proof: z
|
|
76
|
+
.array(z.object({ claim: z.string(), value: z.string().optional(), source: z.string().optional().describe("Link or document"), date: z.string().optional() }))
|
|
77
|
+
.optional(),
|
|
78
|
+
keywords: z
|
|
79
|
+
.array(z.object({ term: z.string(), page: z.string().optional(), source: z.enum(["autocomplete", "search-console", "bing", "manual"]).optional() }))
|
|
80
|
+
.optional(),
|
|
81
|
+
questions: z.array(z.object({ id: z.string().optional(), type: z.enum(QUESTION_TYPES).optional(), text: z.string() })).optional(),
|
|
82
|
+
})
|
|
83
|
+
.describe("Research fields to merge");
|
|
84
|
+
const generateOptions = z
|
|
85
|
+
.object({
|
|
86
|
+
app_paths: z.array(z.string()).optional().describe("robots.txt: extra app paths to close, e.g. [\"/app\", \"/account\"]"),
|
|
87
|
+
training: z.enum(["allow", "block"]).optional().describe("robots.txt: AI training bots (default allow; search/user bots always allowed)"),
|
|
88
|
+
sitemap_url: z.string().optional(),
|
|
89
|
+
faq: z.array(z.object({ q: z.string(), a: z.string() })).optional().describe("faq-jsonld: the exact Q&A visible on the page"),
|
|
90
|
+
items: z.array(z.object({ name: z.string(), url: z.string() })).optional().describe("breadcrumb-jsonld: Home → … → page"),
|
|
91
|
+
target: z.enum(["auto", "gtag", "gtm", "plausible", "posthog", "none"]).optional().describe("analytics-channel-snippet: where to push events"),
|
|
92
|
+
platform: z.enum(BOT_PLATFORMS).optional().describe("bot-counter-snippet platform"),
|
|
93
|
+
type: z.enum(["SoftwareApplication", "WebApplication"]).optional(),
|
|
94
|
+
application_category: z.string().optional(),
|
|
95
|
+
logo: z.string().optional().describe("organization-jsonld: logo URL"),
|
|
96
|
+
page_url: z.string().optional(),
|
|
97
|
+
})
|
|
98
|
+
.optional();
|
|
99
|
+
function wrap(ctx, fn, opts = {}) {
|
|
100
|
+
return async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
const root = await resolveRoot(ctx, args.project_root);
|
|
103
|
+
const out = await fn(root, args);
|
|
104
|
+
const warning = opts.warn === false ? undefined : rootWarning(root);
|
|
105
|
+
const text = warning ? `**Warning:** ${warning}\n\n${out.text}` : out.text;
|
|
106
|
+
return out.isError ? fail(text) : ok(text, out.structured);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return fail(`Katman error: ${e instanceof Error ? e.message : String(e)}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function registerMethodTools(server, ctx) {
|
|
114
|
+
server.registerTool("katman_start", {
|
|
115
|
+
title: "Katman: start here",
|
|
116
|
+
description: "Call this FIRST, and again after every step. Shows the Katman method (gates Read / Quote / Fit; steps Research → Plan → Build → Verify), the status of each step from .katman/ (research %, scan, audit, plan tasks, visibility runs, 14-day re-measure) and the NEXT STEP as an exact tool call with arguments — do that next. Pass project_root (absolute path of the site's codebase) and site_url when known; site_url is saved to .katman/config.json. Writes nothing else.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
project_root: projectRoot,
|
|
119
|
+
site_url: z.string().optional().describe("The live site URL, e.g. https://example.com (saved to .katman/config.json)"),
|
|
120
|
+
lang,
|
|
121
|
+
},
|
|
122
|
+
annotations: { title: "Katman: start here", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
try {
|
|
125
|
+
const root = await resolveRoot(ctx, args.project_root);
|
|
126
|
+
const out = await runStart(root, { siteUrl: args.site_url, hasOpenRouterKey: hasOpenRouterKey(), warning: rootWarning(root), lang: args.lang, license: await startLicense(ctx) });
|
|
127
|
+
return out.isError ? fail(out.text) : ok(out.text, out.structured);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return fail(`Katman error: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
server.registerTool("katman_research", {
|
|
134
|
+
title: "Katman: research (step 1)",
|
|
135
|
+
description: "Step 1. Creates and maintains .katman/research.json + research.md: brand, category, the one sentence in 4 parts (brings → weDo → gets → fear), not-X note, 10–12 unbranded customer situations, competitors (url + checkedAt), proof (with source), keywords, question set. mode template = skeleton; update = merge data (lists merge by id/name/sentence; replace_lists replaces); show = print with completeness % and warnings. generate_questions builds the question set per site language (12+2 for the primary). Fill from the codebase, ask the user for customer sentences; never invent.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
project_root: projectRoot,
|
|
138
|
+
mode: z.enum(["template", "update", "show"]).optional().describe("Default: update when data/generate_questions is given, else show (or template when there's no research)"),
|
|
139
|
+
data: researchData.optional(),
|
|
140
|
+
replace_lists: z.boolean().optional().describe("Replace the lists given in data instead of merging (also allows regenerating the question set)"),
|
|
141
|
+
generate_questions: z.boolean().optional().describe("Build the 12 + 2 question set from research (book ch. 15) in the site's primary language"),
|
|
142
|
+
lang,
|
|
143
|
+
},
|
|
144
|
+
annotations: { title: "Katman: research", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
145
|
+
}, wrap(ctx, (root, a) => runResearch(root, { mode: a.mode, data: a.data, replaceLists: a.replace_lists, generateQuestions: a.generate_questions, lang: a.lang })));
|
|
146
|
+
server.registerTool("katman_plan", {
|
|
147
|
+
title: "Katman: plan (step 2)",
|
|
148
|
+
description: "Step 2. Builds .katman/plan.md + plan.json from research, scan.json, the latest audit and runs: the page plan (existing vs missing pages), what stays out of the index (noindex vs robots.txt Disallow), and the first-30-days task list (P-code, files, acceptance criterion, verifying tool; critical findings first). Ticks in plan.md are kept when it regenerates. Show the plan to the user and get approval before changing code. Re-run after new scans or audits." + PRO_NOTE,
|
|
149
|
+
inputSchema: { project_root: projectRoot, lang },
|
|
150
|
+
annotations: { title: "Katman: plan", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
151
|
+
}, withPro(ctx, "plan", wrap(ctx, (root, a) => runPlan(root, { lang: a.lang }))));
|
|
152
|
+
server.registerTool("katman_prompt", {
|
|
153
|
+
title: "Katman: prompt P0–P14 (step 3)",
|
|
154
|
+
description: "Step 3. Returns prompt P0–P14 from the Katman prompt book filled with this project's research (brand, domain, one sentence, not-X, competitors, situation) plus Accept (acceptance criterion), Why, how Katman verifies it (exact tool call) and framework notes from scan.json. Do the prompt, list the changed files, run the check; tick the task in .katman/plan.md only when it passes. P6 takes situation_id. Unknown values stay [FILL] — ask, don't guess. Read-only." + PRO_NOTE,
|
|
155
|
+
inputSchema: {
|
|
156
|
+
id: z.string().describe("Prompt id: P0–P14 (P0 context, P1 audit, P2 text in HTML, P3 head tags, P4 robots/sitemap, P5 schema, P6 situation page, P7 comparison guide, P8 llms.txt, P9 images, P10 links, P11 404/redirects, P12 speed, P13 measurement, P14 pre-release check)"),
|
|
157
|
+
project_root: projectRoot,
|
|
158
|
+
situation_id: z.string().optional().describe("For P6: the situation id from research (s1, s2…)"),
|
|
159
|
+
lang,
|
|
160
|
+
},
|
|
161
|
+
annotations: { title: "Katman: prompt", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
162
|
+
}, withPro(ctx, "prompts", wrap(ctx, (root, a) => runPrompt(root, { id: a.id, situationId: a.situation_id, lang: a.lang }))));
|
|
163
|
+
server.registerTool("katman_page_brief", {
|
|
164
|
+
title: "Katman: page brief",
|
|
165
|
+
description: "Filled page brief before writing a page: URL, title (situation first, ≈60 chars), H1, first sentence, action, proof, not-for, alternatives, FAQ candidates, related pages, the 11-block anatomy, the JSON-LD to include (only information visible on the page) and where the file goes in this framework. type: situation | comparison | pricing | faq | about | single-question | glossary | tool | home. Use it before P6 / P7. Read-only." + PRO_NOTE,
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: z.enum(BRIEF_TYPES).describe("Page type"),
|
|
168
|
+
project_root: projectRoot,
|
|
169
|
+
situation_id: z.string().optional().describe("For type situation: the research situation id (default: the next one without a page)"),
|
|
170
|
+
topic: z.string().optional().describe("For comparison / single-question / tool: the topic, question or tool name"),
|
|
171
|
+
lang,
|
|
172
|
+
},
|
|
173
|
+
annotations: { title: "Katman: page brief", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
174
|
+
}, withPro(ctx, "briefs", wrap(ctx, (root, a) => runBrief(root, { type: a.type, situationId: a.situation_id, topic: a.topic, lang: a.lang }))));
|
|
175
|
+
server.registerTool("katman_generate", {
|
|
176
|
+
title: "Katman: generate files from research",
|
|
177
|
+
description: "Generates from research (single source of truth): robots.txt (a group per AI bot, app paths closed with trailing slashes, auth pages left to noindex), llms.txt, Organization / WebSite / SoftwareApplication / FAQPage / BreadcrumbList JSON-LD, the AI-channel analytics snippet, a server-side bot counter, the IndexNow key file. Returns content + target path. write: true writes static files into the site's static dir (never over an existing file without overwrite: true) and code/JSON-LD into .katman/generated/; refuses while [FILL] remains." + PRO_NOTE,
|
|
178
|
+
inputSchema: {
|
|
179
|
+
artifact: z.enum(ARTIFACTS).describe("What to generate"),
|
|
180
|
+
project_root: projectRoot,
|
|
181
|
+
options: generateOptions,
|
|
182
|
+
write: z.boolean().optional().describe("Write the file (static artifacts into the static dir; code and JSON-LD into .katman/generated/)"),
|
|
183
|
+
overwrite: z.boolean().optional().describe("Allow replacing an existing file (review the diff first)"),
|
|
184
|
+
},
|
|
185
|
+
annotations: { title: "Katman: generate", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
186
|
+
}, withPro(ctx, "generate", wrap(ctx, (root, a) => runGenerate(root, { artifact: a.artifact, options: a.options, write: a.write, overwrite: a.overwrite }))));
|
|
187
|
+
server.registerTool("katman_lint_content", {
|
|
188
|
+
title: "Katman: check a page's content",
|
|
189
|
+
description: "Checks one page against the Katman content rules before shipping: first 1–3 sentences answer (≤35 words, no preamble), key claim in the first ~200 chars, one sentence consistent with research + llms.txt, hype words, unsourced numbers, placeholders, \"free\" on a paid product, update date, FAQ 5–8, not-for and alternatives sections, ≥3 internal links, title/description length; comparison pages: sources + dates, own gaps, brand not first. Pass file (project path), url (live page, fetched without JS) or text. Read-only." + PRO_NOTE,
|
|
190
|
+
inputSchema: {
|
|
191
|
+
project_root: projectRoot,
|
|
192
|
+
file: z.string().optional().describe("Page source in the project (.md, .mdx, .html, .astro, .tsx, .jsx, .vue, .svelte, llms.txt), relative to project_root"),
|
|
193
|
+
url: z.string().optional().describe("A live page URL (fetched as bots see it, without JavaScript)"),
|
|
194
|
+
text: z.string().optional().describe("The page copy as Markdown/plain text"),
|
|
195
|
+
page_type: z.enum(PAGE_TYPES_LINT).optional().describe("Default: situation when it matches a research situation, else other"),
|
|
196
|
+
situation_id: z.string().optional().describe("Research situation id the page is written for"),
|
|
197
|
+
lang,
|
|
198
|
+
},
|
|
199
|
+
annotations: { title: "Katman: lint content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
200
|
+
}, withPro(ctx, "lint", wrap(ctx, (root, a) => runLint(root, { file: a.file, url: a.url, text: a.text, pageType: a.page_type, situationId: a.situation_id, lang: a.lang }))));
|
|
201
|
+
server.registerTool("katman_report", {
|
|
202
|
+
title: "Katman: report",
|
|
203
|
+
description: "Writes .katman/report.md, a shareable summary: research completeness, the latest audit's gate pass rates (Read / Quote / Fit), AI visibility rates with changes vs the previous comparable run, the competitors named instead and the cited domains (off-site targets), plan progress, the top 10 open issues and the next 7 days. Use after a measurement or at the end of a work session." + PRO_NOTE,
|
|
204
|
+
inputSchema: { project_root: projectRoot, lang },
|
|
205
|
+
annotations: { title: "Katman: report", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
206
|
+
}, withPro(ctx, "report", wrap(ctx, (root, a) => runReport(root, { lang: a.lang }))));
|
|
207
|
+
registerMethodPrompts(server);
|
|
208
|
+
registerMethodResources(server, ctx);
|
|
209
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Canonical tool calls the method module recommends as "next step". Kept in one place so argument names
|
|
2
|
+
// stay in sync with the crawl and visibility modules.
|
|
3
|
+
export const calls = {
|
|
4
|
+
start: (root, siteUrl) => ({ tool: "katman_start", args: { project_root: root, ...(siteUrl ? { site_url: siteUrl } : {}) } }),
|
|
5
|
+
researchTemplate: (root) => ({ tool: "katman_research", args: { project_root: root, mode: "template" } }),
|
|
6
|
+
researchShow: (root) => ({ tool: "katman_research", args: { project_root: root, mode: "show" } }),
|
|
7
|
+
researchUpdate: (root, data, extra = {}) => ({
|
|
8
|
+
tool: "katman_research",
|
|
9
|
+
args: { project_root: root, mode: "update", data, ...extra },
|
|
10
|
+
}),
|
|
11
|
+
generateQuestions: (root) => ({ tool: "katman_research", args: { project_root: root, mode: "update", generate_questions: true } }),
|
|
12
|
+
scan: (root) => ({ tool: "katman_scan_code", args: { project_root: root } }),
|
|
13
|
+
audit: (root, siteUrl) => ({ tool: "katman_audit_site", args: { project_root: root, site_url: siteUrl } }),
|
|
14
|
+
auditUrls: (root, urls) => ({ tool: "katman_audit_site", args: { project_root: root, urls } }),
|
|
15
|
+
visibilityDryRun: (root, extra = {}) => ({
|
|
16
|
+
tool: "katman_visibility_run",
|
|
17
|
+
args: { project_root: root, dry_run: true, ...extra },
|
|
18
|
+
}),
|
|
19
|
+
/** Without answer_text the tool returns the instructions and the questions to ask. */
|
|
20
|
+
manualCheck: (root, assistant = "chatgpt") => ({ tool: "katman_manual_check", args: { project_root: root, assistant } }),
|
|
21
|
+
plan: (root) => ({ tool: "katman_plan", args: { project_root: root } }),
|
|
22
|
+
prompt: (root, id, situationId) => ({
|
|
23
|
+
tool: "katman_prompt",
|
|
24
|
+
args: { project_root: root, id, ...(situationId ? { situation_id: situationId } : {}) },
|
|
25
|
+
}),
|
|
26
|
+
brief: (root, type, situationId) => ({
|
|
27
|
+
tool: "katman_page_brief",
|
|
28
|
+
args: { project_root: root, type, ...(situationId ? { situation_id: situationId } : {}) },
|
|
29
|
+
}),
|
|
30
|
+
generate: (root, artifact, options) => ({
|
|
31
|
+
tool: "katman_generate",
|
|
32
|
+
args: { project_root: root, artifact, ...(options ? { options } : {}) },
|
|
33
|
+
}),
|
|
34
|
+
lint: (root, args) => ({ tool: "katman_lint_content", args: { project_root: root, ...args } }),
|
|
35
|
+
indexnow: (root) => ({ tool: "katman_indexnow", args: { project_root: root, dry_run: true } }),
|
|
36
|
+
keywords: (seed) => ({ tool: "katman_keywords", args: { seed } }),
|
|
37
|
+
report: (root) => ({ tool: "katman_report", args: { project_root: root } }),
|
|
38
|
+
setupMonitoring: (root) => ({ tool: "katman_setup_monitoring", args: { project_root: root } }),
|
|
39
|
+
};
|
|
40
|
+
/** Inline form: `katman_audit_site` {"url":"…"} */
|
|
41
|
+
export function fmtCall(c) {
|
|
42
|
+
return `\`${c.tool}\` \`${JSON.stringify(c.args)}\``;
|
|
43
|
+
}
|
|
44
|
+
/** Block form for the "Next step" section. */
|
|
45
|
+
export function fmtCallBlock(c) {
|
|
46
|
+
return "```json\n" + JSON.stringify({ tool: c.tool, arguments: c.args }, null, 2) + "\n```";
|
|
47
|
+
}
|
|
48
|
+
/** Visibility panels (katman_visibility_run `models`), approximate cost per full question-set run. */
|
|
49
|
+
export const PANEL_COSTS = { economy: "≈$2.4", full: "≈$4.6", probe: "≈$1.8" };
|
|
50
|
+
export const PANEL_COST_TEXT = `default panel "economy" ${PANEL_COSTS.economy} per run; "full" ${PANEL_COSTS.full}; "probe" ${PANEL_COSTS.probe} — paid from the user's OpenRouter credits, shown as a dry-run estimate first`;
|
|
51
|
+
export function hasOpenRouterKey() {
|
|
52
|
+
return !!(process.env.OPENROUTER_API_KEY || process.env.KATMAN_OPENROUTER_API_KEY);
|
|
53
|
+
}
|