katman-mcp 0.1.0 → 0.2.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/README.md +43 -37
- package/dist/cli.js +27 -14
- package/dist/license.js +139 -95
- package/dist/meta.js +16 -6
- package/dist/plans.js +47 -0
- package/dist/tools/account/account.js +51 -30
- package/dist/tools/account/cli.js +4 -2
- package/dist/tools/account/index.js +7 -7
- package/dist/tools/account/sync.js +2 -2
- package/dist/tools/account/upsell.js +305 -117
- package/dist/tools/crawl/cli.js +1 -1
- package/dist/tools/crawl/index.js +16 -13
- package/dist/tools/method/core/start.js +1 -1
- package/dist/tools/method/index.js +27 -17
- package/dist/tools/method/mcp/prompts.js +6 -6
- package/dist/tools/method/mcp/resources.js +19 -16
- package/dist/tools/visibility/index.js +9 -9
- package/dist/tools/visibility/monitoring.js +3 -3
- package/package.json +1 -1
- package/server.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Crawl module: katman_scan_code, katman_audit_site, katman_indexnow
|
|
1
|
+
// Crawl module: katman_scan_code, katman_audit_site, katman_keywords (Audit Week or Pro) and katman_indexnow (Pro).
|
|
2
2
|
// Registration only; the engines live in scan/, audit/, indexnow.ts and keywords.ts (pure or fetcher-injected).
|
|
3
3
|
import { stat } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { resolveRoot, rootWarning } from "../../context.js";
|
|
8
8
|
import { loadConfig, loadResearch, logEvent, readJson, saveConfig, saveResearch, stamp, writeJson, writeText } from "../../state.js";
|
|
9
9
|
import { fail, ok } from "../../util.js";
|
|
10
|
-
import {
|
|
10
|
+
import { tierNote, withPlan } from "../../license.js";
|
|
11
11
|
import { topIssues, auditMarkdown } from "./audit/report.js";
|
|
12
12
|
import { runAudit } from "./audit/index.js";
|
|
13
13
|
import { countBySeverity } from "./common/findings.js";
|
|
@@ -87,13 +87,14 @@ function auditSummary(result, top, jsonPath) {
|
|
|
87
87
|
export function registerCrawlTools(server, ctx) {
|
|
88
88
|
server.registerTool("katman_scan_code", {
|
|
89
89
|
title: "Scan the codebase (framework, rendering, SEO files)",
|
|
90
|
-
description: "Read-only scan of the project's code (no network). Detects framework and builder (Lovable/Bolt/v0/Replit), the rendering verdict (SSR/SSG/CSR + evidence), hosting and preview-URL patterns, public vs app/auth routes, robots/sitemap/llms.txt/404 (static or generated), head-tag source, JSON-LD, i18n, analytics/AI-channel tracking, onClick navigation, hash routing and images without alt. Never opens .env*, keys or secret files. Writes .katman/scan.json; returns findings with gate, P-code and acceptance criterion. Run it first, and after big code changes."
|
|
90
|
+
description: "Read-only scan of the project's code (no network). Detects framework and builder (Lovable/Bolt/v0/Replit), the rendering verdict (SSR/SSG/CSR + evidence), hosting and preview-URL patterns, public vs app/auth routes, robots/sitemap/llms.txt/404 (static or generated), head-tag source, JSON-LD, i18n, analytics/AI-channel tracking, onClick navigation, hash routing and images without alt. Never opens .env*, keys or secret files. Writes .katman/scan.json; returns findings with gate, P-code and acceptance criterion. Run it first, and after big code changes." +
|
|
91
|
+
tierNote("scan"),
|
|
91
92
|
inputSchema: {
|
|
92
93
|
project_root: z.string().optional().describe("Absolute path of the website's codebase (default: KATMAN_PROJECT_ROOT, MCP roots, or cwd)."),
|
|
93
94
|
max_files: z.number().int().min(50).max(50000).optional().describe("Max files to list (default 5000)."),
|
|
94
95
|
},
|
|
95
96
|
annotations: { readOnlyHint: true, openWorldHint: false, destructiveHint: false, idempotentHint: true },
|
|
96
|
-
}, async ({ project_root, max_files }) => {
|
|
97
|
+
}, withPlan(ctx, "scan", async ({ project_root, max_files }) => {
|
|
97
98
|
try {
|
|
98
99
|
const root = await resolveRoot(ctx, project_root);
|
|
99
100
|
if (unsafeRoot(root))
|
|
@@ -115,10 +116,11 @@ export function registerCrawlTools(server, ctx) {
|
|
|
115
116
|
catch (e) {
|
|
116
117
|
return fail(`katman_scan_code failed: ${errText(e)}`);
|
|
117
118
|
}
|
|
118
|
-
});
|
|
119
|
+
}));
|
|
119
120
|
server.registerTool("katman_audit_site", {
|
|
120
121
|
title: "Audit the live site like AI crawlers see it",
|
|
121
|
-
description: "Fetches the live site without JavaScript, like AI crawlers: robots.txt, sitemap, http→https, www/apex, HSTS, compression, soft 404, llms.txt, AI-bot access through the CDN/firewall (GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot…), and per page: status, noindex, title/description, canonical, H1, JS-off words and first paragraph, JSON-LD, og:image, hreflang, internal links. Returns gates, top-10 issues (P-codes + acceptance criteria) and the P14 table. Network: the site only, ≤4 requests at a time. Pass urls to re-verify a fix. Writes .katman/audits/<stamp>.json + .katman/audit.md."
|
|
122
|
+
description: "Fetches the live site without JavaScript, like AI crawlers: robots.txt, sitemap, http→https, www/apex, HSTS, compression, soft 404, llms.txt, AI-bot access through the CDN/firewall (GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot…), and per page: status, noindex, title/description, canonical, H1, JS-off words and first paragraph, JSON-LD, og:image, hreflang, internal links. Returns gates, top-10 issues (P-codes + acceptance criteria) and the P14 table. Network: the site only, ≤4 requests at a time. Pass urls to re-verify a fix. Writes .katman/audits/<stamp>.json + .katman/audit.md." +
|
|
123
|
+
tierNote("audit"),
|
|
122
124
|
inputSchema: {
|
|
123
125
|
site_url: z.string().optional().describe("Site to audit, e.g. https://example.com (default: .katman/config.json siteUrl or research site.url)."),
|
|
124
126
|
project_root: z.string().optional().describe("Absolute project path (where .katman/ lives)."),
|
|
@@ -128,7 +130,7 @@ export function registerCrawlTools(server, ctx) {
|
|
|
128
130
|
preview_urls: z.array(z.string()).optional().describe("Preview/staging copies (e.g. https://proj.vercel.app) that must 301 to the main host or send noindex."),
|
|
129
131
|
},
|
|
130
132
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false, idempotentHint: true },
|
|
131
|
-
}, async (args, extra) => {
|
|
133
|
+
}, withPlan(ctx, "audit", async (args, extra) => {
|
|
132
134
|
try {
|
|
133
135
|
const root = await resolveRoot(ctx, args.project_root);
|
|
134
136
|
const token = extra?._meta?.progressToken;
|
|
@@ -144,10 +146,10 @@ export function registerCrawlTools(server, ctx) {
|
|
|
144
146
|
catch (e) {
|
|
145
147
|
return fail(`katman_audit_site failed: ${errText(e)}`);
|
|
146
148
|
}
|
|
147
|
-
});
|
|
149
|
+
}));
|
|
148
150
|
server.registerTool("katman_indexnow", {
|
|
149
151
|
title: "Notify search engines of new/changed pages (IndexNow)",
|
|
150
|
-
description: "Tells Bing (→ Copilot, ChatGPT search, DuckDuckGo), Yandex, Seznam, Naver and Yep about new or changed URLs via IndexNow. Creates a 32-hex key (saved in .katman/config.json), checks https://<host>/<key>.txt serves it, and selects only new/changed URLs (sitemap lastmod, else a hash of the page text) against .katman/indexnow.json. dry_run (default true) sends nothing; dry_run:false POSTs to api.indexnow.org and records the URLs. Always returns the manual checklist for Google Search Console, Bing Webmaster Tools and Brave." +
|
|
152
|
+
description: "Tells Bing (→ Copilot, ChatGPT search, DuckDuckGo), Yandex, Seznam, Naver and Yep about new or changed URLs via IndexNow. Creates a 32-hex key (saved in .katman/config.json), checks https://<host>/<key>.txt serves it, and selects only new/changed URLs (sitemap lastmod, else a hash of the page text) against .katman/indexnow.json. dry_run (default true) sends nothing; dry_run:false POSTs to api.indexnow.org and records the URLs. Always returns the manual checklist for Google Search Console, Bing Webmaster Tools and Brave." + tierNote("indexnow"),
|
|
151
153
|
inputSchema: {
|
|
152
154
|
project_root: z.string().optional().describe("Absolute project path (where .katman/ lives)."),
|
|
153
155
|
site_url: z.string().optional().describe("Site, e.g. https://example.com (default: config siteUrl)."),
|
|
@@ -156,7 +158,7 @@ export function registerCrawlTools(server, ctx) {
|
|
|
156
158
|
dry_run: z.boolean().optional().describe("Default true: plan only. false = submit to api.indexnow.org."),
|
|
157
159
|
},
|
|
158
160
|
annotations: { readOnlyHint: false, openWorldHint: true, destructiveHint: false, idempotentHint: false },
|
|
159
|
-
},
|
|
161
|
+
}, withPlan(ctx, "indexnow", async ({ project_root, site_url, urls, key, dry_run }) => {
|
|
160
162
|
try {
|
|
161
163
|
const root = await resolveRoot(ctx, project_root);
|
|
162
164
|
if (unsafeRoot(root))
|
|
@@ -185,7 +187,8 @@ export function registerCrawlTools(server, ctx) {
|
|
|
185
187
|
}));
|
|
186
188
|
server.registerTool("katman_keywords", {
|
|
187
189
|
title: "Google autocomplete keyword ideas",
|
|
188
|
-
description: "Expands a seed phrase with Google autocomplete (seed + a–z/0–9; ≤40 requests, 350 ms apart; network: suggestqueries.google.com only) into real search phrases, clustered, and marks single-question, situation and comparison page candidates (English and Turkish question words). Use during research to find the customer's own words. save:true merges them into .katman/research.json keywords (source \"autocomplete\"); otherwise nothing is written."
|
|
190
|
+
description: "Expands a seed phrase with Google autocomplete (seed + a–z/0–9; ≤40 requests, 350 ms apart; network: suggestqueries.google.com only) into real search phrases, clustered, and marks single-question, situation and comparison page candidates (English and Turkish question words). Use during research to find the customer's own words. save:true merges them into .katman/research.json keywords (source \"autocomplete\"); otherwise nothing is written." +
|
|
191
|
+
tierNote("keywords"),
|
|
189
192
|
inputSchema: {
|
|
190
193
|
seed: z.string().min(2).describe("Seed phrase in the customer's words, e.g. \"import invoices into accounting\"."),
|
|
191
194
|
lang: z.string().optional().describe("Interface language (hl), default en; tr uses the Turkish alphabet."),
|
|
@@ -195,7 +198,7 @@ export function registerCrawlTools(server, ctx) {
|
|
|
195
198
|
project_root: z.string().optional().describe("Absolute project path (needed with save)."),
|
|
196
199
|
},
|
|
197
200
|
annotations: { readOnlyHint: false, openWorldHint: true, destructiveHint: false, idempotentHint: true },
|
|
198
|
-
}, async ({ seed, lang, country, expand, save, project_root }) => {
|
|
201
|
+
}, withPlan(ctx, "keywords", async ({ seed, lang, country, expand, save, project_root }) => {
|
|
199
202
|
try {
|
|
200
203
|
const r = await runKeywords({ seed, lang, country, expand });
|
|
201
204
|
let saved;
|
|
@@ -222,5 +225,5 @@ export function registerCrawlTools(server, ctx) {
|
|
|
222
225
|
catch (e) {
|
|
223
226
|
return fail(`katman_keywords failed: ${errText(e)}`);
|
|
224
227
|
}
|
|
225
|
-
});
|
|
228
|
+
}));
|
|
226
229
|
}
|
|
@@ -17,7 +17,7 @@ export function firstRunText(root) {
|
|
|
17
17
|
"**First run in this project. What happens next:**",
|
|
18
18
|
`1. Katman keeps its notes in \`${root}/.katman/\` (safe to commit, never secrets). It writes nowhere else unless you call katman_generate or katman_setup_monitoring with write: true, and it never overwrites without overwrite: true.`,
|
|
19
19
|
"2. It reads your code (skipping node_modules, .git and build output; never .env*, keys or secrets) and, when you run the audit tools, your own live site.",
|
|
20
|
-
"3. Network only when a tool needs it: your site, openrouter.ai with YOUR key after a cost estimate (dry run first), api.indexnow.org on request, and katman.pro only to check KATMAN_KEY (at most once a day,
|
|
20
|
+
"3. Network only when a tool needs it: your site, openrouter.ai with YOUR key after a cost estimate (dry run first), api.indexnow.org on request, and katman.pro only to check KATMAN_KEY (at most once a day, and again when a plan ends) or when you call katman_sync. No telemetry.",
|
|
21
21
|
"4. The order: research (who you are, your customers' own sentences) → plan → build with prompts P0–P14 → verify, then re-measure every 14 days.",
|
|
22
22
|
"5. It never invents facts: unknowns stay [FILL] and you'll be asked.",
|
|
23
23
|
].join("\n");
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// Method module: the Katman book as tools (katman_start, katman_research, katman_plan, katman_prompt,
|
|
2
2
|
// katman_page_brief, katman_generate, katman_lint_content, katman_report), MCP prompts and resources.
|
|
3
|
+
// Every tool needs a plan (withPlan); katman_start without one explains Katman and the plans instead of running.
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { resolveRoot, rootWarning } from "../../context.js";
|
|
5
6
|
import { fail, ok } from "../../util.js";
|
|
6
|
-
import {
|
|
7
|
+
import { startLicense, tierNote, withPlan } from "../../license.js";
|
|
8
|
+
import { startIntro, upsellStructured } from "../account/upsell.js";
|
|
7
9
|
import { hasOpenRouterKey } from "./lib/calls.js";
|
|
10
|
+
import { loadConfigSafe } from "./lib/state.js";
|
|
8
11
|
import { runStart } from "./core/start.js";
|
|
9
12
|
import { runResearch } from "./core/research-run.js";
|
|
10
13
|
import { runPlan } from "./core/plan.js";
|
|
@@ -113,7 +116,8 @@ function wrap(ctx, fn, opts = {}) {
|
|
|
113
116
|
export function registerMethodTools(server, ctx) {
|
|
114
117
|
server.registerTool("katman_start", {
|
|
115
118
|
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."
|
|
119
|
+
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. Without a plan it only explains Katman, the plans and how to set the key." +
|
|
120
|
+
tierNote("start"),
|
|
117
121
|
inputSchema: {
|
|
118
122
|
project_root: projectRoot,
|
|
119
123
|
site_url: z.string().optional().describe("The live site URL, e.g. https://example.com (saved to .katman/config.json)"),
|
|
@@ -122,8 +126,13 @@ export function registerMethodTools(server, ctx) {
|
|
|
122
126
|
annotations: { title: "Katman: start here", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
123
127
|
}, async (args) => {
|
|
124
128
|
try {
|
|
129
|
+
const license = await startLicense(ctx);
|
|
125
130
|
const root = await resolveRoot(ctx, args.project_root);
|
|
126
|
-
|
|
131
|
+
if (!license.allowed) {
|
|
132
|
+
const intro = startIntro(license.status, args.lang ?? (await loadConfigSafe(root)).lang);
|
|
133
|
+
return ok(intro, upsellStructured("katman_start", "start", license.status));
|
|
134
|
+
}
|
|
135
|
+
const out = await runStart(root, { siteUrl: args.site_url, hasOpenRouterKey: hasOpenRouterKey(), warning: rootWarning(root), lang: args.lang, license });
|
|
127
136
|
return out.isError ? fail(out.text) : ok(out.text, out.structured);
|
|
128
137
|
}
|
|
129
138
|
catch (e) {
|
|
@@ -132,7 +141,8 @@ export function registerMethodTools(server, ctx) {
|
|
|
132
141
|
});
|
|
133
142
|
server.registerTool("katman_research", {
|
|
134
143
|
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."
|
|
144
|
+
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." +
|
|
145
|
+
tierNote("research"),
|
|
136
146
|
inputSchema: {
|
|
137
147
|
project_root: projectRoot,
|
|
138
148
|
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)"),
|
|
@@ -142,16 +152,16 @@ export function registerMethodTools(server, ctx) {
|
|
|
142
152
|
lang,
|
|
143
153
|
},
|
|
144
154
|
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 })));
|
|
155
|
+
}, withPlan(ctx, "research", wrap(ctx, (root, a) => runResearch(root, { mode: a.mode, data: a.data, replaceLists: a.replace_lists, generateQuestions: a.generate_questions, lang: a.lang }))));
|
|
146
156
|
server.registerTool("katman_plan", {
|
|
147
157
|
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." +
|
|
158
|
+
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." + tierNote("plan"),
|
|
149
159
|
inputSchema: { project_root: projectRoot, lang },
|
|
150
160
|
annotations: { title: "Katman: plan", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
151
|
-
},
|
|
161
|
+
}, withPlan(ctx, "plan", wrap(ctx, (root, a) => runPlan(root, { lang: a.lang }))));
|
|
152
162
|
server.registerTool("katman_prompt", {
|
|
153
163
|
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." +
|
|
164
|
+
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." + tierNote("prompts"),
|
|
155
165
|
inputSchema: {
|
|
156
166
|
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
167
|
project_root: projectRoot,
|
|
@@ -159,10 +169,10 @@ export function registerMethodTools(server, ctx) {
|
|
|
159
169
|
lang,
|
|
160
170
|
},
|
|
161
171
|
annotations: { title: "Katman: prompt", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
162
|
-
},
|
|
172
|
+
}, withPlan(ctx, "prompts", wrap(ctx, (root, a) => runPrompt(root, { id: a.id, situationId: a.situation_id, lang: a.lang }))));
|
|
163
173
|
server.registerTool("katman_page_brief", {
|
|
164
174
|
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." +
|
|
175
|
+
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." + tierNote("briefs"),
|
|
166
176
|
inputSchema: {
|
|
167
177
|
type: z.enum(BRIEF_TYPES).describe("Page type"),
|
|
168
178
|
project_root: projectRoot,
|
|
@@ -171,10 +181,10 @@ export function registerMethodTools(server, ctx) {
|
|
|
171
181
|
lang,
|
|
172
182
|
},
|
|
173
183
|
annotations: { title: "Katman: page brief", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
174
|
-
},
|
|
184
|
+
}, withPlan(ctx, "briefs", wrap(ctx, (root, a) => runBrief(root, { type: a.type, situationId: a.situation_id, topic: a.topic, lang: a.lang }))));
|
|
175
185
|
server.registerTool("katman_generate", {
|
|
176
186
|
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." +
|
|
187
|
+
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." + tierNote("generate"),
|
|
178
188
|
inputSchema: {
|
|
179
189
|
artifact: z.enum(ARTIFACTS).describe("What to generate"),
|
|
180
190
|
project_root: projectRoot,
|
|
@@ -183,10 +193,10 @@ export function registerMethodTools(server, ctx) {
|
|
|
183
193
|
overwrite: z.boolean().optional().describe("Allow replacing an existing file (review the diff first)"),
|
|
184
194
|
},
|
|
185
195
|
annotations: { title: "Katman: generate", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
186
|
-
},
|
|
196
|
+
}, withPlan(ctx, "generate", wrap(ctx, (root, a) => runGenerate(root, { artifact: a.artifact, options: a.options, write: a.write, overwrite: a.overwrite }))));
|
|
187
197
|
server.registerTool("katman_lint_content", {
|
|
188
198
|
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." +
|
|
199
|
+
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." + tierNote("lint"),
|
|
190
200
|
inputSchema: {
|
|
191
201
|
project_root: projectRoot,
|
|
192
202
|
file: z.string().optional().describe("Page source in the project (.md, .mdx, .html, .astro, .tsx, .jsx, .vue, .svelte, llms.txt), relative to project_root"),
|
|
@@ -197,13 +207,13 @@ export function registerMethodTools(server, ctx) {
|
|
|
197
207
|
lang,
|
|
198
208
|
},
|
|
199
209
|
annotations: { title: "Katman: lint content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
200
|
-
},
|
|
210
|
+
}, withPlan(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
211
|
server.registerTool("katman_report", {
|
|
202
212
|
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." +
|
|
213
|
+
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." + tierNote("report"),
|
|
204
214
|
inputSchema: { project_root: projectRoot, lang },
|
|
205
215
|
annotations: { title: "Katman: report", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
206
|
-
},
|
|
216
|
+
}, withPlan(ctx, "report", wrap(ctx, (root, a) => runReport(root, { lang: a.lang }))));
|
|
207
217
|
registerMethodPrompts(server);
|
|
208
218
|
registerMethodResources(server, ctx);
|
|
209
219
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// MCP prompts (slash commands): katman-start, katman-audit, katman-situation-page, katman-measure.
|
|
2
|
-
// Each returns user-role messages that walk the agent through the tools step by step.
|
|
2
|
+
// Each returns user-role messages that walk the agent through the tools step by step. None of them carries the text
|
|
3
|
+
// of the prompt book P0–P14 (Katman Pro content, served by katman_prompt): they only call tools, and every tool
|
|
4
|
+
// checks the plan itself.
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
const user = (text) => ({ role: "user", content: { type: "text", text } });
|
|
5
7
|
const RULES = `Rules (Katman method):
|
|
@@ -22,14 +24,12 @@ Do this now:
|
|
|
22
24
|
${RULES}`;
|
|
23
25
|
}
|
|
24
26
|
export function auditPromptText(siteUrl) {
|
|
25
|
-
return `Audit this site the way AI crawlers see it
|
|
27
|
+
return `Audit this site the way AI crawlers see it. REPORT ONLY: do not create, modify or delete any file in the project.
|
|
26
28
|
|
|
27
29
|
1. Call katman_start (project_root = the absolute path of this workspace${siteUrl ? `, site_url = "${siteUrl}"` : ""}) to load the state and the site URL. If no site URL is known, ask me for it.
|
|
28
30
|
2. Call katman_scan_code.
|
|
29
31
|
3. Call katman_audit_site with the site URL.
|
|
30
|
-
4. From their output,
|
|
31
|
-
5. Answer: do robots.txt and sitemap.xml exist and how are they generated? Does /this-page-does-not-exist return 404? Are links <a href> or onClick? Is the site also live at a preview/staging address? Do GPTBot, OAI-SearchBot, ClaudeBot and PerplexityBot get 200?
|
|
32
|
-
6. Finish with the 10 most important problems ranked by traffic impact, each with the file that would change and the Katman prompt (P-code) that fixes it.
|
|
32
|
+
4. From their output, give me the 10 most important problems ranked by traffic impact, each with the file that would change, the P-code Katman gives for it and its acceptance criterion.
|
|
33
33
|
|
|
34
34
|
Don't fix anything yet. When I approve, call katman_plan.`;
|
|
35
35
|
}
|
|
@@ -63,7 +63,7 @@ export function registerMethodPrompts(server) {
|
|
|
63
63
|
argsSchema: { site_url: z.string().optional().describe("The live site URL, if known (e.g. https://example.com)") },
|
|
64
64
|
}, ({ site_url }) => ({ messages: [user(startPromptText(site_url))] }));
|
|
65
65
|
server.registerPrompt("katman-audit", {
|
|
66
|
-
title: "Katman: audit (
|
|
66
|
+
title: "Katman: audit (report only)",
|
|
67
67
|
description: "Audit the site like AI crawlers see it — codebase scan + live audit — and report the top 10 problems without changing code.",
|
|
68
68
|
argsSchema: { site_url: z.string().optional().describe("The live site URL, if known") },
|
|
69
69
|
}, ({ site_url }) => ({ messages: [user(auditPromptText(site_url))] }));
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// MCP resources: the method, checklists, content rules, raw prompts and the current project's research.md.
|
|
2
|
+
// Each needs a plan like the tools do: the method texts need "start" (Audit Week or Pro), research.md "research",
|
|
3
|
+
// the prompt bodies "prompts" (Katman Pro). The resource list itself stays visible.
|
|
2
4
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { resolveRoot } from "../../../context.js";
|
|
4
6
|
import { PROMPTS, getPrompt } from "../data/prompts.js";
|
|
5
7
|
import { contentRulesMd, methodOverviewMd, prepublishChecklistMd, rawPromptMd, siteAuditChecklistMd } from "../data/render.js";
|
|
6
8
|
import { loadConfigSafe, loadResearchSafe, loadRuns } from "../lib/state.js";
|
|
7
9
|
import { renderResearchMd } from "../core/research.js";
|
|
8
|
-
import { licenseOf,
|
|
10
|
+
import { licenseOf, requireFeature } from "../../../license.js";
|
|
11
|
+
import { resourcePaywall } from "../../account/upsell.js";
|
|
9
12
|
const MD = "text/markdown";
|
|
10
13
|
function md(uri, text) {
|
|
11
14
|
return { contents: [{ uri: uri.href, mimeType: MD, text }] };
|
|
@@ -18,11 +21,17 @@ export async function researchResourceText(root) {
|
|
|
18
21
|
const [runs, config] = await Promise.all([loadRuns(root), loadConfigSafe(root)]);
|
|
19
22
|
return renderResearchMd(research, runs, config.lang ?? "en");
|
|
20
23
|
}
|
|
24
|
+
/** The resource text when the plan includes the feature, else a short paywall under the same title. */
|
|
25
|
+
async function gated(ctx, uri, feature, title, then, text) {
|
|
26
|
+
const gate = await requireFeature(feature, licenseOf(ctx));
|
|
27
|
+
return md(uri, gate.allowed ? await text() : resourcePaywall(title, feature, gate.status, then));
|
|
28
|
+
}
|
|
29
|
+
const AGAIN = "Then read this resource again.";
|
|
21
30
|
export function registerMethodResources(server, ctx) {
|
|
22
|
-
server.registerResource("katman-method", "katman://method", { title: "The Katman method", description: "3 gates (Read / Quote / Fit), 4 steps, the evidence, the 6 traps, the prompt book P0–P14, the first 30 days.", mimeType: MD }, async (uri) =>
|
|
23
|
-
server.registerResource("katman-checklist-prepublish", "katman://checklist/prepublish", { title: "Pre-publish checklist", description: "The one-page checklist before publishing (book ch. 19), each line with the Katman check that proves it.", mimeType: MD }, async (uri) =>
|
|
24
|
-
server.registerResource("katman-checklist-site-audit", "katman://checklist/site-audit", { title: "Full site audit checklist", description: "Per-URL and site-wide checks (book appendix E) that katman_audit_site runs.", mimeType: MD }, async (uri) =>
|
|
25
|
-
server.registerResource("katman-content-rules", "katman://content-rules", { title: "Content rules", description: "Content rules (appendix D), how content is produced without spam, the 11-block situation page anatomy, and the words katman_lint_content flags.", mimeType: MD }, async (uri) =>
|
|
31
|
+
server.registerResource("katman-method", "katman://method", { title: "The Katman method", description: "3 gates (Read / Quote / Fit), 4 steps, the evidence, the 6 traps, the prompt book P0–P14, the first 30 days.", mimeType: MD }, async (uri) => gated(ctx, uri, "start", "The Katman method", AGAIN, () => methodOverviewMd("en")));
|
|
32
|
+
server.registerResource("katman-checklist-prepublish", "katman://checklist/prepublish", { title: "Pre-publish checklist", description: "The one-page checklist before publishing (book ch. 19), each line with the Katman check that proves it.", mimeType: MD }, async (uri) => gated(ctx, uri, "start", "Pre-publish checklist", AGAIN, () => prepublishChecklistMd("en")));
|
|
33
|
+
server.registerResource("katman-checklist-site-audit", "katman://checklist/site-audit", { title: "Full site audit checklist", description: "Per-URL and site-wide checks (book appendix E) that katman_audit_site runs.", mimeType: MD }, async (uri) => gated(ctx, uri, "start", "Full site audit checklist", AGAIN, () => siteAuditChecklistMd("en")));
|
|
34
|
+
server.registerResource("katman-content-rules", "katman://content-rules", { title: "Content rules", description: "Content rules (appendix D), how content is produced without spam, the 11-block situation page anatomy, and the words katman_lint_content flags.", mimeType: MD }, async (uri) => gated(ctx, uri, "start", "Content rules", AGAIN, () => contentRulesMd("en")));
|
|
26
35
|
server.registerResource("katman-prompt", new ResourceTemplate("katman://prompts/{id}", {
|
|
27
36
|
list: async () => ({
|
|
28
37
|
resources: PROMPTS.map((p) => ({ uri: `katman://prompts/${p.id}`, name: `${p.id} ${p.title.en}`, title: `${p.id} · ${p.title.en}`, mimeType: MD })),
|
|
@@ -35,16 +44,10 @@ export function registerMethodResources(server, ctx) {
|
|
|
35
44
|
const p = getPrompt(String(raw ?? ""));
|
|
36
45
|
if (!p)
|
|
37
46
|
return md(uri, `Unknown prompt "${raw}". Valid ids: ${PROMPTS.map((x) => x.id).join(", ")}.`);
|
|
38
|
-
// The prompt book is Pro content, like the katman_prompt tool: titles
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
return md(uri, `# ${p.id} · ${p.title.en}\n\nThe prompt text is part of Katman Pro (needs KATMAN_KEY). Start the 7-day free trial at https://katman.pro/app, add the key to your MCP config as KATMAN_KEY, then read this resource again or call katman_prompt {"id": "${p.id}"} to get it filled with this project's data.`);
|
|
43
|
-
}
|
|
44
|
-
return md(uri, rawPromptMd(p, "en"));
|
|
45
|
-
});
|
|
46
|
-
server.registerResource("katman-research", "katman://research", { title: "This project's research.md", description: "The current project's research (brand, one sentence, not-X, situations, competitors, proof, question set, latest measurement), rendered from .katman/research.json.", mimeType: MD }, async (uri) => {
|
|
47
|
-
const root = await resolveRoot(ctx);
|
|
48
|
-
return md(uri, await researchResourceText(root));
|
|
47
|
+
// The prompt book is Katman Pro content, like the katman_prompt tool: the titles stay listed, the prompt bodies
|
|
48
|
+
// need Pro (the Audit Week doesn't include them).
|
|
49
|
+
const then = `Then read this resource again, or call katman_prompt {"id": "${p.id}"} to get it filled with this project's data.`;
|
|
50
|
+
return gated(ctx, uri, "prompts", `${p.id} · ${p.title.en}`, then, () => rawPromptMd(p, "en"));
|
|
49
51
|
});
|
|
52
|
+
server.registerResource("katman-research", "katman://research", { title: "This project's research.md", description: "The current project's research (brand, one sentence, not-X, situations, competitors, proof, question set, latest measurement), rendered from .katman/research.json.", mimeType: MD }, async (uri) => gated(ctx, uri, "research", "research.md", AGAIN, async () => researchResourceText(await resolveRoot(ctx))));
|
|
50
53
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Visibility module: measures whether AI assistants name, recommend and cite the brand.
|
|
2
|
-
// Tools: katman_visibility_run (OpenRouter, user's key, dry run first)
|
|
3
|
-
//
|
|
2
|
+
// Tools: katman_visibility_run (OpenRouter, user's key, dry run first) and katman_manual_check (no API cost: the user
|
|
3
|
+
// pastes consumer-app answers), both Audit Week or Pro; katman_setup_monitoring (GitHub Actions, Pro).
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { resolveRoot, rootWarning } from "../../context.js";
|
|
6
6
|
import { fail, ok } from "../../util.js";
|
|
7
|
-
import {
|
|
7
|
+
import { tierNote, withPlan } from "../../license.js";
|
|
8
8
|
import { ASSISTANTS, manualCheck } from "./manual.js";
|
|
9
9
|
import { setupMonitoring } from "./monitoring.js";
|
|
10
10
|
import { redact } from "./openrouter.js";
|
|
@@ -22,7 +22,7 @@ function crash(tool, e) {
|
|
|
22
22
|
export function registerVisibilityTools(server, ctx) {
|
|
23
23
|
server.registerTool("katman_visibility_run", {
|
|
24
24
|
title: "Measure AI visibility (ChatGPT, Claude, Gemini, Perplexity, Grok)",
|
|
25
|
-
description: "Asks the research question set (12 unbranded + 2 diagnostic) to ChatGPT, Claude, Gemini, Perplexity and Grok via OpenRouter with live web search and the user's own key, then records who is named, recommended first and cited. SPENDS THE USER'S CREDITS: always call with dry_run: true first (the default), show the estimate, and get the user's explicit yes before dry_run: false. Default panel \"economy\" (5 assistants, smaller models, ≈$1–4 per run); \"full\" ≈$2–7; \"probe\" ≈$1–3. Refuses to start above max_cost_usd (default 3). A full run takes minutes. API answers are a proxy for the consumer apps. Saves .katman/runs/ and measurements.md." +
|
|
25
|
+
description: "Asks the research question set (12 unbranded + 2 diagnostic) to ChatGPT, Claude, Gemini, Perplexity and Grok via OpenRouter with live web search and the user's own key, then records who is named, recommended first and cited. SPENDS THE USER'S CREDITS: always call with dry_run: true first (the default), show the estimate, and get the user's explicit yes before dry_run: false. Default panel \"economy\" (5 assistants, smaller models, ≈$1–4 per run); \"full\" ≈$2–7; \"probe\" ≈$1–3. Refuses to start above max_cost_usd (default 3). A full run takes minutes. API answers are a proxy for the consumer apps. Saves .katman/runs/ and measurements.md." + tierNote("visibility"),
|
|
26
26
|
inputSchema: {
|
|
27
27
|
project_root: projectRoot,
|
|
28
28
|
dry_run: z.boolean().optional().describe("Default true: estimate only, nothing is spent. false = real run, only after the user agreed to the estimate."),
|
|
@@ -40,7 +40,7 @@ export function registerVisibilityTools(server, ctx) {
|
|
|
40
40
|
allow_google_grounding: z.boolean().optional().describe("Gemini uses Exa search by default. true = Google's own grounding; Google's terms restrict storing and analysing grounded results, and the user is responsible."),
|
|
41
41
|
},
|
|
42
42
|
annotations: { title: "Measure AI visibility", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
43
|
-
},
|
|
43
|
+
}, withPlan(ctx, "visibility", async (args, extra) => {
|
|
44
44
|
let root = "";
|
|
45
45
|
try {
|
|
46
46
|
root = await resolveRoot(ctx, args.project_root);
|
|
@@ -60,7 +60,7 @@ export function registerVisibilityTools(server, ctx) {
|
|
|
60
60
|
}));
|
|
61
61
|
server.registerTool("katman_manual_check", {
|
|
62
62
|
title: "Record a consumer-app answer (no API cost)",
|
|
63
|
-
description: "No API cost, terms-safe measurement: the user asks ChatGPT, Claude, Gemini, Perplexity, Copilot, Grok or Google AI Mode in a private, logged-out window with web search on and pastes the answer. Without answer_text: returns exact instructions and the questions to ask (verbatim). With answer_text (+ question_id or question, sources): detects whether the brand is named, its position, who was recommended, competitors and cited pages, saves a manual run in .katman/runs/ and updates measurements.md. Makes one tiny judge call only if an OpenRouter key is set." +
|
|
63
|
+
description: "No API cost, terms-safe measurement: the user asks ChatGPT, Claude, Gemini, Perplexity, Copilot, Grok or Google AI Mode in a private, logged-out window with web search on and pastes the answer. Without answer_text: returns exact instructions and the questions to ask (verbatim). With answer_text (+ question_id or question, sources): detects whether the brand is named, its position, who was recommended, competitors and cited pages, saves a manual run in .katman/runs/ and updates measurements.md. Makes one tiny judge call only if an OpenRouter key is set." + tierNote("manual_check"),
|
|
64
64
|
inputSchema: {
|
|
65
65
|
project_root: projectRoot,
|
|
66
66
|
assistant: z.enum(ASSISTANTS).describe("The consumer app the user asked."),
|
|
@@ -72,7 +72,7 @@ export function registerVisibilityTools(server, ctx) {
|
|
|
72
72
|
web_search: z.boolean().optional().describe("Was web search on? Default true."),
|
|
73
73
|
},
|
|
74
74
|
annotations: { title: "Record a consumer-app answer", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
75
|
-
},
|
|
75
|
+
}, withPlan(ctx, "manual_check", async (args) => {
|
|
76
76
|
let root = "";
|
|
77
77
|
try {
|
|
78
78
|
root = await resolveRoot(ctx, args.project_root);
|
|
@@ -84,7 +84,7 @@ export function registerVisibilityTools(server, ctx) {
|
|
|
84
84
|
}));
|
|
85
85
|
server.registerTool("katman_setup_monitoring", {
|
|
86
86
|
title: "Schedule AI visibility checks (GitHub Actions)",
|
|
87
|
-
description: "Generates a GitHub Actions workflow that re-runs the visibility check (and optionally the live site audit) on a schedule with the user's OPENROUTER_API_KEY repository secret, commits .katman/ changes and opens an issue when the mention rate drops. Returns the YAML, the estimated cost per run and month, and the steps to add the secret and site variable. Makes no network calls. write: true writes .github/workflows/katman.yml (refuses to replace an existing file unless overwrite: true)." +
|
|
87
|
+
description: "Generates a GitHub Actions workflow that re-runs the visibility check (and optionally the live site audit) on a schedule with the user's OPENROUTER_API_KEY repository secret, commits .katman/ changes and opens an issue when the mention rate drops. Returns the YAML, the estimated cost per run and month, and the steps to add the secret and site variable. Makes no network calls. write: true writes .github/workflows/katman.yml (refuses to replace an existing file unless overwrite: true)." + tierNote("monitoring"),
|
|
88
88
|
inputSchema: {
|
|
89
89
|
project_root: projectRoot,
|
|
90
90
|
schedule: z.string().optional().describe('Cron (UTC), default "0 6 * * 1" (Mondays 06:00). The book re-measures every 14 days: "0 6 1,15 * *".'),
|
|
@@ -95,7 +95,7 @@ export function registerVisibilityTools(server, ctx) {
|
|
|
95
95
|
models: z.string().optional().describe('Preset or comma list for the scheduled check, e.g. "economy" or "probe".'),
|
|
96
96
|
},
|
|
97
97
|
annotations: { title: "Schedule AI visibility checks", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
98
|
-
},
|
|
98
|
+
}, withPlan(ctx, "monitoring", async (args) => {
|
|
99
99
|
let root = "";
|
|
100
100
|
try {
|
|
101
101
|
root = await resolveRoot(ctx, args.project_root);
|
|
@@ -35,7 +35,7 @@ export function buildWorkflowYaml(o) {
|
|
|
35
35
|
"# Katman AI visibility monitoring (generated by katman-mcp; safe to edit).",
|
|
36
36
|
"# Re-asks your question set through OpenRouter with YOUR key (capped per run), commits .katman/",
|
|
37
37
|
`# and opens an issue when the share of answers that name you drops by ${DROP_POINTS}+ points.`,
|
|
38
|
-
|
|
38
|
+
`# \`katman-mcp check\`${o.includeAudit ? " and `katman-mcp audit` need" : " needs"} the KATMAN_KEY repository secret (a Katman key with an active plan).`,
|
|
39
39
|
"name: Katman AI visibility",
|
|
40
40
|
"",
|
|
41
41
|
"on:",
|
|
@@ -72,7 +72,7 @@ export function buildWorkflowYaml(o) {
|
|
|
72
72
|
y.push("", " - name: Live site audit (as AI crawlers see it)");
|
|
73
73
|
if (!site)
|
|
74
74
|
y.push(" if: ${{ vars.KATMAN_SITE_URL != '' }}");
|
|
75
|
-
y.push(" continue-on-error: true", " env:", site ? ` KATMAN_SITE_URL: \${{ vars.KATMAN_SITE_URL || '${site}' }}` : " KATMAN_SITE_URL: ${{ vars.KATMAN_SITE_URL }}", ' run: npx -y katman-mcp audit "$KATMAN_SITE_URL" --root .');
|
|
75
|
+
y.push(" continue-on-error: true", " env:", " KATMAN_KEY: ${{ secrets.KATMAN_KEY }}", site ? ` KATMAN_SITE_URL: \${{ vars.KATMAN_SITE_URL || '${site}' }}` : " KATMAN_SITE_URL: ${{ vars.KATMAN_SITE_URL }}", ' run: npx -y katman-mcp audit "$KATMAN_SITE_URL" --root .');
|
|
76
76
|
}
|
|
77
77
|
y.push("", " - name: Commit .katman changes", " run: |", ' git config user.name "katman-bot"', ' git config user.email "41898282+github-actions[bot]@users.noreply.github.com"', " git add -A .katman ':(exclude).katman/cache'", " if git diff --cached --quiet; then", ' echo "No .katman changes."', " else", ' git commit -m "katman: AI visibility check $(date -u +%F)"', " git pull --rebase --autostash", " git push", " fi", "", " - name: Compare the last two runs", " if: always()", " id: drop", " run: |", " node <<'EOF'", " const fs = require('fs');", " const path = require('path');", " const dir = '.katman/runs';", " const out = (k, v) => fs.appendFileSync(process.env.GITHUB_OUTPUT, `${k}=${v}\\n`);", " let files = [];", " try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('manual-')).sort(); } catch {}", " const runs = files", " .map((f) => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch { return null; } })", " .filter((r) => r && r.summary && r.summary.answered >= 5);", " if (runs.length < 2) { out('dropped', 'false'); process.exit(0); }", " const [prev, cur] = runs.slice(-2);", " const before = Math.round(prev.summary.mentionRate * 100);", " const after = Math.round(cur.summary.mentionRate * 100);", " out('before', before);", " out('after', after);", " out('run', cur.id);", " // Only a run made by this workflow run counts; otherwise the same drop would open an issue every week.", " const fresh = Date.now() - Date.parse(cur.ts) < 36 * 3600 * 1000;", ` out('dropped', String(fresh && before - after >= ${DROP_POINTS}));`, " EOF", "", " - name: Open an issue when the mention rate dropped", " if: always() && steps.drop.outputs.dropped == 'true'", " env:", " GH_TOKEN: ${{ github.token }}", " BEFORE: ${{ steps.drop.outputs.before }}", " AFTER: ${{ steps.drop.outputs.after }}", " RUN_ID: ${{ steps.drop.outputs.run }}", " run: |", " cat > katman-issue.md <<EOF", " AI assistants named you in ${AFTER}% of the unbranded answers in run ${RUN_ID}, down from ${BEFORE}% in the run before.", "", " - Who was recommended instead and which sources the assistants used: .katman/measurements.md", " - One run is a small sample; check whether the drop holds in the next run before changing course.", " - Confirm key questions by hand with katman_manual_check (logged out, web search on).", " EOF", ' gh issue create --title "Katman: AI assistants name you less often (${BEFORE}% → ${AFTER}%)" --body-file katman-issue.md');
|
|
78
78
|
return y.join("\n") + "\n";
|
|
@@ -143,7 +143,7 @@ export async function setupMonitoring(root, args) {
|
|
|
143
143
|
"",
|
|
144
144
|
"## Setup steps",
|
|
145
145
|
"",
|
|
146
|
-
"1. Add your keys as repository secrets: GitHub → Settings → Secrets and variables → Actions → New repository secret, name `OPENROUTER_API_KEY` (or `gh secret set OPENROUTER_API_KEY`), and `KATMAN_KEY` with your Katman
|
|
146
|
+
"1. Add your keys as repository secrets: GitHub → Settings → Secrets and variables → Actions → New repository secret, name `OPENROUTER_API_KEY` (or `gh secret set OPENROUTER_API_KEY`), and `KATMAN_KEY` with your Katman key from https://katman.pro/app/mcp (`gh secret set KATMAN_KEY`; every katman-mcp command except account needs it and stops without an active plan). Give the OpenRouter key its own credit limit at https://openrouter.ai/keys, e.g. your monthly budget.",
|
|
147
147
|
includeAudit
|
|
148
148
|
? `2. Add a repository variable \`KATMAN_SITE_URL\` = your live URL (Variables tab, or \`gh variable set KATMAN_SITE_URL --body https://…\`)${siteUrl ? `; without it the workflow audits ${siteUrl}` : "; without it the audit step is skipped"}.`
|
|
149
149
|
: "2. (Audit step off.)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "katman-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Katman MCP: makes your vibe-coded site readable, quotable and recommendable by ChatGPT, Claude, Gemini and Perplexity. Scans your codebase, audits your live site like AI crawlers do, measures AI visibility with your own OpenRouter key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.json
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
"name": "pro.katman/mcp",
|
|
4
4
|
"title": "Katman",
|
|
5
5
|
"description": "Get your vibe-coded site read, quoted and recommended by ChatGPT, Claude, Gemini and Perplexity.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.2.0",
|
|
7
7
|
"websiteUrl": "https://katman.pro/mcp",
|
|
8
8
|
"packages": [
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
12
12
|
"identifier": "katman-mcp",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.2.0",
|
|
14
14
|
"runtimeHint": "npx",
|
|
15
15
|
"transport": { "type": "stdio" },
|
|
16
16
|
"environmentVariables": [
|
|
17
17
|
{
|
|
18
18
|
"name": "KATMAN_KEY",
|
|
19
|
-
"description": "
|
|
20
|
-
"isRequired":
|
|
19
|
+
"description": "Required. Your Katman licence key (km_live_…) from katman.pro/app/mcp. Every tool except katman_account needs it with an active plan: Audit Week ($3 for 7 days: audit and recommendations) or Katman Pro ($29/month founding price: everything). Plans: katman.pro/pricing.",
|
|
20
|
+
"isRequired": true,
|
|
21
21
|
"isSecret": true,
|
|
22
22
|
"format": "string"
|
|
23
23
|
},
|