katman-mcp 0.1.0 → 0.3.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.
Files changed (43) hide show
  1. package/README.md +56 -44
  2. package/dist/cli.js +27 -14
  3. package/dist/license.js +331 -98
  4. package/dist/meta.js +29 -7
  5. package/dist/plans.js +54 -0
  6. package/dist/tools/account/account.js +60 -30
  7. package/dist/tools/account/cli.js +5 -2
  8. package/dist/tools/account/index.js +7 -7
  9. package/dist/tools/account/sync.js +2 -2
  10. package/dist/tools/account/upsell.js +423 -118
  11. package/dist/tools/crawl/audit/report.js +2 -2
  12. package/dist/tools/crawl/cli.js +17 -5
  13. package/dist/tools/crawl/index.js +52 -18
  14. package/dist/tools/crawl/scan/framework.js +4 -1
  15. package/dist/tools/method/core/lint.js +15 -1
  16. package/dist/tools/method/core/prompt.js +2 -1
  17. package/dist/tools/method/core/start.js +1 -1
  18. package/dist/tools/method/data/_store.js +17 -0
  19. package/dist/tools/method/data/anatomy.js +12 -141
  20. package/dist/tools/method/data/appendices.js +22 -202
  21. package/dist/tools/method/data/cloudflare.js +28 -192
  22. package/dist/tools/method/data/evidence.js +18 -137
  23. package/dist/tools/method/data/geo-jobs.js +8 -128
  24. package/dist/tools/method/data/landing.js +14 -67
  25. package/dist/tools/method/data/lessons.js +16 -127
  26. package/dist/tools/method/data/measurement.js +38 -142
  27. package/dist/tools/method/data/offsite.js +16 -148
  28. package/dist/tools/method/data/pages.js +16 -167
  29. package/dist/tools/method/data/prompts.js +8 -633
  30. package/dist/tools/method/data/publish.js +14 -113
  31. package/dist/tools/method/data/research.js +14 -131
  32. package/dist/tools/method/data/roadmap.js +12 -105
  33. package/dist/tools/method/data/traps.js +6 -74
  34. package/dist/tools/method/data/verify.js +12 -88
  35. package/dist/tools/method/data/viral-list.js +12 -39
  36. package/dist/tools/method/index.js +27 -17
  37. package/dist/tools/method/lib/prompt-lookup.js +18 -0
  38. package/dist/tools/method/mcp/prompts.js +6 -6
  39. package/dist/tools/method/mcp/resources.js +21 -17
  40. package/dist/tools/visibility/index.js +10 -10
  41. package/dist/tools/visibility/monitoring.js +6 -3
  42. package/package.json +5 -3
  43. package/server.json +4 -4
@@ -1,13 +1,25 @@
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_COUNT_NOTE, AUDIT_COUNT_NOTE_TR, BILLING_URL, BUY_PRO_1_URL, BUY_PRO_3_URL, KEYS_URL, KEY_DOCS_URL, PRICING_URL, PRO_3_NOTE, PRO_PRICE_NOTE, PRO_PRICE_NOTE_TR, VERSION, } 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). ${PRO_PRICE_NOTE}: audit, research, plan and AI-visibility checks, 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
+ /** For an account that bought an Audit Week earlier (no longer sold): what it covers and the way up. */
17
+ const AUDIT_COVERS = `Your Audit Week covers audit and recommendations; Pro builds the pages. Upgrade: ${BILLING_URL}`;
18
+ /** An Audit Week (bought earlier) calling a Pro-only tool or command. */
19
+ function proToolText(name, kind) {
20
+ return `${name} is a Katman Pro ${kind}. ${AUDIT_COVERS}`;
21
+ }
22
+ const upperFirst = (x) => x.replace(/^./, (c) => c.toUpperCase());
11
23
  const filled = (s) => !!s && !!s.trim() && !/\[(FILL|DOLDUR)\]/i.test(s);
12
24
  export async function projectFacts(root) {
13
25
  if (root === path.parse(root).root || root === os.homedir())
@@ -45,21 +57,33 @@ const pick = (g) => ({ pass: g.pass, total: g.total });
45
57
  function gatesText(a) {
46
58
  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
59
  }
48
- function foundText(f) {
60
+ /** "the code scan (…) and the live audit (…)", or "" when neither exists yet. */
61
+ function foundParts(f) {
49
62
  const parts = [];
50
63
  if (f?.scan)
51
64
  parts.push(`the code scan (${f.scan.findings} issue${f.scan.findings === 1 ? "" : "s"}${f.scan.critical ? `, ${f.scan.critical} critical` : ""})`);
52
65
  if (f?.audit)
53
66
  parts.push(`the live audit (${gatesText(f.audit)})`);
54
- return parts.length ? ` in ${parts.join(" and ")}` : "";
67
+ return parts.join(" and ");
55
68
  }
56
- /** One sentence: what this Pro tool would do for this project. */
69
+ /** One sentence: what this tool would do for this project. */
57
70
  export function pitch(tool, args, f) {
58
71
  const brand = f?.brand ?? f?.domain ?? "this site";
59
72
  const str = (k) => (typeof args[k] === "string" && args[k] ? String(args[k]) : undefined);
73
+ const found = foundParts(f);
60
74
  switch (tool) {
75
+ case "katman_start":
76
+ return `show where ${brand} stands in the Katman method (research, scan, audit, plan, measurements) and the exact next tool call.`;
77
+ case "katman_research":
78
+ 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.`;
79
+ case "katman_scan_code":
80
+ 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.`;
81
+ case "katman_audit_site":
82
+ 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.`;
83
+ case "katman_keywords":
84
+ 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
85
  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.`;
86
+ 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
87
  case "katman_prompt":
64
88
  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
89
  case "katman_page_brief":
@@ -81,92 +105,162 @@ export function pitch(tool, args, f) {
81
105
  case "katman_sync":
82
106
  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
107
  default:
84
- return "run the Katman Pro step of the method for this project.";
108
+ return "run this step of the Katman method for this project.";
85
109
  }
86
110
  }
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.`;
111
+ /** What .katman/ already holds from an earlier plan, so the pitch can build on it. */
112
+ function doneText(f) {
113
+ const found = foundParts(f);
114
+ if (!found)
115
+ return undefined;
116
+ const top = f.topIssues.length ? ` Top issues: ${f.topIssues.map((x) => `“${x}”`).join("; ")}.` : "";
117
+ return `**Already in .katman/:** ${found}.${top}`;
93
118
  }
94
119
  // ---------------------------------------------------------------------------------------------------------------
95
120
  // Status texts
96
121
  const day = (iso) => (iso ? iso.slice(0, 10) : "?");
97
122
  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)
123
+ /** "Katman Pro, 1 site" / "Katman Pro, 3 sites" from the plan's limits; "Katman Pro" when there are none (older plans). */
124
+ export function proPlanName(limits) {
125
+ if (limits?.sites === 1 && limits.auditsPerMonth === 5)
126
+ return "Katman Pro, 1 site";
127
+ if (limits?.sites === 3 && limits.auditsPerMonth === 30)
128
+ return "Katman Pro, 3 sites";
129
+ return "Katman Pro";
130
+ }
131
+ const countOf = (n, one, many) => `${n} ${n === 1 ? one : many}`;
132
+ /** "3 of 5 audits, 1 of 1 site used this month (counts start again 2026-11-01)"; without counts: "5 audits and 1 site a month". */
133
+ export function usageText(limits, usage) {
134
+ if (!usage)
135
+ return `${countOf(limits.auditsPerMonth, "audit", "audits")} and ${countOf(limits.sites, "site", "sites")} a month`;
136
+ return `${usage.audits} of ${countOf(limits.auditsPerMonth, "audit", "audits")}, ${usage.sites} of ${countOf(limits.sites, "site", "sites")} used this month (counts start again ${day(usage.resetsAt)})`;
137
+ }
138
+ /** The update notice for a katman-mcp too old to be counted (katman.pro's updateMcpTo). */
139
+ function updateText(s) {
140
+ return `This katman-mcp (${VERSION}) can't count audits against your plan, so the audit is off until you update to ${s.updateMcpTo} or newer: run npx -y katman-mcp@latest, restart the MCP server (or reload the editor window), then call katman_account with refresh: true.`;
141
+ }
142
+ /** Days left in a running Audit Week (7 right after it starts, 1 on its last day). */
143
+ export function planDaysLeft(s, now = new Date()) {
144
+ if (s.plan !== "audit" || !s.planEndsAt)
100
145
  return undefined;
101
- return Math.max(0, Math.ceil((Date.parse(s.trialEndsAt) - now.getTime()) / 86_400_000));
146
+ return Math.max(0, Math.ceil((Date.parse(s.planEndsAt) - now.getTime()) / 86_400_000));
102
147
  }
103
148
  function offlineText(s) {
104
149
  return s.source === "offline-grace" ? ` · offline: using the check from ${day(s.checkedAt)}, works until ${day(s.graceUntil)}` : "";
105
150
  }
106
- /** Why a Pro tool didn't run (one or two sentences). */
107
- export function reasonText(s, feature) {
151
+ /** "Audit Week ended 2026-10-01" (for a table cell or a status line). */
152
+ export function endedPhrase(s) {
153
+ const what = s.endedPlan === "pro" ? "Katman Pro ended" : s.endedPlan === "audit" ? "Audit Week ended" : "plan ended";
154
+ return `${what}${s.planEndsAt ? ` ${day(s.planEndsAt)}` : ""}`;
155
+ }
156
+ /** "Your Audit Week ended on 2026-10-01." */
157
+ function endedText(s, lang = "en") {
158
+ const on = s.planEndsAt ? day(s.planEndsAt) : undefined;
159
+ if (lang === "tr") {
160
+ const what = s.endedPlan === "pro" ? "Katman Pro aboneliğiniz" : s.endedPlan === "audit" ? "Denetim Haftanız" : "Planınız";
161
+ return `${what} ${on ? `${on} tarihinde ` : ""}bitti.`;
162
+ }
163
+ const what = s.endedPlan === "pro" ? "Your Katman Pro subscription" : s.endedPlan === "audit" ? "Your Audit Week" : "Your Katman plan";
164
+ return `${what} ${on ? `ended on ${on}` : "has ended"}.`;
165
+ }
166
+ /** The only way on after an Audit Week (each account gets one), or after Pro ended. */
167
+ const onlyPro = (s) => !!s.planEnded || !!s.auditWeekUsed;
168
+ /** Why a tool didn't run (one or two sentences). `name` is the tool, or the CLI command. */
169
+ export function reasonText(s, o = {}) {
170
+ const tr = o.lang === "tr";
108
171
  switch (s.state) {
109
172
  case "no_key":
110
- return s.hint ?? "No KATMAN_KEY is set in this MCP server's configuration.";
173
+ if (s.hint)
174
+ return s.hint;
175
+ if (tr)
176
+ return "Bu MCP sunucusunun yapılandırmasında KATMAN_KEY yok.";
177
+ return o.surface === "cli" ? "No KATMAN_KEY is set in the environment." : "No KATMAN_KEY is set in this MCP server's configuration.";
111
178
  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_…"}).`;
179
+ if (s.source === "local") {
180
+ return tr
181
+ ? "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."
182
+ : s.hint ?? "KATMAN_KEY doesn't look like a Katman key.";
183
+ }
184
+ 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
185
  case "revoked":
114
- return `This KATMAN_KEY (${s.keyHint ?? "km_…"}) was revoked on katman.pro.`;
186
+ 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
187
  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` : ""}.`;
188
+ return tr
189
+ ? `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` : ""}.`
190
+ : `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
191
  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.`;
192
+ if (s.plan === "none") {
193
+ if (s.planEnded)
194
+ return endedText(s, o.lang);
195
+ if (s.auditWeekUsed)
196
+ 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.";
197
+ return tr ? "Bu hesabın henüz etkin bir Katman Pro planı yok." : "This account has no active Katman Pro plan yet.";
198
+ }
199
+ if (s.plan === "audit" && o.feature && isProOnly(o.feature))
200
+ return proToolText(o.name ?? "This tool", o.surface === "cli" ? "command" : "tool");
201
+ if (s.updateMcpTo) {
202
+ return tr
203
+ ? `Bu katman-mcp (${VERSION}) planınızdaki denetimleri sayamıyor; ${s.updateMcpTo} ya da daha yeni bir sürüme güncelleyene kadar denetim kapalı: npx -y katman-mcp@latest çalıştırın, MCP sunucusunu yeniden başlatın, sonra katman_account aracını refresh: true ile çağırın.`
204
+ : updateText(s);
205
+ }
206
+ return tr
207
+ ? `Planınız katman.pro’ya göre “${o.feature ?? "bu özellik"}” özelliğini içermiyor.`
208
+ : `Your ${s.plan === "pro" ? "Katman Pro" : "Audit Week"} plan doesn't include "${o.feature ?? "this feature"}" according to katman.pro.`;
121
209
  }
122
210
  }
123
- /** One line for katman_start / katman_account. */
211
+ /** One line for katman_start. */
124
212
  export function statusLine(s, now = new Date()) {
125
213
  switch (s.state) {
126
214
  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}`;
215
+ return s.hint ? `**Katman plan:** none · ${s.hint}` : `**Katman plan:** none (no KATMAN_KEY) · get a key at ${PRICING_URL}`;
128
216
  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}`;
217
+ 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
218
  case "revoked":
131
- return `**Katman plan:** free · this KATMAN_KEY was revoked · create a new one at ${KEYS_URL}`;
219
+ return `**Katman plan:** none · this KATMAN_KEY was revoked · create a new one at ${KEYS_URL}`;
132
220
  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`;
221
+ 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
222
  case "valid": {
135
223
  const who = s.emailMasked ? ` · ${s.emailMasked}` : "";
136
- 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)}`;
224
+ if (s.plan === "pro") {
225
+ const used = s.limits ? ` · ${usageText(s.limits, s.usage)}` : "";
226
+ const update = s.updateMcpTo ? ` · update katman-mcp to ${s.updateMcpTo} (npx -y katman-mcp@latest): audits are off until then` : "";
227
+ return `**Katman plan:** ${proPlanName(s.limits)} · every tool on${used}${update}${who}${offlineText(s)}`;
141
228
  }
142
- return `**Katman plan:** free${s.trialEnded ? ` (trial ended ${day(s.trialEndsAt)})` : ""} · Pro tools are off: upgrade at ${BILLING_URL}${who}`;
229
+ if (s.plan === "audit") {
230
+ const left = planDaysLeft(s, now);
231
+ 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)}`;
232
+ }
233
+ const note = s.planEnded ? ` (${endedPhrase(s)})` : s.auditWeekUsed ? " (Audit Week used)" : "";
234
+ const next = s.endedPlan === "pro" ? "renew Katman Pro" : onlyPro(s) ? "upgrade to Katman Pro" : "choose Katman Pro (1 site $49/month, 3 sites $88/month)";
235
+ return `**Katman plan:** none${note} · ${next} at ${BILLING_URL}${who}`;
143
236
  }
144
237
  }
145
238
  }
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;
239
+ /** One line: what to do next, by license state (resources, notices). */
240
+ function nextStepLine(s, feature) {
150
241
  switch (s.state) {
151
242
  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;
243
+ return `Get a key at ${PRICING_URL}, then add KATMAN_KEY to the MCP config (${KEY_DOCS_URL}).`;
155
244
  case "invalid":
156
245
  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;
246
+ return `Create a new key at ${KEYS_URL} and replace KATMAN_KEY in the MCP config.`;
160
247
  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;
248
+ return "Check the internet connection, then call katman_account with refresh: true.";
164
249
  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;
250
+ if (s.plan === "none") {
251
+ return `${s.endedPlan === "pro" ? "Renew" : onlyPro(s) ? "Upgrade to" : "Choose"} Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}. Then call katman_account with refresh: true.`;
252
+ }
253
+ if (s.updateMcpTo)
254
+ return "Run the latest katman-mcp (npx -y katman-mcp@latest), restart the MCP server, then call katman_account with refresh: true.";
255
+ return "Call katman_account with refresh: true; if it persists, run the latest Katman (npx -y katman-mcp@latest).";
256
+ }
257
+ }
258
+ /** katman_start: the recommended next tool isn't in the plan. */
259
+ export function planNotice(tool, feature, s) {
260
+ if (s.state === "valid" && s.plan === "audit" && isProOnly(feature)) {
261
+ return `> **Needs Katman Pro:** ${proToolText(`\`${tool}\``, "tool")}\n> ${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)}` : ""}.`;
168
262
  }
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.`;
263
+ return `> **Not in your plan:** \`${tool}\` won't run: ${reasonText(s, { feature, name: tool })} ${nextStepLine(s, feature)} Tell the user.`;
170
264
  }
171
265
  // ---------------------------------------------------------------------------------------------------------------
172
266
  // Snippets
@@ -177,7 +271,7 @@ export function installSnippets() {
177
271
  "claude mcp remove katman --scope user # only if Katman is already added",
178
272
  "claude mcp add katman --scope user -e KATMAN_KEY=km_live_… -e OPENROUTER_API_KEY=sk-or-… -- npx -y katman-mcp",
179
273
  "```",
180
- "OPENROUTER_API_KEY is optional (only for API visibility runs). With the Katman Claude Code plugin, set its `katman_key` option instead.",
274
+ "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
275
  "",
182
276
  "**Cursor**: in `~/.cursor/mcp.json` (your user file, not a committed project file); keep any other env entries you have:",
183
277
  "```json",
@@ -187,104 +281,315 @@ export function installSnippets() {
187
281
  "**VS Code**: in `.vscode/mcp.json`; VS Code asks for the key once and keeps it in its secret storage:",
188
282
  "```json",
189
283
  "{",
190
- ' "inputs": [{ "type": "promptString", "id": "katman_key", "description": "Katman key (katman.pro/app)", "password": true }],',
284
+ ' "inputs": [{ "type": "promptString", "id": "katman_key", "description": "Katman key (katman.pro/app/mcp)", "password": true }],',
191
285
  ' "servers": { "katman": { "type": "stdio", "command": "npx", "args": ["-y", "katman-mcp"], "env": { "KATMAN_KEY": "${input:katman_key}" } } }',
192
286
  "}",
193
287
  "```",
194
288
  ].join("\n");
195
289
  }
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` : "";
290
+ /** What to do next, by license state. `rerun` is the tool to call again afterwards. */
291
+ export function howToText(s, o = {}) {
292
+ const tr = o.lang === "tr";
293
+ const again = o.rerun ? (tr ? `, sonra ${o.rerun} aracını yeniden çağırın` : ` and run ${o.rerun} again`) : "";
294
+ const restart = tr
295
+ ? `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}.`
296
+ : `Restart the Katman MCP server (or reload the editor window) so it reads the key, then call katman_account to confirm${again}.`;
297
+ const refresh = tr ? `katman_account aracını refresh: true ile çağırın${again}.` : `call katman_account with refresh: true${again}.`;
298
+ const Refresh = upperFirst(refresh);
200
299
  switch (s.state) {
201
300
  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");
301
+ return (tr
302
+ ? [
303
+ "### Anahtar alın",
304
+ `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).`,
305
+ `2. Anahtarı MCP yapılandırmasına \`KATMAN_KEY\` olarak ekleyin (her editör için adım adım: ${KEY_DOCS_URL}):`,
306
+ ]
307
+ : [
308
+ "### Get a key",
309
+ `1. Choose a plan at ${PRICING_URL}, then open ${KEYS_URL} → **Create key** and copy the key (km_live_…; it's shown once).`,
310
+ `2. Add it to the MCP config as \`KATMAN_KEY\` (step by step for each editor: ${KEY_DOCS_URL}):`,
311
+ ]).concat(["", installSnippets(), "", `3. ${restart}`]).join("\n");
213
312
  case "invalid":
214
313
  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");
314
+ return (tr
315
+ ? [
316
+ "### Geçerli bir anahtar kullanın",
317
+ `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"}.`,
318
+ "2. MCP yapılandırmasındaki `KATMAN_KEY` değerini değiştirin:",
319
+ ]
320
+ : [
321
+ "### Use a valid key",
322
+ `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"}.`,
323
+ "2. Replace `KATMAN_KEY` in the MCP config:",
324
+ ]).concat(["", installSnippets(), "", `3. ${restart}`]).join("\n");
325
+ case "unreachable": {
326
+ const api = /KATMAN_API_URL/.test(s.error ?? "") ? ", KATMAN_API_URL" : "";
327
+ return tr
328
+ ? `### 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}`
329
+ : `### 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}`;
330
+ }
230
331
  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");
332
+ if (s.plan === "none" && s.endedPlan === "pro") {
333
+ return tr
334
+ ? `### Katman Pro’yu yenileyin\nAnahtarınız aynı kalır: ${BILLING_URL} adresinden yeniden abone olun (${PRO_PRICE_NOTE_TR}). Yeniden abone oldunuz mu? ${refresh}`
335
+ : `### Renew Katman Pro\nThe key stays the same: resubscribe at ${BILLING_URL}. ${PRO_PRICE_NOTE}. Already renewed? ${Refresh}`;
336
+ }
337
+ if (s.plan === "none" && onlyPro(s)) {
338
+ return tr
339
+ ? `### Katman Pro’ya geçin\n${PRO_PRICE_NOTE_TR}: ${BILLING_URL}. Anahtar aynı kalır. Geçtiniz mi? ${refresh}`
340
+ : `### Upgrade to Katman Pro\n${PRO_PRICE_NOTE}. Upgrade at ${BILLING_URL}; the key stays the same. Already upgraded? ${Refresh}`;
237
341
  }
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");
342
+ if (s.plan === "none") {
343
+ return tr
344
+ ? `### Bir plan seçin\n${plansList("tr")}\n\nSatın alma: ${BILLING_URL}; anahtar aynı kalır. Satın aldıktan sonra ${refresh}`
345
+ : `### Choose a plan\n${plansList("en")}\n\nBuy at ${BILLING_URL}; the key stays the same. Then ${refresh}`;
346
+ }
347
+ if (s.plan === "audit" && o.feature && isProOnly(o.feature)) {
348
+ return `### Upgrade to Katman Pro\n${PRO_PRICE_NOTE}. Upgrade at ${BILLING_URL}; the key stays the same. Your Audit Week tools keep working${s.planEndsAt ? ` until ${day(s.planEndsAt)}` : ""}. Already upgraded? ${Refresh}`;
349
+ }
350
+ if (s.updateMcpTo) {
351
+ return tr
352
+ ? `### Katman’ı güncelleyin\nBu sürüm planınızdaki denetimleri sayamıyor. En son sürümü çalıştırın (npx -y katman-mcp@latest), MCP sunucusunu yeniden başlatın ve ${refresh}`
353
+ : `### Update Katman\nThis version can't count audits against your plan. Run the latest version (npx -y katman-mcp@latest), restart the MCP server (or reload the editor window), then ${refresh}`;
354
+ }
355
+ return tr
356
+ ? `### 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.`
357
+ : `### Update Katman\nRun the latest version (npx -y katman-mcp@latest) and ${refresh} If it persists, write to hello@katman.pro.`;
358
+ }
359
+ }
360
+ /** The two plans as a short list. */
361
+ function plansList(lang) {
362
+ return lang === "tr"
363
+ ? [
364
+ "- **Katman Pro, 1 site**: ayda 49 $. 1 site, ayda 5 denetim. Tüm araçlar: P0–P14 prompt’ları, sayfa brief’leri ve dosya üretimi dâhil.",
365
+ "- **Katman Pro, 3 site**: ayda 88 $. 3 site, ayda 30 denetim. Tüm araçlar, 1 siteli plandakiyle aynı.",
366
+ "",
367
+ AUDIT_COUNT_NOTE_TR,
368
+ ].join("\n")
369
+ : [
370
+ "- **Katman Pro, 1 site**: $49/month. 1 site, 5 audits a month. Every tool, including the P0–P14 prompts, page briefs and file generation.",
371
+ "- **Katman Pro, 3 sites**: $88/month. 3 sites, 30 audits a month. Every tool, the same as the 1-site plan.",
372
+ "",
373
+ AUDIT_COUNT_NOTE,
374
+ ].join("\n");
375
+ }
376
+ /** The two plans as a table (katman_start without a plan). */
377
+ function plansTable(lang) {
378
+ return lang === "tr"
379
+ ? mdTable(["Plan", "Fiyat", "Kapsadığı"], [
380
+ ["**Katman Pro, 1 site**", "Ayda 49 $", "1 site, ayda 5 denetim. Tüm araçlar: P0–P14 prompt’ları, sayfa brief’leri, dosya üretimi (robots.txt, llms.txt, JSON-LD), IndexNow, zamanlanmış izleme ve katman.pro paneli dâhil."],
381
+ ["**Katman Pro, 3 site**", "Ayda 88 $", "3 site, ayda 30 denetim. Tüm araçlar, 1 siteli plandakiyle aynı."],
382
+ ])
383
+ : mdTable(["Plan", "Price", "What it covers"], [
384
+ ["**Katman Pro, 1 site**", "$49/month", "1 site, 5 audits a month. Every tool, including the P0–P14 prompts, page briefs, file generation (robots.txt, llms.txt, JSON-LD), IndexNow, scheduled monitoring and the katman.pro dashboard."],
385
+ ["**Katman Pro, 3 sites**", "$88/month", "3 sites, 30 audits a month. Every tool, the same as the 1-site plan."],
386
+ ]);
387
+ }
388
+ function headline(name, feature, s) {
389
+ switch (s.state) {
390
+ case "no_key":
391
+ return `${name} needs a Katman licence key`;
392
+ case "invalid":
393
+ case "revoked":
394
+ return `${name} needs a valid Katman key`;
395
+ case "unreachable":
396
+ return `${name} couldn't check the Katman licence`;
397
+ case "valid":
398
+ if (s.plan === "none")
399
+ return `${name} needs an active Katman plan`;
400
+ return s.plan === "audit" && isProOnly(feature) ? `${name} needs Katman Pro` : `${name} isn't in your Katman plan`;
242
401
  }
243
402
  }
244
403
  export function renderUpsell(o) {
245
404
  if (o.surface === "cli")
246
405
  return renderCliUpsell(o);
247
406
  const where = o.facts?.brand ?? o.facts?.domain;
407
+ const done = doneText(o.facts);
248
408
  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.`,
409
+ `## ${headline(o.tool, o.feature, o.status)}`,
410
+ `**What it would do${where ? ` for ${where}` : ""}:** ${upperFirst(pitch(o.tool, o.args ?? {}, o.facts))}`,
411
+ ...(done ? [done] : []),
412
+ `**Why it didn't run:** ${reasonText(o.status, { feature: o.feature, name: o.tool })}`,
413
+ ...(o.status.state === "no_key" ? [NO_KEY_TEXT] : []),
414
+ howToText(o.status, { rerun: o.tool, feature: o.feature }),
415
+ "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
416
  ].join("\n\n");
256
417
  }
257
418
  function renderCliUpsell(o) {
258
419
  const cmd = o.command ?? o.tool;
420
+ const s = o.status;
259
421
  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)}`,
422
+ `${headline(cmd, o.feature, s)}: it would ${pitch(o.tool, o.args ?? {}, o.facts)}`,
423
+ `Why it didn't run: ${reasonText(s, { feature: o.feature, name: cmd, surface: "cli" })}`,
262
424
  "",
263
425
  ];
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`);
426
+ if (s.state === "valid") {
427
+ if (s.plan === "audit" && isProOnly(o.feature)) {
428
+ L.push(`Upgrade to Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}. The key stays the same. Already upgraded? Run: katman-mcp account --refresh`);
429
+ }
430
+ else if (s.plan === "none" && s.endedPlan === "pro") {
431
+ L.push(`Renew Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}. The key stays the same. Already renewed? Run: katman-mcp account --refresh`);
432
+ }
433
+ else if (s.plan === "none") {
434
+ L.push(`${onlyPro(s) ? "Upgrade to" : "Choose"} Katman Pro at ${BILLING_URL}: ${PRO_PRICE_NOTE}. The key stays the same. Already ${onlyPro(s) ? "upgraded" : "bought"}? Run: katman-mcp account --refresh`);
435
+ }
436
+ else {
437
+ L.push("Run the latest version (npx -y katman-mcp@latest), then: katman-mcp account --refresh. If it persists, write to hello@katman.pro.");
438
+ }
266
439
  }
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");
440
+ else if (s.state === "unreachable") {
441
+ 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
442
  }
270
443
  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}.`);
444
+ if (s.state === "no_key")
445
+ L.push(NO_KEY_TEXT, "");
446
+ 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
447
  }
275
- L.push("", "Free without a key: katman-mcp scan, audit, keywords, models, indexnow (dry run), account.");
448
+ L.push("", "Without a key: katman-mcp account, --help and --version.");
276
449
  return L.join("\n");
277
450
  }
278
- /** structuredContent of an upsell (no secrets). */
451
+ /** The two plans on sale, for structuredContent (no secrets). */
452
+ const PLANS_STRUCTURED = [
453
+ { id: "pro_1", name: "Katman Pro, 1 site", price: "$49/month", sites: 1, auditsPerMonth: 5, url: BUY_PRO_1_URL },
454
+ { id: "pro_3", name: "Katman Pro, 3 sites", price: "$88/month", sites: 3, auditsPerMonth: 30, url: BUY_PRO_3_URL },
455
+ ];
456
+ /** structuredContent of a tool that didn't run for lack of a plan (no secrets). */
279
457
  export function upsellStructured(tool, feature, s) {
280
458
  return {
281
459
  licensed: false,
282
460
  tool,
283
461
  feature,
284
- license: { state: s.state, plan: s.plan, ...(s.trialEnded ? { trialEnded: true, trialEndsAt: s.trialEndsAt } : {}) },
285
- trialUrl: APP_URL,
462
+ /** The plan to buy: every tool is in both Katman Pro plans (the Audit Week is no longer sold). */
463
+ requiredPlan: "pro",
464
+ plans: PLANS_STRUCTURED,
465
+ license: {
466
+ state: s.state,
467
+ plan: s.plan,
468
+ ...(s.planEndsAt ? { planEndsAt: s.planEndsAt } : {}),
469
+ ...(s.planEnded ? { planEnded: true } : {}),
470
+ ...(s.auditWeekUsed ? { auditWeekUsed: true } : {}),
471
+ },
472
+ pricingUrl: PRICING_URL,
286
473
  keysUrl: KEYS_URL,
287
474
  upgradeUrl: BILLING_URL,
475
+ docsUrl: KEY_DOCS_URL,
476
+ };
477
+ }
478
+ // ---------------------------------------------------------------------------------------------------------------
479
+ // katman_start without a plan, gated resources
480
+ /** katman_start without a plan: what Katman does, the two plans and how to set the key. No method walk-through. */
481
+ export function startIntro(s, lang = "en") {
482
+ const L = (en, tr) => (lang === "tr" ? tr : en);
483
+ const gates = GATES.map((g) => `- **${t(g.name, lang)}**: ${t(g.summary, lang)}`).join("\n");
484
+ const steps = STEPS.map((x) => `${x.n} ${t(x.name, lang)}`).join(" → ");
485
+ const heading = s.state === "no_key"
486
+ ? L("Katman needs a licence key", "Katman bir lisans anahtarı ister")
487
+ : s.state === "invalid" || s.state === "revoked"
488
+ ? L("Katman needs a valid key", "Katman geçerli bir anahtar ister")
489
+ : s.state === "unreachable"
490
+ ? L("Katman couldn't check your licence", "Katman lisansınızı kontrol edemedi")
491
+ : L("Katman needs an active plan", "Katman etkin bir plan ister");
492
+ const why = s.state === "no_key" && !s.hint ? undefined : reasonText(s, { feature: "start", name: "katman_start", lang });
493
+ return [
494
+ "# Katman",
495
+ 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."),
496
+ `${L("**3 gates** every public page must pass:", "Her herkese açık sayfanın geçmesi gereken **3 kapı**:")}\n${gates}`,
497
+ L(`**4 steps:** ${steps}, then re-measure every 14 days.`, `**4 adım:** ${steps}; sonra 14 günde bir yeniden ölçüm.`),
498
+ `## ${heading}`,
499
+ ...(why ? [`**${L("Why nothing ran", "Neden çalışmadı")}:** ${why}`] : []),
500
+ ...(s.state === "unreachable" ? [] : [plansTable(lang), lang === "tr" ? AUDIT_COUNT_NOTE_TR : AUDIT_COUNT_NOTE]),
501
+ howToText(s, { rerun: "katman_start", feature: "start", lang }),
502
+ "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.",
503
+ ].join("\n\n");
504
+ }
505
+ /** A gated resource without the plan: its title, why, and what to do. */
506
+ export function resourcePaywall(title, feature, s, then) {
507
+ const why = s.state === "no_key" && !s.hint
508
+ ? NO_KEY_TEXT
509
+ : s.state === "valid" && s.plan === "audit" && isProOnly(feature)
510
+ ? `This is part of Katman Pro. ${AUDIT_COVERS}`
511
+ : `${reasonText(s, { feature })} ${nextStepLine(s, feature)}`;
512
+ return `# ${title}\n\n${why}\n\n${then}`;
513
+ }
514
+ /** The status the no-plan / rejected-key answer of the audit check stands for, so the usual upsell can explain it. */
515
+ function refusedStatus(o) {
516
+ const { use, status } = o;
517
+ if (use.reason === "invalid_key" || use.reason === "revoked") {
518
+ return { state: use.reason === "revoked" ? "revoked" : "invalid", plan: "none", features: [], source: "network", ...(status.keyHint ? { keyHint: status.keyHint } : {}) };
519
+ }
520
+ return { ...status, state: "valid", plan: "none", features: [] };
521
+ }
522
+ const onePro = (l) => l?.auditsPerMonth === 5;
523
+ /** Why this month's allowance doesn't cover the audit (audit_limit / site_limit): one text for the MCP tool and the CLI. */
524
+ function limitLines(o) {
525
+ const { use, domain } = o;
526
+ const lim = use.limits ?? undefined;
527
+ const usage = use.usage;
528
+ const resets = usage ? `Counts start again on ${day(usage.resetsAt)}.` : "Counts start again on the 1st of next month (UTC).";
529
+ const plan = proPlanName(lim);
530
+ const lines = [];
531
+ if (use.reason === "audit_limit") {
532
+ lines.push(lim
533
+ ? `All ${lim.auditsPerMonth} audits of this month are used (${plan}), so Katman didn't audit ${domain}. ${resets}`
534
+ : `This month's audits are used, so Katman didn't audit ${domain}. ${resets}`);
535
+ }
536
+ else {
537
+ const left = lim && usage ? Math.max(0, lim.auditsPerMonth - usage.audits) : undefined;
538
+ lines.push(lim
539
+ ? `${plan} covers ${countOf(lim.sites, "site", "sites")} a month and you already audited ${usage ? usage.sites : lim.sites} this month, so Katman didn't audit ${domain}, a new site. ${resets}`
540
+ : `This plan's sites for this month are used, so Katman didn't audit ${domain}. ${resets}`);
541
+ lines.push(`Audits of a site you already audited this month still work${left !== undefined ? ` (${countOf(left, "audit", "audits")} left)` : ""}.`);
542
+ }
543
+ if (onePro(lim))
544
+ lines.push(`To audit more now: upgrade to ${PRO_3_NOTE} at ${BUY_PRO_3_URL}; the key stays the same. Already upgraded? Call katman_account with refresh: true.`);
545
+ lines.push("Re-reading earlier results doesn't use an audit: the last audit of this project is in .katman/audit.md. Every other tool keeps working.");
546
+ return lines;
547
+ }
548
+ /** An audit that didn't run: this month's audits or sites are used up (explained), or the plan or key isn't valid (the usual upsell). */
549
+ export function renderAuditRefusal(o) {
550
+ const { use } = o;
551
+ if (use.reason !== "audit_limit" && use.reason !== "site_limit") {
552
+ return renderUpsell({ tool: "katman_audit_site", feature: "audit", status: refusedStatus(o), facts: o.facts, args: { site_url: o.domain }, surface: o.surface, command: o.command, example: o.example });
553
+ }
554
+ const lines = limitLines(o);
555
+ if (o.surface === "cli") {
556
+ return [`${o.command ?? "katman-mcp audit"} didn't run: ${use.reason === "audit_limit" ? "this month's audits are used" : "this month's sites are used"}.`, "", ...lines, "", "This month's counts: katman-mcp account"].join("\n");
557
+ }
558
+ return [
559
+ `## katman_audit_site didn't run: ${use.reason === "audit_limit" ? "this month's audits are used" : "this month's sites are used"}`,
560
+ lines.join("\n\n"),
561
+ "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 shows this month's counts.",
562
+ ].join("\n\n");
563
+ }
564
+ /** structuredContent of an audit that didn't run for lack of audits or sites this month (no secrets). */
565
+ export function auditRefusalStructured(o) {
566
+ const { use } = o;
567
+ if (use.reason !== "audit_limit" && use.reason !== "site_limit")
568
+ return upsellStructured("katman_audit_site", "audit", refusedStatus(o));
569
+ return {
570
+ ran: false,
571
+ tool: "katman_audit_site",
572
+ reason: use.reason,
573
+ domain: o.domain,
574
+ ...(use.plan ? { plan: use.plan } : {}),
575
+ ...(use.limits ? { limits: use.limits } : {}),
576
+ ...(use.usage ? { usage: use.usage } : {}),
577
+ upgradeUrl: onePro(use.limits) ? BUY_PRO_3_URL : BILLING_URL,
578
+ };
579
+ }
580
+ /** After an audit that ran: this month's count on a plan with limits, or the note that the count couldn't be checked. */
581
+ export function auditCountLine(use) {
582
+ if (use.unchecked)
583
+ return `> **Audit count not checked:** couldn't reach katman.pro (${use.error ?? "no answer"}), so this audit ran without being counted.`;
584
+ return use.limits ? `**${proPlanName(use.limits)}:** ${usageText(use.limits, use.usage)}.` : undefined;
585
+ }
586
+ /** structuredContent.auditCount of an audit that ran. */
587
+ export function auditCountStructured(use) {
588
+ return {
589
+ counted: !use.unchecked,
590
+ ...(use.unchecked ? { error: use.error ?? "no answer" } : {}),
591
+ ...(use.limits ? { limits: use.limits } : {}),
592
+ ...(use.usage ? { usage: use.usage } : {}),
288
593
  };
289
594
  }
290
595
  export { when as fmtWhen, day as fmtDay };