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,13 +1,24 @@
1
- // Katman Pro texts: the upsell a Pro tool returns without a license (MCP and CLI), the one-line plan status,
2
- // the "needs Pro" notice for katman_start and the MCP config snippets with KATMAN_KEY.
3
- // Only type imports from license.ts (license.ts imports this module).
1
+ // Katman plan texts: what a tool returns without the plan it needs (MCP and CLI), katman_start without a plan, the
2
+ // one-line plan status, the notice katman_start adds when the next tool isn't in the plan, the paywall of a gated
3
+ // resource and the MCP config snippets with KATMAN_KEY.
4
+ // Only type imports from license.ts (license.ts imports this module); the plans come from plans.ts.
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
- import { APP_URL, BILLING_URL, KEYS_URL, PRO_PRICE_NOTE, TRIAL_DAYS } from "../../meta.js";
7
+ import { AUDIT_PRICE_NOTE, BILLING_URL, KEYS_URL, KEY_DOCS_URL, PRICING_URL, PRO_PRICE_NOTE } from "../../meta.js";
8
+ import { isProOnly } from "../../plans.js";
7
9
  import { listFiles } from "../../state.js";
8
- import { domainOf, sortFindings, truncate } from "../../util.js";
10
+ import { domainOf, mdTable, sortFindings, truncate } from "../../util.js";
11
+ import { GATES, STEPS } from "../method/data/method.js";
12
+ import { t } from "../method/lib/l10n.js";
9
13
  import { loadConfigSafe, loadLatestAudit, loadResearchSafe, loadScan } from "../method/lib/state.js";
10
- const FREE_LIST = "katman_start, katman_research, katman_scan_code, katman_audit_site, katman_keywords";
14
+ /** Without a key: the two plans, where to get a key and where it goes. */
15
+ export const NO_KEY_TEXT = `Katman needs a licence key (KATMAN_KEY). Audit Week — $3 for 7 days: audit, research, plan and AI-visibility checks; it doesn't build pages. Katman Pro — $29/month founding price ($290/year): everything, including the P0–P14 prompts, page briefs and file generation. Get a key at ${PRICING_URL}, then add KATMAN_KEY to your MCP config (${KEY_DOCS_URL}).`;
16
+ const AUDIT_COVERS = `Your Audit Week covers audit and recommendations; Pro builds the pages. Upgrade: ${BILLING_URL}`;
17
+ /** Audit Week calling a Pro-only tool or command. */
18
+ function proToolText(name, kind) {
19
+ return `${name} is a Katman Pro ${kind}. ${AUDIT_COVERS}`;
20
+ }
21
+ const upperFirst = (x) => x.replace(/^./, (c) => c.toUpperCase());
11
22
  const filled = (s) => !!s && !!s.trim() && !/\[(FILL|DOLDUR)\]/i.test(s);
12
23
  export async function projectFacts(root) {
13
24
  if (root === path.parse(root).root || root === os.homedir())
@@ -45,21 +56,33 @@ const pick = (g) => ({ pass: g.pass, total: g.total });
45
56
  function gatesText(a) {
46
57
  return `Read ${a.gates.read.pass}/${a.gates.read.total} · Quote ${a.gates.quote.pass}/${a.gates.quote.total} · Fit ${a.gates.fit.pass}/${a.gates.fit.total} pages pass`;
47
58
  }
48
- function foundText(f) {
59
+ /** "the code scan (…) and the live audit (…)", or "" when neither exists yet. */
60
+ function foundParts(f) {
49
61
  const parts = [];
50
62
  if (f?.scan)
51
63
  parts.push(`the code scan (${f.scan.findings} issue${f.scan.findings === 1 ? "" : "s"}${f.scan.critical ? `, ${f.scan.critical} critical` : ""})`);
52
64
  if (f?.audit)
53
65
  parts.push(`the live audit (${gatesText(f.audit)})`);
54
- return parts.length ? ` in ${parts.join(" and ")}` : "";
66
+ return parts.join(" and ");
55
67
  }
56
- /** One sentence: what this Pro tool would do for this project. */
68
+ /** One sentence: what this tool would do for this project. */
57
69
  export function pitch(tool, args, f) {
58
70
  const brand = f?.brand ?? f?.domain ?? "this site";
59
71
  const str = (k) => (typeof args[k] === "string" && args[k] ? String(args[k]) : undefined);
72
+ const found = foundParts(f);
60
73
  switch (tool) {
74
+ case "katman_start":
75
+ return `show where ${brand} stands in the Katman method (research, scan, audit, plan, measurements) and the exact next tool call.`;
76
+ case "katman_research":
77
+ return `build ${brand}'s research: the one sentence in 4 parts, the not-X note, 10–12 customer situations in the customers' own words, competitors with sources, proof and the 12 + 2 question set.`;
78
+ case "katman_scan_code":
79
+ return `scan ${f?.framework ? `this ${f.framework} project` : "this project"}'s code: whether pages render on the server or only in JavaScript, public vs app routes, robots/sitemap/llms.txt, head tags, JSON-LD and links, each finding with its gate and fix.`;
80
+ case "katman_audit_site":
81
+ return `audit ${str("site_url") ?? f?.domain ?? "the live site"} the way AI crawlers see it: robots.txt and CDN/firewall blocks for GPTBot, ClaudeBot and PerplexityBot, text with JavaScript off, soft 404s, canonicals, sitemap, JSON-LD and internal links, with the top 10 issues ranked.`;
82
+ case "katman_keywords":
83
+ return `expand ${str("seed") ? `“${str("seed")}”` : "a seed phrase"} with Google autocomplete into the phrases customers really type, grouped into situation, single-question and comparison page ideas.`;
61
84
  case "katman_plan":
62
- return `turn what Katman found${foundText(f)} into ${brand}'s page plan (which situation, comparison, pricing and FAQ pages are missing) and a 30-day task list, each task with a prompt code and an acceptance check.`;
85
+ return `turn what Katman found${found ? ` in ${found}` : ""} into ${brand}'s page plan (which situation, comparison, pricing and FAQ pages are missing) and a 30-day task list, each task with a prompt code and an acceptance check.`;
63
86
  case "katman_prompt":
64
87
  return `give your AI prompt ${str("id") ?? "P0–P14"} filled with ${brand}'s research (one sentence, not-X note, competitors${f?.situations ? `, ${f.situations} customer situations` : ""}), with the acceptance criterion and the exact Katman check that proves the task is done.`;
65
88
  case "katman_page_brief":
@@ -81,92 +104,135 @@ export function pitch(tool, args, f) {
81
104
  case "katman_sync":
82
105
  return `send ${brand}'s progress (gate pass counts, visibility rates, plan progress, top issue titles; never code or content) to your katman.pro dashboard.`;
83
106
  default:
84
- return "run the Katman Pro step of the method for this project.";
107
+ return "run this step of the Katman method for this project.";
85
108
  }
86
109
  }
87
- function freeDoneText(f) {
88
- if (f?.scan || f?.audit) {
89
- const top = f.topIssues.length ? ` Top issues: ${f.topIssues.map((t) => `“${t}”`).join("; ")}.` : "";
90
- return `**Already done for free:** the diagnosis${foundText(f)}.${top} The free tools (${FREE_LIST}) keep working without a key.`;
91
- }
92
- return `**Free, no key needed:** the diagnosis. katman_scan_code (your code) and katman_audit_site (your live site, as AI crawlers see it) find what keeps assistants from reading, quoting and recommending the site; katman_research and katman_keywords build the research.`;
110
+ /** What .katman/ already holds from an earlier plan, so the pitch can build on it. */
111
+ function doneText(f) {
112
+ const found = foundParts(f);
113
+ if (!found)
114
+ return undefined;
115
+ const top = f.topIssues.length ? ` Top issues: ${f.topIssues.map((x) => `“${x}”`).join("; ")}.` : "";
116
+ return `**Already in .katman/:** ${found}.${top}`;
93
117
  }
94
118
  // ---------------------------------------------------------------------------------------------------------------
95
119
  // Status texts
96
120
  const day = (iso) => (iso ? iso.slice(0, 10) : "?");
97
121
  const when = (iso) => (iso ? `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC` : "–");
98
- export function trialDaysLeft(s, now = new Date()) {
99
- if (s.plan !== "trial" || !s.trialEndsAt)
122
+ /** Days left in a running Audit Week (7 right after it starts, 1 on its last day). */
123
+ export function planDaysLeft(s, now = new Date()) {
124
+ if (s.plan !== "audit" || !s.planEndsAt)
100
125
  return undefined;
101
- return Math.max(0, Math.ceil((Date.parse(s.trialEndsAt) - now.getTime()) / 86_400_000));
126
+ return Math.max(0, Math.ceil((Date.parse(s.planEndsAt) - now.getTime()) / 86_400_000));
102
127
  }
103
128
  function offlineText(s) {
104
129
  return s.source === "offline-grace" ? ` · offline: using the check from ${day(s.checkedAt)}, works until ${day(s.graceUntil)}` : "";
105
130
  }
106
- /** Why a Pro tool didn't run (one or two sentences). */
107
- export function reasonText(s, feature) {
131
+ /** "Audit Week ended 2026-10-01" (for a table cell or a status line). */
132
+ export function endedPhrase(s) {
133
+ const what = s.endedPlan === "pro" ? "Katman Pro ended" : s.endedPlan === "audit" ? "Audit Week ended" : "plan ended";
134
+ return `${what}${s.planEndsAt ? ` ${day(s.planEndsAt)}` : ""}`;
135
+ }
136
+ /** "Your Audit Week ended on 2026-10-01." */
137
+ function endedText(s, lang = "en") {
138
+ const on = s.planEndsAt ? day(s.planEndsAt) : undefined;
139
+ if (lang === "tr") {
140
+ const what = s.endedPlan === "pro" ? "Katman Pro aboneliğiniz" : s.endedPlan === "audit" ? "Denetim Haftanız" : "Planınız";
141
+ return `${what} ${on ? `${on} tarihinde ` : ""}bitti.`;
142
+ }
143
+ const what = s.endedPlan === "pro" ? "Your Katman Pro subscription" : s.endedPlan === "audit" ? "Your Audit Week" : "Your Katman plan";
144
+ return `${what} ${on ? `ended on ${on}` : "has ended"}.`;
145
+ }
146
+ /** The only way on after an Audit Week (each account gets one), or after Pro ended. */
147
+ const onlyPro = (s) => !!s.planEnded || !!s.auditWeekUsed;
148
+ /** Why a tool didn't run (one or two sentences). `name` is the tool, or the CLI command. */
149
+ export function reasonText(s, o = {}) {
150
+ const tr = o.lang === "tr";
108
151
  switch (s.state) {
109
152
  case "no_key":
110
- return s.hint ?? "No KATMAN_KEY is set in this MCP server's configuration.";
153
+ if (s.hint)
154
+ return s.hint;
155
+ if (tr)
156
+ return "Bu MCP sunucusunun yapılandırmasında KATMAN_KEY yok.";
157
+ return o.surface === "cli" ? "No KATMAN_KEY is set in the environment." : "No KATMAN_KEY is set in this MCP server's configuration.";
111
158
  case "invalid":
112
- return s.source === "local" ? s.hint ?? "KATMAN_KEY doesn't look like a Katman key." : `katman.pro doesn't recognise this KATMAN_KEY (${s.keyHint ?? "km_…"}).`;
159
+ if (s.source === "local") {
160
+ return tr
161
+ ? "KATMAN_KEY bir Katman anahtarına benzemiyor: anahtarlar km_live_ ile başlar, ardından harfler ve rakamlar gelir (tırnak, boşluk ya da satır sonu olmadan). Hiçbir yere gönderilmedi."
162
+ : s.hint ?? "KATMAN_KEY doesn't look like a Katman key.";
163
+ }
164
+ return tr ? `katman.pro bu KATMAN_KEY’i tanımıyor (${s.keyHint ?? "km_…"}).` : `katman.pro doesn't recognise this KATMAN_KEY (${s.keyHint ?? "km_…"}).`;
113
165
  case "revoked":
114
- return `This KATMAN_KEY (${s.keyHint ?? "km_…"}) was revoked on katman.pro.`;
166
+ return tr ? `Bu KATMAN_KEY (${s.keyHint ?? "km_…"}) katman.pro’da iptal edilmiş.` : `This KATMAN_KEY (${s.keyHint ?? "km_…"}) was revoked on katman.pro.`;
115
167
  case "unreachable":
116
- return `Katman couldn't reach katman.pro to check KATMAN_KEY (${s.error ?? "network error"})${s.checkedAt ? `, and the last successful check (${day(s.checkedAt)}) is more than 7 days old` : ""}.`;
168
+ return tr
169
+ ? `Katman, KATMAN_KEY’i kontrol etmek için katman.pro’ya ulaşamadı (${s.error ?? "ağ hatası"})${s.checkedAt ? `; son başarılı kontrol (${day(s.checkedAt)}) 7 günden eski` : ""}.`
170
+ : `Katman couldn't reach katman.pro to check KATMAN_KEY (${s.error ?? "network error"})${s.checkedAt ? `, and the last successful check (${day(s.checkedAt)}) is more than 7 days old` : ""}.`;
117
171
  case "valid":
118
- if (s.plan === "free")
119
- return s.trialEnded ? `The ${TRIAL_DAYS}-day trial on this account ended on ${day(s.trialEndsAt)}.` : "This account is on the free plan (no active trial or Katman Pro subscription).";
120
- return `Your ${s.plan} plan doesn't include "${feature ?? "this feature"}" according to katman.pro.`;
172
+ if (s.plan === "none") {
173
+ if (s.planEnded)
174
+ return endedText(s, o.lang);
175
+ if (s.auditWeekUsed)
176
+ return tr ? "Bu hesabın etkin bir planı yok: tek Denetim Haftası kullanıldı." : "This account has no active plan: its one Audit Week has been used.";
177
+ return tr ? "Bu hesabın henüz etkin bir planı yok (Denetim Haftası ya da Katman Pro)." : "This account has no active plan yet (no Audit Week or Katman Pro).";
178
+ }
179
+ if (s.plan === "audit" && o.feature && isProOnly(o.feature))
180
+ return proToolText(o.name ?? "This tool", o.surface === "cli" ? "command" : "tool");
181
+ return tr
182
+ ? `Planınız katman.pro’ya göre “${o.feature ?? "bu özellik"}” özelliğini içermiyor.`
183
+ : `Your ${s.plan === "pro" ? "Katman Pro" : "Audit Week"} plan doesn't include "${o.feature ?? "this feature"}" according to katman.pro.`;
121
184
  }
122
185
  }
123
- /** One line for katman_start / katman_account. */
186
+ /** One line for katman_start. */
124
187
  export function statusLine(s, now = new Date()) {
125
188
  switch (s.state) {
126
189
  case "no_key":
127
- return s.hint ? `**Katman plan:** free · ${s.hint}` : `**Katman plan:** free (no KATMAN_KEY) · Pro tools: ${TRIAL_DAYS}-day free trial at ${APP_URL}`;
190
+ return s.hint ? `**Katman plan:** none · ${s.hint}` : `**Katman plan:** none (no KATMAN_KEY) · get a key at ${PRICING_URL}`;
128
191
  case "invalid":
129
- return `**Katman plan:** free · KATMAN_KEY ${s.source === "local" ? "doesn't look like a Katman key" : "isn't recognised by katman.pro"} · get a key at ${KEYS_URL}`;
192
+ return `**Katman plan:** none · KATMAN_KEY ${s.source === "local" ? "doesn't look like a Katman key" : "isn't recognised by katman.pro"} · get a key at ${KEYS_URL}`;
130
193
  case "revoked":
131
- return `**Katman plan:** free · this KATMAN_KEY was revoked · create a new one at ${KEYS_URL}`;
194
+ return `**Katman plan:** none · this KATMAN_KEY was revoked · create a new one at ${KEYS_URL}`;
132
195
  case "unreachable":
133
- return `**Katman plan:** not verified · couldn't reach katman.pro to check KATMAN_KEY (${s.error ?? "network error"}) · Pro tools are off until a check succeeds`;
196
+ return `**Katman plan:** not verified · couldn't reach katman.pro to check KATMAN_KEY (${s.error ?? "network error"}) · Katman's tools are off until a check succeeds`;
134
197
  case "valid": {
135
198
  const who = s.emailMasked ? ` · ${s.emailMasked}` : "";
136
199
  if (s.plan === "pro")
137
- return `**Katman plan:** Pro · all Pro tools on${who}${offlineText(s)}`;
138
- if (s.plan === "trial") {
139
- const left = trialDaysLeft(s, now);
140
- return `**Katman plan:** trial · ${left === undefined ? "" : `${left} day${left === 1 ? "" : "s"} left (until ${day(s.trialEndsAt)}) · `}all Pro tools on${who}${offlineText(s)}`;
200
+ return `**Katman plan:** Katman Pro · every tool on${who}${offlineText(s)}`;
201
+ if (s.plan === "audit") {
202
+ const left = planDaysLeft(s, now);
203
+ return `**Katman plan:** Audit Week · ${left === undefined ? "" : `${left} day${left === 1 ? "" : "s"} left (until ${day(s.planEndsAt)}) · `}audit and recommendation tools on; the page-building tools need Katman Pro (${BILLING_URL})${who}${offlineText(s)}`;
141
204
  }
142
- return `**Katman plan:** free${s.trialEnded ? ` (trial ended ${day(s.trialEndsAt)})` : ""} · Pro tools are off: upgrade at ${BILLING_URL}${who}`;
205
+ const note = s.planEnded ? ` (${endedPhrase(s)})` : s.auditWeekUsed ? " (Audit Week used)" : "";
206
+ const next = s.endedPlan === "pro" ? "renew Katman Pro" : onlyPro(s) ? "upgrade to Katman Pro" : "Audit Week ($3 for 7 days) or Katman Pro";
207
+ return `**Katman plan:** none${note} · ${next} at ${BILLING_URL}${who}`;
143
208
  }
144
209
  }
145
210
  }
146
- /** katman_start: the next step is a Pro tool and the license doesn't cover it. */
147
- export function proNotice(tool, s) {
148
- let why;
149
- let action;
211
+ /** One line: what to do next, by license state (resources, notices). */
212
+ function nextStepLine(s, feature) {
150
213
  switch (s.state) {
151
214
  case "no_key":
152
- why = "this Katman server has no KATMAN_KEY";
153
- action = `Start the ${TRIAL_DAYS}-day free trial (no card): ${APP_URL} → create a key → add it to the MCP config as KATMAN_KEY (katman_account shows the exact snippet for each editor)`;
154
- break;
215
+ return `Get a key at ${PRICING_URL}, then add KATMAN_KEY to the MCP config (${KEY_DOCS_URL}).`;
155
216
  case "invalid":
156
217
  case "revoked":
157
- why = `KATMAN_KEY was ${s.state === "revoked" ? "revoked" : "not accepted"}`;
158
- action = `Create a new key at ${KEYS_URL} and replace KATMAN_KEY in the MCP config`;
159
- break;
218
+ return `Create a new key at ${KEYS_URL} and replace KATMAN_KEY in the MCP config.`;
160
219
  case "unreachable":
161
- why = "katman.pro couldn't be reached to check KATMAN_KEY";
162
- action = "Check the internet connection, then call katman_account with refresh: true";
163
- break;
220
+ return "Check the internet connection, then call katman_account with refresh: true.";
164
221
  case "valid":
165
- why = s.plan === "free" ? (s.trialEnded ? "the trial on this account has ended" : "this account is on the free plan") : "this plan doesn't include it";
166
- action = `Upgrade at ${BILLING_URL} (${PRO_PRICE_NOTE}), then call katman_account with refresh: true`;
167
- break;
222
+ if (s.plan === "none") {
223
+ return onlyPro(s) || (feature && isProOnly(feature))
224
+ ? `${s.endedPlan === "pro" ? "Renew" : "Upgrade to"} Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}. Then call katman_account with refresh: true.`
225
+ : `Choose the Audit Week ($3 for 7 days) or Katman Pro at ${BILLING_URL}, then call katman_account with refresh: true.`;
226
+ }
227
+ return "Call katman_account with refresh: true; if it persists, run the latest Katman (npx -y katman-mcp@latest).";
168
228
  }
169
- return `> **Needs Katman Pro:** \`${tool}\` is a Katman Pro tool and ${why}. ${action}. Tell the user; the free diagnosis (katman_research, katman_scan_code, katman_audit_site, katman_keywords) keeps working.`;
229
+ }
230
+ /** katman_start: the recommended next tool isn't in the plan. */
231
+ export function planNotice(tool, feature, s) {
232
+ if (s.state === "valid" && s.plan === "audit" && isProOnly(feature)) {
233
+ return `> **Needs Katman Pro:** ${proToolText(`\`${tool}\``, "tool")}\n> Katman Pro: ${PRO_PRICE_NOTE}; the key stays the same. After upgrading, call katman_account with refresh: true. Tell the user; the Audit Week tools keep working${s.planEndsAt ? ` until ${day(s.planEndsAt)}` : ""}.`;
234
+ }
235
+ return `> **Not in your plan:** \`${tool}\` won't run: ${reasonText(s, { feature, name: tool })} ${nextStepLine(s, feature)} Tell the user.`;
170
236
  }
171
237
  // ---------------------------------------------------------------------------------------------------------------
172
238
  // Snippets
@@ -177,7 +243,7 @@ export function installSnippets() {
177
243
  "claude mcp remove katman --scope user # only if Katman is already added",
178
244
  "claude mcp add katman --scope user -e KATMAN_KEY=km_live_… -e OPENROUTER_API_KEY=sk-or-… -- npx -y katman-mcp",
179
245
  "```",
180
- "OPENROUTER_API_KEY is optional (only for API visibility runs). With the Katman Claude Code plugin, set its `katman_key` option instead.",
246
+ "OPENROUTER_API_KEY is optional (only for API visibility runs). With the Katman Claude Code plugin, set its `katman_key` option instead (`/plugin configure katman@katman`).",
181
247
  "",
182
248
  "**Cursor**: in `~/.cursor/mcp.json` (your user file, not a committed project file); keep any other env entries you have:",
183
249
  "```json",
@@ -187,104 +253,226 @@ export function installSnippets() {
187
253
  "**VS Code**: in `.vscode/mcp.json`; VS Code asks for the key once and keeps it in its secret storage:",
188
254
  "```json",
189
255
  "{",
190
- ' "inputs": [{ "type": "promptString", "id": "katman_key", "description": "Katman key (katman.pro/app)", "password": true }],',
256
+ ' "inputs": [{ "type": "promptString", "id": "katman_key", "description": "Katman key (katman.pro/app/mcp)", "password": true }],',
191
257
  ' "servers": { "katman": { "type": "stdio", "command": "npx", "args": ["-y", "katman-mcp"], "env": { "KATMAN_KEY": "${input:katman_key}" } } }',
192
258
  "}",
193
259
  "```",
194
260
  ].join("\n");
195
261
  }
196
- const RESTART = "Restart the Katman MCP server (or reload the editor window) so it reads the key, then call katman_account to confirm.";
197
- /** What to do next, by license state. */
198
- export function howToText(s, rerun) {
199
- const again = rerun ? ` and run ${rerun} again` : "";
262
+ /** What to do next, by license state. `rerun` is the tool to call again afterwards. */
263
+ export function howToText(s, o = {}) {
264
+ const tr = o.lang === "tr";
265
+ const again = o.rerun ? (tr ? `, sonra ${o.rerun} aracını yeniden çağırın` : ` and run ${o.rerun} again`) : "";
266
+ const restart = tr
267
+ ? `Anahtarı okuması için Katman MCP sunucusunu yeniden başlatın (ya da editör penceresini yeniden yükleyin), katman_account ile kontrol edin${again}.`
268
+ : `Restart the Katman MCP server (or reload the editor window) so it reads the key, then call katman_account to confirm${again}.`;
269
+ const refresh = tr ? `katman_account aracını refresh: true ile çağırın${again}.` : `call katman_account with refresh: true${again}.`;
270
+ const Refresh = upperFirst(refresh);
200
271
  switch (s.state) {
201
272
  case "no_key":
202
- return [
203
- `### Start the ${TRIAL_DAYS}-day free trial (no card)`,
204
- `1. Open ${APP_URL}, sign in with your email, then **MCP → Create key** and copy the key (km_live_…; it's shown once).`,
205
- "2. Add it to the MCP config as `KATMAN_KEY`:",
206
- "",
207
- installSnippets(),
208
- "",
209
- `3. ${RESTART.replace(/\.$/, "")}${again}.`,
210
- "",
211
- `After the trial: Katman Pro, ${PRO_PRICE_NOTE}.`,
212
- ].join("\n");
273
+ return (tr
274
+ ? [
275
+ "### Anahtar alın",
276
+ `1. ${PRICING_URL} adresinden bir plan seçin, sonra ${KEYS_URL} → **Anahtar oluştur** ile anahtarınızı oluşturup kopyalayın (km_live_…; yalnızca bir kez gösterilir).`,
277
+ `2. Anahtarı MCP yapılandırmasına \`KATMAN_KEY\` olarak ekleyin (her editör için adım adım: ${KEY_DOCS_URL}):`,
278
+ ]
279
+ : [
280
+ "### Get a key",
281
+ `1. Choose a plan at ${PRICING_URL}, then open ${KEYS_URL} → **Create key** and copy the key (km_live_…; it's shown once).`,
282
+ `2. Add it to the MCP config as \`KATMAN_KEY\` (step by step for each editor: ${KEY_DOCS_URL}):`,
283
+ ]).concat(["", installSnippets(), "", `3. ${restart}`]).join("\n");
213
284
  case "invalid":
214
285
  case "revoked":
215
- return [
216
- "### Use a valid key",
217
- `1. Open ${KEYS_URL}. ${s.state === "revoked" ? "Create a new key" : "Copy the key again (the whole km_live_… string, no quotes or spaces), or create a new one if you no longer have it"}.`,
218
- "2. Replace `KATMAN_KEY` in the MCP config:",
219
- "",
220
- installSnippets(),
221
- "",
222
- `3. ${RESTART.replace(/\.$/, "")}${again}.`,
223
- ].join("\n");
224
- case "unreachable":
225
- return [
226
- "### Check the connection",
227
- "Pro tools keep working offline for 7 days after a successful check with katman.pro. Make sure katman.pro is reachable from this machine (internet connection, proxy or firewall" +
228
- `${/KATMAN_API_URL/.test(s.error ?? "") ? ", KATMAN_API_URL" : ""}), then call katman_account with refresh: true${again}.`,
229
- ].join("\n");
286
+ return (tr
287
+ ? [
288
+ "### Geçerli bir anahtar kullanın",
289
+ `1. ${KEYS_URL} adresini açın. ${s.state === "revoked" ? "Yeni bir anahtar oluşturun" : "Anahtarı yeniden kopyalayın (km_live_… dizesinin tamamı, tırnak ya da boşluk olmadan) ya da elinizde yoksa yeni bir tane oluşturun"}.`,
290
+ "2. MCP yapılandırmasındaki `KATMAN_KEY` değerini değiştirin:",
291
+ ]
292
+ : [
293
+ "### Use a valid key",
294
+ `1. Open ${KEYS_URL}. ${s.state === "revoked" ? "Create a new key" : "Copy the key again (the whole km_live_… string, no quotes or spaces), or create a new one if you no longer have it"}.`,
295
+ "2. Replace `KATMAN_KEY` in the MCP config:",
296
+ ]).concat(["", installSnippets(), "", `3. ${restart}`]).join("\n");
297
+ case "unreachable": {
298
+ const api = /KATMAN_API_URL/.test(s.error ?? "") ? ", KATMAN_API_URL" : "";
299
+ return tr
300
+ ? `### Bağlantıyı kontrol edin\nKatman, katman.pro ile başarılı bir kontrolden sonra 7 gün çevrimdışı çalışır (bir Denetim Haftasının bitişini aşmadan). Bu bilgisayardan katman.pro’ya ulaşılabildiğinden emin olun (internet bağlantısı, proxy ya da güvenlik duvarı${api}), sonra ${refresh}`
301
+ : `### Check the connection\nKatman keeps working offline for 7 days after a successful check with katman.pro (never past the end of an Audit Week). Make sure katman.pro is reachable from this machine (internet connection, proxy or firewall${api}), then ${refresh}`;
302
+ }
230
303
  case "valid":
231
- if (s.plan === "free") {
232
- return [
233
- "### Upgrade to Katman Pro",
234
- `Katman Pro is ${PRO_PRICE_NOTE}: ${BILLING_URL}. The key stays the same.`,
235
- `Already upgraded? Call katman_account with refresh: true${again}.`,
236
- ].join("\n");
304
+ if (s.plan === "none" && s.endedPlan === "pro") {
305
+ return tr
306
+ ? `### Katman Pro’yu yenileyin\nAnahtarınız aynı kalır: ${BILLING_URL} adresinden yeniden abone olun (kurucu fiyatıyla ayda 29 $ ya da yılda 290 $). Yeniden abone oldunuz mu? ${refresh}`
307
+ : `### Renew Katman Pro\nThe key stays the same: resubscribe at ${BILLING_URL}. Katman Pro: ${PRO_PRICE_NOTE}. Already renewed? ${Refresh}`;
308
+ }
309
+ if (s.plan === "none" && (onlyPro(s) || (o.feature && isProOnly(o.feature)))) {
310
+ const why = onlyPro(s)
311
+ ? tr ? "Her hesabın tek bir Denetim Haftası var; devam etmek için Katman Pro" : "Each account gets one Audit Week, so Katman Pro is the way to continue"
312
+ : tr ? "Bu araç Katman Pro’da; Denetim Haftası yalnızca denetim araçlarını kapsar" : "This tool is in Katman Pro; the Audit Week covers only the audit tools";
313
+ return tr
314
+ ? `### Katman Pro’ya geçin\n${why}: kurucu fiyatıyla ayda 29 $ (ilk 100 müşteri; abonelik sürdükçe değişmez, sonrasında 49 $) ya da yılda 290 $, ${BILLING_URL}. Anahtar aynı kalır. Geçtiniz mi? ${refresh}`
315
+ : `### Upgrade to Katman Pro\n${why}: ${PRO_PRICE_NOTE}, at ${BILLING_URL}. The key stays the same. Already upgraded? ${Refresh}`;
237
316
  }
238
- return [
239
- "### Update Katman",
240
- `Run the latest version (npx -y katman-mcp@latest), call katman_account with refresh: true${again}. If it persists, write to hello@katman.pro.`,
241
- ].join("\n");
317
+ if (s.plan === "none") {
318
+ return tr
319
+ ? `### Bir plan seçin\n${plansList("tr")}\n\nSatın alma: ${BILLING_URL}; anahtar aynı kalır. Satın aldıktan sonra ${refresh}`
320
+ : `### Choose a plan\n${plansList("en")}\n\nBuy at ${BILLING_URL}; the key stays the same. Then ${refresh}`;
321
+ }
322
+ if (s.plan === "audit" && o.feature && isProOnly(o.feature)) {
323
+ return `### Upgrade to Katman Pro\nKatman Pro: ${PRO_PRICE_NOTE}; the key stays the same. Your Audit Week tools keep working${s.planEndsAt ? ` until ${day(s.planEndsAt)}` : ""}. Already upgraded? ${Refresh}`;
324
+ }
325
+ return tr
326
+ ? `### Katman’ı güncelleyin\nEn son sürümü çalıştırın (npx -y katman-mcp@latest) ve ${refresh} Sorun sürerse hello@katman.pro adresine yazın.`
327
+ : `### Update Katman\nRun the latest version (npx -y katman-mcp@latest) and ${refresh} If it persists, write to hello@katman.pro.`;
328
+ }
329
+ }
330
+ /** The two plans as a short list. */
331
+ function plansList(lang) {
332
+ return lang === "tr"
333
+ ? [
334
+ "- **Denetim Haftası**: 7 gün için 3 $, tek seferlik (kendiliğinden yenilenmez, hesap başına bir kez). Denetim, araştırma, plan ve yapay zekâ görünürlük ölçümleri; sayfa yapmaz, site dosyası yazmaz.",
335
+ "- **Katman Pro**: kurucu fiyatıyla ayda 29 $ (ilk 100 müşteri; abonelik sürdükçe değişmez, sonrasında 49 $) ya da yılda 290 $. Her şey: P0–P14 prompt’ları, sayfa brief’leri ve dosya üretimi dâhil.",
336
+ ].join("\n")
337
+ : [
338
+ `- **Audit Week**: ${AUDIT_PRICE_NOTE}. Audit, research, plan and AI-visibility checks; it doesn't build pages or write site files.`,
339
+ `- **Katman Pro**: ${PRO_PRICE_NOTE}. Everything, including the P0–P14 prompts, page briefs and file generation.`,
340
+ ].join("\n");
341
+ }
342
+ /** The two plans as a table (katman_start without a plan). */
343
+ function plansTable(lang) {
344
+ return lang === "tr"
345
+ ? mdTable(["Plan", "Fiyat", "Kapsadığı"], [
346
+ ["**Denetim Haftası**", "7 gün için 3 $, tek seferlik (kendiliğinden yenilenmez, hesap başına bir kez)", "Denetim ve öneriler: katman_start, araştırma, kod taraması, canlı site denetimi, anahtar kelimeler, plan, içerik kontrolü, yapay zekâ görünürlük ölçümleri, elle kontroller ve rapor. Sayfa yapmaz, site dosyası yazmaz."],
347
+ ["**Katman Pro**", "Kurucu fiyatıyla ayda 29 $ (ilk 100 müşteri; abonelik sürdükçe değişmez, sonrasında 49 $) ya da yılda 290 $", "Her şey: P0–P14 prompt’ları, sayfa brief’leri, dosya üretimi (robots.txt, llms.txt, JSON-LD), IndexNow, zamanlanmış izleme ve katman.pro paneli dâhil."],
348
+ ])
349
+ : mdTable(["Plan", "Price", "What it covers"], [
350
+ ["**Audit Week**", AUDIT_PRICE_NOTE, "Audit and recommendations: katman_start, research, code scan, live audit, keywords, plan, content lint, AI-visibility runs, manual checks and the report. It doesn't build pages or write site files."],
351
+ ["**Katman Pro**", PRO_PRICE_NOTE, "Everything, including the P0–P14 prompts, page briefs, file generation (robots.txt, llms.txt, JSON-LD), IndexNow, scheduled monitoring and the katman.pro dashboard."],
352
+ ]);
353
+ }
354
+ function headline(name, feature, s) {
355
+ switch (s.state) {
356
+ case "no_key":
357
+ return `${name} needs a Katman licence key`;
358
+ case "invalid":
359
+ case "revoked":
360
+ return `${name} needs a valid Katman key`;
361
+ case "unreachable":
362
+ return `${name} couldn't check the Katman licence`;
363
+ case "valid":
364
+ if (s.plan === "none")
365
+ return `${name} needs an active Katman plan`;
366
+ return s.plan === "audit" && isProOnly(feature) ? `${name} needs Katman Pro` : `${name} isn't in your Katman plan`;
242
367
  }
243
368
  }
244
369
  export function renderUpsell(o) {
245
370
  if (o.surface === "cli")
246
371
  return renderCliUpsell(o);
247
372
  const where = o.facts?.brand ?? o.facts?.domain;
373
+ const done = doneText(o.facts);
374
+ const proOnly = isProOnly(o.feature) ? ` ${o.tool} is in Katman Pro; the Audit Week doesn't include it.` : "";
248
375
  return [
249
- `## ${o.tool} is part of Katman Pro`,
250
- `**What it would do${where ? ` for ${where}` : ""}:** ${pitch(o.tool, o.args ?? {}, o.facts).replace(/^./, (c) => c.toUpperCase())}`,
251
- freeDoneText(o.facts),
252
- `**Why it didn't run:** ${reasonText(o.status, o.feature)}`,
253
- howToText(o.status, o.tool),
254
- `Tell the user this in your own words. Don't try to work around it; the free tools (${FREE_LIST}) keep working.`,
376
+ `## ${headline(o.tool, o.feature, o.status)}`,
377
+ `**What it would do${where ? ` for ${where}` : ""}:** ${upperFirst(pitch(o.tool, o.args ?? {}, o.facts))}`,
378
+ ...(done ? [done] : []),
379
+ `**Why it didn't run:** ${reasonText(o.status, { feature: o.feature, name: o.tool })}`,
380
+ ...(o.status.state === "no_key" ? [NO_KEY_TEXT + proOnly] : []),
381
+ howToText(o.status, { rerun: o.tool, feature: o.feature }),
382
+ "Tell the user this in your own words. Don't try to work around it (no editing Katman's code or cache, no made-up keys); katman_account always works.",
255
383
  ].join("\n\n");
256
384
  }
257
385
  function renderCliUpsell(o) {
258
386
  const cmd = o.command ?? o.tool;
387
+ const s = o.status;
259
388
  const L = [
260
- `${cmd} is part of Katman Pro: it would ${pitch(o.tool, o.args ?? {}, o.facts)}`,
261
- `Why it didn't run: ${reasonText(o.status, o.feature)}`,
389
+ `${headline(cmd, o.feature, s)}: it would ${pitch(o.tool, o.args ?? {}, o.facts)}`,
390
+ `Why it didn't run: ${reasonText(s, { feature: o.feature, name: cmd, surface: "cli" })}`,
262
391
  "",
263
392
  ];
264
- if (o.status.state === "valid" && o.status.plan === "free") {
265
- L.push(`Upgrade: ${BILLING_URL} (${PRO_PRICE_NOTE}); the key stays the same. Already upgraded? Run: katman-mcp account --refresh`);
393
+ if (s.state === "valid") {
394
+ if (s.plan === "audit" && isProOnly(o.feature)) {
395
+ L.push(`Katman Pro: ${PRO_PRICE_NOTE}; the key stays the same. Already upgraded? Run: katman-mcp account --refresh`);
396
+ }
397
+ else if (s.plan === "none" && s.endedPlan === "pro") {
398
+ L.push(`Renew Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}; the key stays the same. Already renewed? Run: katman-mcp account --refresh`);
399
+ }
400
+ else if (s.plan === "none" && (onlyPro(s) || isProOnly(o.feature))) {
401
+ L.push(`Upgrade to Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}${onlyPro(s) ? " (each account gets one Audit Week)" : ""}. The key stays the same. Already upgraded? Run: katman-mcp account --refresh`);
402
+ }
403
+ else if (s.plan === "none") {
404
+ L.push(`Choose a plan at ${BILLING_URL}. Audit Week: ${AUDIT_PRICE_NOTE}. Katman Pro: ${PRO_PRICE_NOTE}. The key stays the same. Already bought? Run: katman-mcp account --refresh`);
405
+ }
406
+ else {
407
+ L.push("Run the latest version (npx -y katman-mcp@latest), then: katman-mcp account --refresh. If it persists, write to hello@katman.pro.");
408
+ }
266
409
  }
267
- else if (o.status.state === "unreachable") {
268
- L.push("Pro commands keep working offline for 7 days after a successful check. Make sure katman.pro is reachable, then run: katman-mcp account --refresh");
410
+ else if (s.state === "unreachable") {
411
+ L.push("Katman keeps working offline for 7 days after a successful check (never past the end of an Audit Week). Make sure katman.pro is reachable, then run: katman-mcp account --refresh");
269
412
  }
270
413
  else {
271
- L.push(`1. ${o.status.state === "no_key" ? `Start the ${TRIAL_DAYS}-day free trial (no card): ${APP_URL} → MCP → Create key` : `Get a valid key: ${KEYS_URL}`}`, "2. Run with the key in the environment:", ` KATMAN_KEY=km_live_… npx -y ${cmd} --root .`, " GitHub Actions: add the repository secret KATMAN_KEY (gh secret set KATMAN_KEY) and pass it as env.");
272
- if (o.status.state === "no_key")
273
- L.push(`After the trial: Katman Pro, ${PRO_PRICE_NOTE}.`);
414
+ if (s.state === "no_key")
415
+ L.push(NO_KEY_TEXT, "");
416
+ L.push(`1. ${s.state === "no_key" ? `Get a key: ${PRICING_URL}, then ${KEYS_URL} → Create key` : `Get a valid key: ${KEYS_URL}`}`, "2. Run with the key in the environment:", ` KATMAN_KEY=km_live_… npx -y ${o.example ?? cmd}`, " GitHub Actions: add the repository secret KATMAN_KEY (gh secret set KATMAN_KEY) and pass it as env.");
274
417
  }
275
- L.push("", "Free without a key: katman-mcp scan, audit, keywords, models, indexnow (dry run), account.");
418
+ L.push("", "Without a key: katman-mcp account, --help and --version.");
276
419
  return L.join("\n");
277
420
  }
278
- /** structuredContent of an upsell (no secrets). */
421
+ /** structuredContent of a tool that didn't run for lack of a plan (no secrets). */
279
422
  export function upsellStructured(tool, feature, s) {
280
423
  return {
281
424
  licensed: false,
282
425
  tool,
283
426
  feature,
284
- license: { state: s.state, plan: s.plan, ...(s.trialEnded ? { trialEnded: true, trialEndsAt: s.trialEndsAt } : {}) },
285
- trialUrl: APP_URL,
427
+ /** The cheapest plan that includes the feature. */
428
+ requiredPlan: isProOnly(feature) ? "pro" : "audit",
429
+ license: {
430
+ state: s.state,
431
+ plan: s.plan,
432
+ ...(s.planEndsAt ? { planEndsAt: s.planEndsAt } : {}),
433
+ ...(s.planEnded ? { planEnded: true } : {}),
434
+ ...(s.auditWeekUsed ? { auditWeekUsed: true } : {}),
435
+ },
436
+ pricingUrl: PRICING_URL,
286
437
  keysUrl: KEYS_URL,
287
438
  upgradeUrl: BILLING_URL,
439
+ docsUrl: KEY_DOCS_URL,
288
440
  };
289
441
  }
442
+ // ---------------------------------------------------------------------------------------------------------------
443
+ // katman_start without a plan, gated resources
444
+ /** katman_start without a plan: what Katman does, the two plans and how to set the key. No method walk-through. */
445
+ export function startIntro(s, lang = "en") {
446
+ const L = (en, tr) => (lang === "tr" ? tr : en);
447
+ const gates = GATES.map((g) => `- **${t(g.name, lang)}**: ${t(g.summary, lang)}`).join("\n");
448
+ const steps = STEPS.map((x) => `${x.n} ${t(x.name, lang)}`).join(" → ");
449
+ const heading = s.state === "no_key"
450
+ ? L("Katman needs a licence key", "Katman bir lisans anahtarı ister")
451
+ : s.state === "invalid" || s.state === "revoked"
452
+ ? L("Katman needs a valid key", "Katman geçerli bir anahtar ister")
453
+ : s.state === "unreachable"
454
+ ? L("Katman couldn't check your licence", "Katman lisansınızı kontrol edemedi")
455
+ : L("Katman needs an active plan", "Katman etkin bir plan ister");
456
+ const why = s.state === "no_key" && !s.hint ? undefined : reasonText(s, { feature: "start", name: "katman_start", lang });
457
+ return [
458
+ "# Katman",
459
+ L("Katman makes your public website readable, quotable and recommendable by AI assistants (ChatGPT, Claude, Gemini, Perplexity, Copilot) and findable in Google, Bing and Brave. It reads your code and your live site the way AI crawlers do, tells your coding agent what to fix and which pages to write, and checks that it was done.", "Katman, herkese açık web sitenizi yapay zekâ asistanlarının (ChatGPT, Claude, Gemini, Perplexity, Copilot) okuyabileceği, alıntılayabileceği ve önerebileceği hâle getirir; Google, Bing ve Brave’de de bulunur kılar. Kodunuzu ve yayındaki sitenizi yapay zekâ tarayıcılarının gördüğü gibi okur, kod aracınıza neyi düzelteceğini ve hangi sayfaları yazacağını söyler, yapılıp yapılmadığını kontrol eder."),
460
+ `${L("**3 gates** every public page must pass:", "Her herkese açık sayfanın geçmesi gereken **3 kapı**:")}\n${gates}`,
461
+ L(`**4 steps:** ${steps}, then re-measure every 14 days.`, `**4 adım:** ${steps}; sonra 14 günde bir yeniden ölçüm.`),
462
+ `## ${heading}`,
463
+ ...(why ? [`**${L("Why nothing ran", "Neden çalışmadı")}:** ${why}`] : []),
464
+ ...(s.state === "unreachable" ? [] : [plansTable(lang)]),
465
+ howToText(s, { rerun: "katman_start", feature: "start", lang }),
466
+ "Tell the user this in your own words, then stop: nothing else in Katman runs until KATMAN_KEY has an active plan. katman_account shows the plan at any time.",
467
+ ].join("\n\n");
468
+ }
469
+ /** A gated resource without the plan: its title, why, and what to do. */
470
+ export function resourcePaywall(title, feature, s, then) {
471
+ const why = s.state === "no_key" && !s.hint
472
+ ? NO_KEY_TEXT
473
+ : s.state === "valid" && s.plan === "audit" && isProOnly(feature)
474
+ ? `This is part of Katman Pro. ${AUDIT_COVERS}`
475
+ : `${reasonText(s, { feature })} ${nextStepLine(s, feature)}`;
476
+ return `# ${title}\n\n${why}\n\n${then}`;
477
+ }
290
478
  export { when as fmtWhen, day as fmtDay };
@@ -100,7 +100,7 @@ export const help = [
100
100
  " katman-mcp audit <url> [--max 50] [--root .] [--urls u1,u2] [--preview u1] [--no-bots] [--strict] [--no-save]",
101
101
  " audit the live site like AI crawlers see it (--strict: exit 2 on critical)",
102
102
  " katman-mcp indexnow [--root .] [--site url] [--urls u1,u2] [--submit]",
103
- " IndexNow key check + new/changed URLs (dry run unless --submit)",
103
+ " IndexNow key check + new/changed URLs (dry run unless --submit; Katman Pro)",
104
104
  ' katman-mcp keywords "<seed>" [--lang en] [--country us] [--no-expand] [--save]',
105
105
  " Google autocomplete ideas (≤40 requests)",
106
106
  ];