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.
@@ -1,4 +1,4 @@
1
- // Crawl module: katman_scan_code, katman_audit_site, katman_indexnow, katman_keywords.
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 { PRO_NOTE, withPro } from "../../license.js";
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." + PRO_NOTE,
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
- }, withPro(ctx, "indexnow", async ({ project_root, site_url, urls, key, dry_run }) => {
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, only when it is set) or when you call katman_sync. No telemetry.",
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 { PRO_NOTE, startLicense, withPro } from "../../license.js";
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
- const out = await runStart(root, { siteUrl: args.site_url, hasOpenRouterKey: hasOpenRouterKey(), warning: rootWarning(root), lang: args.lang, license: await startLicense(ctx) });
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." + PRO_NOTE,
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
- }, withPro(ctx, "plan", wrap(ctx, (root, a) => runPlan(root, { lang: a.lang }))));
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." + PRO_NOTE,
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
- }, withPro(ctx, "prompts", wrap(ctx, (root, a) => runPrompt(root, { id: a.id, situationId: a.situation_id, lang: a.lang }))));
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." + PRO_NOTE,
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
- }, withPro(ctx, "briefs", wrap(ctx, (root, a) => runBrief(root, { type: a.type, situationId: a.situation_id, topic: a.topic, lang: a.lang }))));
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." + PRO_NOTE,
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
- }, withPro(ctx, "generate", wrap(ctx, (root, a) => runGenerate(root, { artifact: a.artifact, options: a.options, write: a.write, overwrite: a.overwrite }))));
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." + PRO_NOTE,
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
- }, 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 }))));
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." + PRO_NOTE,
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
- }, withPro(ctx, "report", wrap(ctx, (root, a) => runReport(root, { lang: a.lang }))));
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 (Katman prompt P1). REPORT ONLY: do not create, modify or delete any file in the project.
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, write the P1 report: one row per public page — URL | SSR/SSG/CSR | title + length | meta description + length | canonical | robots meta | H1 count | images without alt | og:image | JSON-LD types | pages linking to it.
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 (P1, report only)",
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, requirePro } from "../../../license.js";
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) => md(uri, methodOverviewMd("en")));
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) => md(uri, prepublishChecklistMd("en")));
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) => md(uri, siteAuditChecklistMd("en")));
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) => md(uri, contentRulesMd("en")));
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 and acceptance criteria stay listed,
39
- // the prompt bodies need a Pro or trial license.
40
- const gate = await requirePro("prompts", licenseOf(ctx));
41
- if (!gate.allowed) {
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), katman_manual_check
3
- // (no API cost: the user pastes consumer-app answers), katman_setup_monitoring (GitHub Actions).
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 { PRO_NOTE, withPro } from "../../license.js";
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." + PRO_NOTE,
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
- }, withPro(ctx, "visibility", async (args, extra) => {
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." + PRO_NOTE,
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
- }, withPro(ctx, "manual_check", async (args) => {
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)." + PRO_NOTE,
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
- }, withPro(ctx, "monitoring", async (args) => {
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
- "# `katman-mcp check` is a Katman Pro command: it needs the KATMAN_KEY repository secret (katman.pro/app).",
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 Pro key from https://katman.pro/app → MCP (`gh secret set KATMAN_KEY`; `katman-mcp check` is a Pro command and stops without it). Give the OpenRouter key its own credit limit at https://openrouter.ai/keys, e.g. your monthly budget.",
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.1.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.1.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.1.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": "Optional. Your Katman Pro key from katman.pro/app (7-day free trial, no card). Unlocks the Pro tools; the free diagnosis tools work without it.",
20
- "isRequired": false,
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
  },