katman-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -37
- package/dist/cli.js +27 -14
- package/dist/license.js +139 -95
- package/dist/meta.js +16 -6
- package/dist/plans.js +47 -0
- package/dist/tools/account/account.js +51 -30
- package/dist/tools/account/cli.js +4 -2
- package/dist/tools/account/index.js +7 -7
- package/dist/tools/account/sync.js +2 -2
- package/dist/tools/account/upsell.js +305 -117
- package/dist/tools/crawl/cli.js +1 -1
- package/dist/tools/crawl/index.js +16 -13
- package/dist/tools/method/core/start.js +1 -1
- package/dist/tools/method/index.js +27 -17
- package/dist/tools/method/mcp/prompts.js +6 -6
- package/dist/tools/method/mcp/resources.js +19 -16
- package/dist/tools/visibility/index.js +9 -9
- package/dist/tools/visibility/monitoring.js +3 -3
- package/package.json +1 -1
- package/server.json +4 -4
package/dist/license.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// Katman
|
|
1
|
+
// Katman licensing (docs/07-APP-SPEC.md §6). Nothing is free: every tool except katman_account needs KATMAN_KEY
|
|
2
|
+
// with an active plan, the Audit Week (audit and recommendations, 7 days) or Katman Pro (everything); the plans,
|
|
3
|
+
// features and the tool → feature map live in plans.ts.
|
|
2
4
|
//
|
|
3
5
|
// - KATMAN_KEY comes from the environment (the MCP config). It is sent only to KATMAN_API_URL (default
|
|
4
6
|
// https://katman.pro), in the JSON body of POST /api/web/mcp/verify and /api/web/mcp/sync, and is never
|
|
5
7
|
// printed, logged or written anywhere.
|
|
6
|
-
// - Verification runs at most once per `checkAgainAfter` (
|
|
7
|
-
// ~/.katman/license.json under the key's SHA-256 hash (never the key).
|
|
8
|
-
// - Offline grace: when katman.pro can't be reached, the last good answer keeps working for 7 days
|
|
9
|
-
//
|
|
8
|
+
// - Verification runs at most once per `checkAgainAfter` (≤ 24 h), and again as soon as the plan's end date
|
|
9
|
+
// (planEndsAt) has passed. The answer is cached in ~/.katman/license.json under the key's SHA-256 hash (never the key).
|
|
10
|
+
// - Offline grace: when katman.pro can't be reached, the last good answer keeps working for 7 days, never past the
|
|
11
|
+
// end of an Audit Week.
|
|
12
|
+
// - Every tool goes through withPlan() at registration (katman_start through startLicense()); katman_account is the
|
|
13
|
+
// only open tool, and the CLI uses the same gate. Without a key nothing is sent to katman.pro.
|
|
10
14
|
// - Tests inject a LicenseProvider (createServer({ license }) / runCli(args, { license })); there is no env
|
|
11
15
|
// switch that turns the gate off.
|
|
12
16
|
import { createHash } from "node:crypto";
|
|
@@ -15,31 +19,10 @@ import path from "node:path";
|
|
|
15
19
|
import { promises as fs } from "node:fs";
|
|
16
20
|
import { resolveRoot } from "./context.js";
|
|
17
21
|
import { HOMEPAGE, VERSION } from "./meta.js";
|
|
22
|
+
import { AUDIT_FEATURES, PRO_FEATURES, TOOL_FEATURES, toolForFeature } from "./plans.js";
|
|
18
23
|
import { ok } from "./util.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
export const PRO_FEATURES = ["plan", "prompts", "briefs", "generate", "lint", "visibility", "manual_check", "indexnow", "report", "monitoring", "sync"];
|
|
22
|
-
/** Tools that never need a key (the free diagnosis). */
|
|
23
|
-
export const FREE_TOOLS = ["katman_start", "katman_research", "katman_scan_code", "katman_audit_site", "katman_keywords", "katman_account"];
|
|
24
|
-
/** Pro tool → license feature. */
|
|
25
|
-
export const PRO_TOOLS = {
|
|
26
|
-
katman_plan: "plan",
|
|
27
|
-
katman_prompt: "prompts",
|
|
28
|
-
katman_page_brief: "briefs",
|
|
29
|
-
katman_generate: "generate",
|
|
30
|
-
katman_lint_content: "lint",
|
|
31
|
-
katman_visibility_run: "visibility",
|
|
32
|
-
katman_manual_check: "manual_check",
|
|
33
|
-
katman_indexnow: "indexnow",
|
|
34
|
-
katman_report: "report",
|
|
35
|
-
katman_setup_monitoring: "monitoring",
|
|
36
|
-
katman_sync: "sync",
|
|
37
|
-
};
|
|
38
|
-
/** Appended to every Pro tool's description. */
|
|
39
|
-
export const PRO_NOTE = " (Katman Pro — needs KATMAN_KEY; 7-day free trial at katman.pro/app)";
|
|
40
|
-
export function toolForFeature(feature) {
|
|
41
|
-
return Object.keys(PRO_TOOLS).find((t) => PRO_TOOLS[t] === feature) ?? `katman_${feature}`;
|
|
42
|
-
}
|
|
24
|
+
import { planNotice, projectFacts, renderUpsell, statusLine, upsellStructured } from "./tools/account/upsell.js";
|
|
25
|
+
export * from "./plans.js";
|
|
43
26
|
export function allows(status, feature) {
|
|
44
27
|
return status.state === "valid" && status.features.includes(feature);
|
|
45
28
|
}
|
|
@@ -114,6 +97,11 @@ export const GRACE_MS = 7 * DAY_S * 1000;
|
|
|
114
97
|
/** After a failed attempt, wait this long before trying the network again while the grace covers us. */
|
|
115
98
|
const RETRY_AFTER_FAILURE_MS = 10 * 60 * 1000;
|
|
116
99
|
const TIMEOUT_MS = 8_000;
|
|
100
|
+
/**
|
|
101
|
+
* Entries this version writes (verify API v2: plans none / audit / pro). Katman 0.1.x cached free / trial / pro with
|
|
102
|
+
* other feature names in the same file; its entries don't count here and are replaced at the next check.
|
|
103
|
+
*/
|
|
104
|
+
const CACHE_CONTRACT = 2;
|
|
117
105
|
export function defaultCacheFile() {
|
|
118
106
|
return path.join(os.homedir(), ".katman", "license.json");
|
|
119
107
|
}
|
|
@@ -150,9 +138,10 @@ function prune(entries, keep, now) {
|
|
|
150
138
|
out.sort((a, b) => Date.parse(b[1].checkedAt ?? "") - Date.parse(a[1].checkedAt ?? ""));
|
|
151
139
|
return Object.fromEntries(out.slice(0, 20));
|
|
152
140
|
}
|
|
141
|
+
/** checkAgainAfter in seconds: at least a minute, at most a day (the server lowers it at the end of an Audit Week). */
|
|
153
142
|
function ttlSeconds(x) {
|
|
154
143
|
const n = Number(x);
|
|
155
|
-
return Number.isFinite(n) && n > 0 ? Math.min(Math.max(Math.round(n), 60),
|
|
144
|
+
return Number.isFinite(n) && n > 0 ? Math.min(Math.max(Math.round(n), 60), DAY_S) : DEFAULT_TTL_S;
|
|
156
145
|
}
|
|
157
146
|
function toIso(x) {
|
|
158
147
|
if (typeof x === "string" && x.trim()) {
|
|
@@ -163,7 +152,11 @@ function toIso(x) {
|
|
|
163
152
|
return new Date(x < 1e12 ? x * 1000 : x).toISOString();
|
|
164
153
|
return undefined;
|
|
165
154
|
}
|
|
166
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Parse a /api/web/mcp/verify response (spec §6, API v2). Only a 401 (or an explicit ok:false) is a rejection.
|
|
157
|
+
* Tolerant of older servers: an unknown plan (trial, free…) is "none", `features` is read from the array whatever the
|
|
158
|
+
* plan says, and trialEndsAt counts as planEndsAt.
|
|
159
|
+
*/
|
|
167
160
|
export function parseVerify(res) {
|
|
168
161
|
let j;
|
|
169
162
|
try {
|
|
@@ -175,16 +168,28 @@ export function parseVerify(res) {
|
|
|
175
168
|
j = undefined;
|
|
176
169
|
}
|
|
177
170
|
if (res.status === 200 && j?.ok === true) {
|
|
178
|
-
const plan = j.plan === "
|
|
171
|
+
const plan = j.plan === "audit" || j.plan === "pro" ? j.plan : "none";
|
|
179
172
|
const features = Array.isArray(j.features)
|
|
180
173
|
? j.features.filter((f) => typeof f === "string").slice(0, 64)
|
|
181
|
-
: plan === "
|
|
182
|
-
? []
|
|
183
|
-
:
|
|
174
|
+
: plan === "pro"
|
|
175
|
+
? [...PRO_FEATURES]
|
|
176
|
+
: plan === "audit"
|
|
177
|
+
? [...AUDIT_FEATURES]
|
|
178
|
+
: [];
|
|
184
179
|
const account = j.account && typeof j.account === "object" ? j.account : {};
|
|
185
180
|
const emailMasked = typeof account.emailMasked === "string" ? account.emailMasked.slice(0, 120) : undefined;
|
|
186
|
-
const
|
|
187
|
-
return {
|
|
181
|
+
const planEndsAt = toIso(j.planEndsAt) ?? toIso(j.trialEndsAt);
|
|
182
|
+
return {
|
|
183
|
+
kind: "good",
|
|
184
|
+
good: {
|
|
185
|
+
plan,
|
|
186
|
+
features,
|
|
187
|
+
checkAgainAfter: ttlSeconds(j.checkAgainAfter),
|
|
188
|
+
...(planEndsAt ? { planEndsAt } : {}),
|
|
189
|
+
...(j.auditWeekUsed === true ? { auditWeekUsed: true } : {}),
|
|
190
|
+
...(emailMasked ? { emailMasked } : {}),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
188
193
|
}
|
|
189
194
|
if (res.status === 401 || (j?.ok === false && (j.error === "invalid_key" || j.error === "revoked"))) {
|
|
190
195
|
return { kind: "rejected", error: j?.error === "revoked" ? "revoked" : "invalid_key" };
|
|
@@ -195,6 +200,30 @@ export function parseVerify(res) {
|
|
|
195
200
|
return { kind: "failed", error: `unexpected redirect (HTTP ${res.status}); check KATMAN_API_URL` };
|
|
196
201
|
return { kind: "failed", error: `HTTP ${res.status}${res.status === 200 ? " (unexpected answer)" : ""}` };
|
|
197
202
|
}
|
|
203
|
+
/** The end date of a plan that grants features and still ran when it was checked; once it passes, check again. */
|
|
204
|
+
function endsAfterCheck(g, checkedAt) {
|
|
205
|
+
const ends = Date.parse(g.planEndsAt ?? "");
|
|
206
|
+
return g.features.length && Number.isFinite(ends) && ends > checkedAt ? ends : undefined;
|
|
207
|
+
}
|
|
208
|
+
function nextCheck(g) {
|
|
209
|
+
const checked = Date.parse(g.checkedAt);
|
|
210
|
+
return new Date(Math.min(checked + g.checkAgainAfter * 1000, endsAfterCheck(g, checked) ?? Infinity)).toISOString();
|
|
211
|
+
}
|
|
212
|
+
/** The offline grace: 7 days after the last good check, never past the end of an Audit Week. */
|
|
213
|
+
function graceUntil(g) {
|
|
214
|
+
const grace = Date.parse(g.checkedAt) + GRACE_MS;
|
|
215
|
+
const ends = g.plan === "pro" ? NaN : Date.parse(g.planEndsAt ?? "");
|
|
216
|
+
return new Date(Number.isFinite(ends) ? Math.min(grace, ends) : grace).toISOString();
|
|
217
|
+
}
|
|
218
|
+
/** A "none" answer right after a paid plan remembers which plan ended and, when the old end date has passed, when. */
|
|
219
|
+
function withEnded(next, prev, at) {
|
|
220
|
+
if (next.plan !== "none" || next.features.length || !prev)
|
|
221
|
+
return next;
|
|
222
|
+
if (prev.plan === "none")
|
|
223
|
+
return prev.ended ? { ...next, ended: prev.ended } : next;
|
|
224
|
+
const ends = Date.parse(prev.planEndsAt ?? "");
|
|
225
|
+
return { ...next, ended: { plan: prev.plan, ...(Number.isFinite(ends) && ends <= at ? { at: prev.planEndsAt } : {}) } };
|
|
226
|
+
}
|
|
198
227
|
export function createLicenseProvider(deps = {}) {
|
|
199
228
|
const env = () => deps.env ?? process.env;
|
|
200
229
|
const post = deps.post ?? postJson;
|
|
@@ -206,34 +235,40 @@ export function createLicenseProvider(deps = {}) {
|
|
|
206
235
|
/** Never let the key appear in an error string (belt and braces: it's only ever in a request body). */
|
|
207
236
|
const scrub = (s, key) => (key ? s.split(key).join(keyHint(key)) : s);
|
|
208
237
|
function fromGood(g, source, at, extra = {}) {
|
|
209
|
-
// A
|
|
210
|
-
|
|
211
|
-
const
|
|
238
|
+
// A plan past its end date gives nothing, even if the cached or offline answer still lists features (no offline
|
|
239
|
+
// extension of an Audit Week). Pro renews into a new period, so its end date only triggers a new check (fresh()).
|
|
240
|
+
const expired = g.plan !== "pro" && !!g.planEndsAt && Date.parse(g.planEndsAt) <= at;
|
|
241
|
+
const ended = expired ? { plan: g.plan === "audit" ? "audit" : g.ended?.plan, at: g.planEndsAt } : g.plan === "none" ? g.ended : undefined;
|
|
242
|
+
const endsAt = g.planEndsAt ?? ended?.at;
|
|
212
243
|
return {
|
|
213
244
|
state: "valid",
|
|
214
|
-
plan:
|
|
215
|
-
features:
|
|
216
|
-
...(
|
|
217
|
-
...(ended ? {
|
|
245
|
+
plan: expired ? "none" : g.plan,
|
|
246
|
+
features: expired ? [] : [...g.features],
|
|
247
|
+
...(endsAt ? { planEndsAt: endsAt } : {}),
|
|
248
|
+
...(ended ? { planEnded: true, ...(ended.plan ? { endedPlan: ended.plan } : {}) } : {}),
|
|
249
|
+
...(g.auditWeekUsed || (expired && g.plan === "audit") ? { auditWeekUsed: true } : {}),
|
|
218
250
|
...(g.emailMasked ? { emailMasked: g.emailMasked } : {}),
|
|
219
251
|
checkedAt: g.checkedAt,
|
|
220
|
-
nextCheckAt:
|
|
252
|
+
nextCheckAt: nextCheck(g),
|
|
221
253
|
source,
|
|
222
254
|
...extra,
|
|
223
255
|
};
|
|
224
256
|
}
|
|
225
257
|
function rejected(error, source, key) {
|
|
226
|
-
return { state: error === "revoked" ? "revoked" : "invalid", plan: "
|
|
258
|
+
return { state: error === "revoked" ? "revoked" : "invalid", plan: "none", features: [], source, keyHint: keyHint(key) };
|
|
227
259
|
}
|
|
228
260
|
function fresh(e, at) {
|
|
229
261
|
if (!e.result || !e.checkedAt)
|
|
230
262
|
return false;
|
|
231
|
-
const
|
|
263
|
+
const checked = Date.parse(e.checkedAt);
|
|
264
|
+
const age = at - checked;
|
|
232
265
|
if (!(age >= 0))
|
|
233
266
|
return false; // unparsable or in the future (clock change): check again
|
|
234
267
|
if (!e.result.ok)
|
|
235
268
|
return age < REJECTED_TTL_S * 1000;
|
|
236
|
-
|
|
269
|
+
// Once the plan's end date has passed, check again at once: an Audit Week ends, Pro renews into a new period.
|
|
270
|
+
const ends = endsAfterCheck(e.result, checked);
|
|
271
|
+
if (ends !== undefined && ends <= at)
|
|
237
272
|
return false;
|
|
238
273
|
return age < e.result.checkAgainAfter * 1000;
|
|
239
274
|
}
|
|
@@ -249,8 +284,12 @@ export function createLicenseProvider(deps = {}) {
|
|
|
249
284
|
const server = base.url ?? HOMEPAGE;
|
|
250
285
|
const cache = await readCache(cacheFile);
|
|
251
286
|
const cached = cache.entries[hash];
|
|
252
|
-
const entry = cached && (cached.base ?? HOMEPAGE) === server ? cached : {};
|
|
287
|
+
const entry = cached && cached.contract === CACHE_CONTRACT && (cached.base ?? HOMEPAGE) === server ? cached : {};
|
|
253
288
|
const hint = keyHint(key);
|
|
289
|
+
const offline = (g, error) => ({
|
|
290
|
+
...fromGood(g, "offline-grace", at, { graceUntil: graceUntil(g), ...(error ? { error } : {}) }),
|
|
291
|
+
keyHint: hint,
|
|
292
|
+
});
|
|
254
293
|
if (!refresh && fresh(entry, at)) {
|
|
255
294
|
const r = entry.result;
|
|
256
295
|
return r.ok ? { ...fromGood({ ...r, checkedAt: entry.checkedAt }, "cache", at), keyHint: hint } : rejected(r.error, "cache", key);
|
|
@@ -258,7 +297,7 @@ export function createLicenseProvider(deps = {}) {
|
|
|
258
297
|
// Offline and recently failed: don't wait for another timeout on every tool call.
|
|
259
298
|
const failedAt = Date.parse(entry.failedAt ?? "");
|
|
260
299
|
if (!refresh && Number.isFinite(failedAt) && at - failedAt >= 0 && at - failedAt < RETRY_AFTER_FAILURE_MS && withinGrace(entry, at)) {
|
|
261
|
-
return
|
|
300
|
+
return offline(entry.good, entry.failure);
|
|
262
301
|
}
|
|
263
302
|
const res = base.url
|
|
264
303
|
? await post(`${base.url}/api/web/mcp/verify`, { key, mcpVersion: version }, { timeoutMs })
|
|
@@ -267,21 +306,22 @@ export function createLicenseProvider(deps = {}) {
|
|
|
267
306
|
const checkedAt = new Date(at).toISOString();
|
|
268
307
|
let status;
|
|
269
308
|
if (parsed.kind === "good") {
|
|
270
|
-
|
|
271
|
-
|
|
309
|
+
const good = withEnded(parsed.good, entry.good, at);
|
|
310
|
+
cache.entries[hash] = { contract: CACHE_CONTRACT, base: server, checkedAt, result: { ok: true, ...good }, good: { ...good, checkedAt } };
|
|
311
|
+
status = { ...fromGood({ ...good, checkedAt }, "network", at), keyHint: hint };
|
|
272
312
|
}
|
|
273
313
|
else if (parsed.kind === "rejected") {
|
|
274
|
-
cache.entries[hash] = { base: server, checkedAt, result: { ok: false, error: parsed.error } };
|
|
314
|
+
cache.entries[hash] = { contract: CACHE_CONTRACT, base: server, checkedAt, result: { ok: false, error: parsed.error } };
|
|
275
315
|
status = rejected(parsed.error, "network", key);
|
|
276
316
|
}
|
|
277
317
|
else {
|
|
278
318
|
const error = scrub(parsed.error, key);
|
|
279
|
-
cache.entries[hash] = { ...entry, base: server, failedAt: checkedAt, failure: error };
|
|
319
|
+
cache.entries[hash] = { ...entry, contract: CACHE_CONTRACT, base: server, failedAt: checkedAt, failure: error };
|
|
280
320
|
status = withinGrace(entry, at)
|
|
281
|
-
?
|
|
321
|
+
? offline(entry.good, error)
|
|
282
322
|
: {
|
|
283
323
|
state: "unreachable",
|
|
284
|
-
plan: "
|
|
324
|
+
plan: "none",
|
|
285
325
|
features: [],
|
|
286
326
|
source: "network",
|
|
287
327
|
error,
|
|
@@ -299,11 +339,11 @@ export function createLicenseProvider(deps = {}) {
|
|
|
299
339
|
async status(opts = {}) {
|
|
300
340
|
const { key, hint } = readKey(env());
|
|
301
341
|
if (!key)
|
|
302
|
-
return { state: "no_key", plan: "
|
|
342
|
+
return { state: "no_key", plan: "none", features: [], source: "none", ...(hint ? { hint } : {}) };
|
|
303
343
|
if (!KEY_RE.test(key)) {
|
|
304
344
|
return {
|
|
305
345
|
state: "invalid",
|
|
306
|
-
plan: "
|
|
346
|
+
plan: "none",
|
|
307
347
|
features: [],
|
|
308
348
|
source: "local",
|
|
309
349
|
keyHint: keyHint(key),
|
|
@@ -345,7 +385,7 @@ export function createLicenseProvider(deps = {}) {
|
|
|
345
385
|
return { ok: true, status: res.status, dashboardUrl: dashboardUrl(base.url, j.url ?? j.dashboardUrl, projectId), ...(projectId ? { projectId } : {}), ...(message ? { message } : {}) };
|
|
346
386
|
}
|
|
347
387
|
const code = typeof j.error === "string" ? j.error.slice(0, 60) : undefined;
|
|
348
|
-
const error = res.status === 0 ? "network" : res.status === 401 ? (code === "revoked" ? "revoked" : "invalid_key") : res.status === 402 || res.status === 403 ? code ?? "
|
|
388
|
+
const error = res.status === 0 ? "network" : res.status === 401 ? (code === "revoked" ? "revoked" : "invalid_key") : res.status === 402 || res.status === 403 ? code ?? "pro_required" : code ?? `HTTP ${res.status}`;
|
|
349
389
|
return { ok: false, status: res.status, error, ...(res.status === 0 && res.error ? { message: scrub(res.error, key) } : message ? { message } : {}) };
|
|
350
390
|
},
|
|
351
391
|
};
|
|
@@ -375,24 +415,26 @@ export function licenseOf(ctx) {
|
|
|
375
415
|
}
|
|
376
416
|
// ---------------------------------------------------------------------------------------------------------------
|
|
377
417
|
// Gate
|
|
378
|
-
|
|
379
|
-
let status;
|
|
418
|
+
async function statusOf(provider) {
|
|
380
419
|
try {
|
|
381
|
-
|
|
420
|
+
return await provider.status();
|
|
382
421
|
}
|
|
383
422
|
catch (e) {
|
|
384
|
-
|
|
423
|
+
return { state: "unreachable", plan: "none", features: [], source: "network", error: e instanceof Error ? e.message : String(e) };
|
|
385
424
|
}
|
|
425
|
+
}
|
|
426
|
+
export async function requireFeature(feature, provider) {
|
|
427
|
+
const status = await statusOf(provider);
|
|
386
428
|
return { allowed: allows(status, feature), status };
|
|
387
429
|
}
|
|
388
430
|
/**
|
|
389
|
-
* Wrap a
|
|
431
|
+
* Wrap a tool's handler at registration. Without a plan that includes the feature it returns a short explanation
|
|
390
432
|
* (not isError, so the agent relays it) instead of running the tool.
|
|
391
433
|
*/
|
|
392
|
-
export function
|
|
434
|
+
export function withPlan(ctx, feature, handler) {
|
|
393
435
|
const tool = toolForFeature(feature);
|
|
394
436
|
return async (args, extra) => {
|
|
395
|
-
const gate = await
|
|
437
|
+
const gate = await requireFeature(feature, licenseOf(ctx));
|
|
396
438
|
if (gate.allowed)
|
|
397
439
|
return handler(args, extra);
|
|
398
440
|
let facts;
|
|
@@ -407,42 +449,44 @@ export function withPro(ctx, feature, handler) {
|
|
|
407
449
|
return ok(renderUpsell({ tool, feature, status: gate.status, facts, args: a, surface: "mcp" }), upsellStructured(tool, feature, gate.status));
|
|
408
450
|
};
|
|
409
451
|
}
|
|
410
|
-
/**
|
|
411
|
-
* For katman_start: a one-line plan status when KATMAN_KEY is set (no line and no network without it), and a
|
|
412
|
-
* notice when the recommended next tool is a Pro tool the license doesn't cover.
|
|
413
|
-
*/
|
|
452
|
+
/** For katman_start: the gate, the plan line, and a notice when the recommended next tool isn't in the plan. */
|
|
414
453
|
export async function startLicense(ctx) {
|
|
415
454
|
const provider = licenseOf(ctx);
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
status = await provider.status();
|
|
419
|
-
}
|
|
420
|
-
catch (e) {
|
|
421
|
-
status = { state: "unreachable", plan: "free", features: [], source: "network", error: e instanceof Error ? e.message : String(e) };
|
|
422
|
-
}
|
|
423
|
-
const keySet = provider.hasKey();
|
|
455
|
+
const status = await statusOf(provider);
|
|
424
456
|
return {
|
|
425
|
-
|
|
457
|
+
allowed: allows(status, "start"),
|
|
458
|
+
status,
|
|
459
|
+
line: statusLine(status),
|
|
426
460
|
notice: (tool) => {
|
|
427
|
-
const feature =
|
|
428
|
-
return feature && !allows(status, feature) ?
|
|
461
|
+
const feature = Object.hasOwn(TOOL_FEATURES, tool) ? TOOL_FEATURES[tool] : undefined;
|
|
462
|
+
return feature && !allows(status, feature) ? planNotice(tool, feature, status) : undefined;
|
|
463
|
+
},
|
|
464
|
+
structured: {
|
|
465
|
+
keySet: provider.hasKey(),
|
|
466
|
+
state: status.state,
|
|
467
|
+
plan: status.plan,
|
|
468
|
+
...(status.planEndsAt ? { planEndsAt: status.planEndsAt } : {}),
|
|
469
|
+
...(status.planEnded ? { planEnded: true } : {}),
|
|
429
470
|
},
|
|
430
|
-
structured: { keySet, state: status.state, plan: status.plan, ...(status.trialEndsAt ? { trialEndsAt: status.trialEndsAt } : {}) },
|
|
431
471
|
};
|
|
432
472
|
}
|
|
433
473
|
// ---------------------------------------------------------------------------------------------------------------
|
|
434
474
|
// CLI
|
|
435
|
-
/** Exit code of a
|
|
436
|
-
export const
|
|
437
|
-
/** CLI command → feature
|
|
438
|
-
export
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
475
|
+
/** Exit code of a CLI command without the plan it needs. */
|
|
476
|
+
export const EXIT_NEEDS_PLAN = 4;
|
|
477
|
+
/** CLI command → feature. Every command needs its feature except the OPEN_COMMANDS (and --help / --version). */
|
|
478
|
+
export const CLI_FEATURES = {
|
|
479
|
+
scan: "scan",
|
|
480
|
+
audit: "audit",
|
|
481
|
+
keywords: "keywords",
|
|
482
|
+
plan: "plan",
|
|
483
|
+
report: "report",
|
|
484
|
+
check: "visibility",
|
|
485
|
+
models: "visibility",
|
|
486
|
+
indexnow: "indexnow",
|
|
487
|
+
};
|
|
488
|
+
/** Commands that always work, even without a key. */
|
|
489
|
+
export const OPEN_COMMANDS = ["account"];
|
|
490
|
+
export function cliFeature(cmd) {
|
|
491
|
+
return cmd !== undefined && Object.hasOwn(CLI_FEATURES, cmd) ? CLI_FEATURES[cmd] : undefined;
|
|
448
492
|
}
|
package/dist/meta.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.2.0";
|
|
2
2
|
export const HOMEPAGE = "https://katman.pro";
|
|
3
|
-
/**
|
|
3
|
+
/** Plans (docs/07-APP-SPEC.md §1): nothing is free. Audit Week = $3 once for 7 days; Katman Pro = the founding price. */
|
|
4
4
|
export const APP_URL = `${HOMEPAGE}/app`;
|
|
5
5
|
export const KEYS_URL = `${APP_URL}/mcp`;
|
|
6
6
|
export const BILLING_URL = `${APP_URL}/billing`;
|
|
7
|
-
export const
|
|
7
|
+
export const PRICING_URL = `${HOMEPAGE}/pricing`;
|
|
8
|
+
/** How to add KATMAN_KEY to each editor's MCP config. */
|
|
9
|
+
export const KEY_DOCS_URL = `${HOMEPAGE}/docs/getting-started`;
|
|
10
|
+
export const AUDIT_WEEK_DAYS = 7;
|
|
11
|
+
export const AUDIT_WEEK_PRICE = "$3";
|
|
8
12
|
export const PRO_PRICE = "$29/month";
|
|
9
|
-
|
|
13
|
+
/** Audit Week in one phrase. */
|
|
14
|
+
export const AUDIT_PRICE_NOTE = "$3 once for 7 days, no auto-renewal, one per account";
|
|
15
|
+
/** Katman Pro in one phrase. */
|
|
16
|
+
export const PRO_PRICE_NOTE = "$29/month founding price for the first 100 customers, locked while you stay subscribed (later $49), or $290/year";
|
|
10
17
|
/** Sent to the client model at initialization. Keep it short and imperative. */
|
|
11
18
|
export const SERVER_INSTRUCTIONS = `Katman makes this project's public website readable, quotable and recommendable by AI assistants (ChatGPT, Claude, Gemini, Perplexity, Copilot) and findable in Google, Bing and Brave.
|
|
12
19
|
|
|
@@ -15,8 +22,11 @@ It follows the Katman method: 4 steps (1 Research → 2 Plan → 3 Build → 4 V
|
|
|
15
22
|
- Quote: the first 1–3 sentences answer the question on their own, with the brand's one-sentence description.
|
|
16
23
|
- Fit: there is a page written for the situation the customer describes to the assistant.
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
Plans: every tool except katman_account needs KATMAN_KEY with an active plan in the MCP config.
|
|
26
|
+
- Audit Week ($3 for 7 days): katman_start, katman_research, katman_scan_code, katman_audit_site, katman_keywords, katman_plan, katman_lint_content, katman_visibility_run, katman_manual_check, katman_report. Audit and recommendations only: it never builds pages or writes site files.
|
|
27
|
+
- Katman Pro ($29/month founding price, or $290/year): all of the above plus katman_prompt (P0–P14), katman_page_brief, katman_generate, katman_indexnow, katman_setup_monitoring and katman_sync.
|
|
28
|
+
Without the right plan a tool returns a short explanation instead of running: pass it on to the user. katman_account always works and shows the plan, the days left and how to get a key.
|
|
29
|
+
Never try to bypass or work around the licence (no editing Katman's code or cache, no made-up keys).
|
|
20
30
|
|
|
21
31
|
How to work:
|
|
22
32
|
1. Always call katman_start first; it tells you the next step. Pass project_root (absolute path of the project) and site_url when you know them.
|
package/dist/plans.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Katman plans and license features (docs/07-APP-SPEC.md §1 and §6). Nothing is free: every tool except
|
|
2
|
+
// katman_account needs KATMAN_KEY with an active plan. Shared by the gate (license.ts) and its texts
|
|
3
|
+
// (tools/account/upsell.ts); no imports, so both sides can use it.
|
|
4
|
+
/** Audit Week ($3 for 7 days): audit and recommendations. It never builds pages or writes site files. */
|
|
5
|
+
export const AUDIT_FEATURES = ["start", "research", "scan", "audit", "keywords", "plan", "lint", "visibility", "manual_check", "report"];
|
|
6
|
+
/** Katman Pro: the Audit Week set plus the tools that build pages and keep things running. */
|
|
7
|
+
export const PRO_FEATURES = [...AUDIT_FEATURES, "prompts", "briefs", "generate", "indexnow", "monitoring", "sync"];
|
|
8
|
+
/** Tool → license feature. Every tool needs its feature, except the OPEN_TOOLS. */
|
|
9
|
+
export const TOOL_FEATURES = {
|
|
10
|
+
katman_start: "start",
|
|
11
|
+
katman_research: "research",
|
|
12
|
+
katman_scan_code: "scan",
|
|
13
|
+
katman_audit_site: "audit",
|
|
14
|
+
katman_keywords: "keywords",
|
|
15
|
+
katman_plan: "plan",
|
|
16
|
+
katman_lint_content: "lint",
|
|
17
|
+
katman_visibility_run: "visibility",
|
|
18
|
+
katman_manual_check: "manual_check",
|
|
19
|
+
katman_report: "report",
|
|
20
|
+
katman_prompt: "prompts",
|
|
21
|
+
katman_page_brief: "briefs",
|
|
22
|
+
katman_generate: "generate",
|
|
23
|
+
katman_indexnow: "indexnow",
|
|
24
|
+
katman_setup_monitoring: "monitoring",
|
|
25
|
+
katman_sync: "sync",
|
|
26
|
+
};
|
|
27
|
+
/** Tools that always work, even without a key. */
|
|
28
|
+
export const OPEN_TOOLS = ["katman_account"];
|
|
29
|
+
/** Appended to the description of every Audit Week tool. */
|
|
30
|
+
export const AUDIT_NOTE = " (Audit Week or Pro — needs KATMAN_KEY)";
|
|
31
|
+
/** Appended to the description of every Pro-only tool. */
|
|
32
|
+
export const PRO_NOTE = " (Katman Pro — needs KATMAN_KEY)";
|
|
33
|
+
/** True for the features only Katman Pro unlocks (not in the Audit Week). */
|
|
34
|
+
export function isProOnly(feature) {
|
|
35
|
+
return !AUDIT_FEATURES.includes(feature);
|
|
36
|
+
}
|
|
37
|
+
/** The description note for a tool's feature. */
|
|
38
|
+
export function tierNote(feature) {
|
|
39
|
+
return isProOnly(feature) ? PRO_NOTE : AUDIT_NOTE;
|
|
40
|
+
}
|
|
41
|
+
export function toolForFeature(feature) {
|
|
42
|
+
return Object.keys(TOOL_FEATURES).find((t) => TOOL_FEATURES[t] === feature) ?? `katman_${feature}`;
|
|
43
|
+
}
|
|
44
|
+
/** The tools of one tier: "audit" = the Audit Week tools (also in Pro), "pro" = the Pro-only tools. */
|
|
45
|
+
export function toolsOf(tier) {
|
|
46
|
+
return Object.keys(TOOL_FEATURES).filter((t) => isProOnly(TOOL_FEATURES[t]) === (tier === "pro"));
|
|
47
|
+
}
|
|
@@ -1,64 +1,85 @@
|
|
|
1
|
-
// katman_account (
|
|
2
|
-
// email, last check, and how to
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
// katman_account (always open, even without a key): the Katman plan behind KATMAN_KEY (none, Audit Week with the
|
|
2
|
+
// days left, or Katman Pro), the tools it unlocks, the masked account email, the last check, and how to get a key
|
|
3
|
+
// or upgrade. Never shows the key.
|
|
4
|
+
import { OPEN_TOOLS, TOOL_FEATURES, allows, toolsOf } from "../../license.js";
|
|
5
|
+
import { APP_URL, BILLING_URL, KEYS_URL, PRICING_URL, PRO_PRICE_NOTE } from "../../meta.js";
|
|
6
|
+
import { NO_KEY_TEXT, endedPhrase, fmtDay, fmtWhen, howToText, planDaysLeft, reasonText } from "./upsell.js";
|
|
7
|
+
function planCell(s, left) {
|
|
8
|
+
if (s.state === "no_key")
|
|
9
|
+
return s.hint ? "None (KATMAN_KEY placeholder not filled)" : "None (no KATMAN_KEY)";
|
|
10
|
+
if (s.state !== "valid")
|
|
11
|
+
return `None (key ${s.state === "unreachable" ? "not verified" : s.state})`;
|
|
12
|
+
if (s.plan === "audit")
|
|
13
|
+
return `Audit Week: ${left ?? "?"} day${left === 1 ? "" : "s"} left (ends ${fmtDay(s.planEndsAt)}; no auto-renewal)`;
|
|
14
|
+
if (s.plan === "pro")
|
|
15
|
+
return "Katman Pro";
|
|
16
|
+
return `No active plan${s.planEnded ? ` (${endedPhrase(s)})` : s.auditWeekUsed ? " (Audit Week used)" : ""}`;
|
|
17
|
+
}
|
|
18
|
+
/** "on", "3 of 10 on" or "off". */
|
|
19
|
+
function onText(tools, s) {
|
|
20
|
+
const on = tools.filter((t) => allows(s, TOOL_FEATURES[t])).length;
|
|
21
|
+
return on === tools.length ? "on" : on ? `${on} of ${tools.length} on` : "off";
|
|
22
|
+
}
|
|
6
23
|
export async function runAccount(provider, opts = {}) {
|
|
7
24
|
const now = opts.now ?? new Date();
|
|
8
25
|
const s = await provider.status({ refresh: opts.refresh });
|
|
9
|
-
const left =
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
? "Free (no KATMAN_KEY)"
|
|
14
|
-
: s.state !== "valid"
|
|
15
|
-
? `Free (key ${s.state === "unreachable" ? "not verified" : s.state})`
|
|
16
|
-
: s.plan === "trial"
|
|
17
|
-
? `Trial: ${left ?? "?"} day${left === 1 ? "" : "s"} left (ends ${fmtDay(s.trialEndsAt)})`
|
|
18
|
-
: s.plan === "pro"
|
|
19
|
-
? "Katman Pro"
|
|
20
|
-
: `Free${s.trialEnded ? ` (trial ended ${fmtDay(s.trialEndsAt)})` : ""}`;
|
|
26
|
+
const left = planDaysLeft(s, now);
|
|
27
|
+
const auditTools = toolsOf("audit");
|
|
28
|
+
const proTools = toolsOf("pro");
|
|
29
|
+
const on = Object.keys(TOOL_FEATURES).filter((t) => allows(s, TOOL_FEATURES[t]));
|
|
21
30
|
const source = s.source === "cache" ? "cached answer" : s.source === "network" ? "just checked" : s.source === "offline-grace" ? `offline grace until ${fmtWhen(s.graceUntil)}` : s.source === "local" ? "not sent: malformed key" : "no key";
|
|
22
31
|
const rows = [
|
|
23
|
-
["Plan", planCell],
|
|
32
|
+
["Plan", planCell(s, left)],
|
|
24
33
|
["Account", s.emailMasked ?? "–"],
|
|
25
34
|
["Key", s.state === "no_key" ? (s.hint ? "placeholder, not filled" : "not set (KATMAN_KEY)") : `${s.keyHint ?? "km_…"} (from KATMAN_KEY)`],
|
|
26
|
-
["
|
|
35
|
+
["Features", s.features.length ? s.features.join(", ") : "none"],
|
|
27
36
|
["Last check", s.checkedAt ? `${fmtWhen(s.checkedAt)} (${source}${s.nextCheckAt && s.source !== "offline-grace" ? `; next check after ${fmtWhen(s.nextCheckAt)}` : ""})` : `– (${source})`],
|
|
28
37
|
];
|
|
29
38
|
if (s.error)
|
|
30
39
|
rows.push(["Connection", `couldn't reach katman.pro: ${s.error}`]);
|
|
31
40
|
const L = ["# Katman account", rows.map(([k, v]) => `- **${k}:** ${v}`).join("\n")];
|
|
32
|
-
L.push(
|
|
41
|
+
L.push([
|
|
42
|
+
`**Audit Week tools** (${onText(auditTools, s)}; Audit Week or Pro): ${auditTools.join(", ")}.`,
|
|
43
|
+
`**Katman Pro tools** (${onText(proTools, s)}; Pro only): ${proTools.join(", ")}.`,
|
|
44
|
+
`**Always open:** ${OPEN_TOOLS.join(", ")}.`,
|
|
45
|
+
].join("\n"));
|
|
33
46
|
if (s.state === "valid" && s.plan === "pro") {
|
|
34
47
|
L.push(`Manage the subscription and invoices at ${BILLING_URL}.`);
|
|
35
48
|
}
|
|
36
|
-
else if (s.state === "valid" && s.plan === "
|
|
37
|
-
L.push(`
|
|
49
|
+
else if (s.state === "valid" && s.plan === "audit") {
|
|
50
|
+
L.push(`Your Audit Week ends on ${fmtDay(s.planEndsAt)} and doesn't renew. Katman Pro builds the pages (the P0–P14 prompts, page briefs, file generation, IndexNow, monitoring and the dashboard sync): ${PRO_PRICE_NOTE}, at ${BILLING_URL}. The key stays the same.`);
|
|
38
51
|
}
|
|
39
52
|
else {
|
|
40
|
-
if (s.state
|
|
41
|
-
L.push(
|
|
53
|
+
if (s.state === "no_key" && !s.hint)
|
|
54
|
+
L.push(NO_KEY_TEXT);
|
|
55
|
+
else
|
|
56
|
+
L.push(`**Why the tools are off:** ${reasonText(s)}`);
|
|
42
57
|
L.push(howToText(s));
|
|
43
58
|
}
|
|
44
|
-
L.push(`Network: Katman contacts katman.pro only to check this key (at most once a day, only when KATMAN_KEY is set) and when you call katman_sync. The key is never shown or written to disk; the cache (~/.katman/license.json) stores its SHA-256 hash.
|
|
59
|
+
L.push(`Network: Katman contacts katman.pro only to check this key (at most once a day and when a plan ends, only when KATMAN_KEY is set) and when you call katman_sync. The key is never shown or written to disk; the cache (~/.katman/license.json) stores its SHA-256 hash. Plans: ${PRICING_URL} · account and keys: ${APP_URL}.`);
|
|
45
60
|
return {
|
|
46
61
|
text: L.join("\n\n"),
|
|
47
62
|
structured: {
|
|
48
63
|
state: s.state,
|
|
49
64
|
plan: s.plan,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
planEndsAt: s.planEndsAt ?? null,
|
|
66
|
+
planDaysLeft: left ?? null,
|
|
67
|
+
planEnded: !!s.planEnded,
|
|
68
|
+
endedPlan: s.endedPlan ?? null,
|
|
69
|
+
auditWeekUsed: !!s.auditWeekUsed,
|
|
53
70
|
features: s.features,
|
|
54
71
|
account: { emailMasked: s.emailMasked ?? null },
|
|
55
72
|
checkedAt: s.checkedAt ?? null,
|
|
56
73
|
nextCheckAt: s.nextCheckAt ?? null,
|
|
57
74
|
source: s.source,
|
|
58
75
|
graceUntil: s.graceUntil ?? null,
|
|
59
|
-
|
|
76
|
+
auditTools,
|
|
60
77
|
proTools,
|
|
61
|
-
|
|
78
|
+
openTools: [...OPEN_TOOLS],
|
|
79
|
+
toolsOn: on,
|
|
80
|
+
pricingUrl: PRICING_URL,
|
|
81
|
+
upgradeUrl: BILLING_URL,
|
|
82
|
+
keysUrl: KEYS_URL,
|
|
62
83
|
},
|
|
63
84
|
};
|
|
64
85
|
}
|
|
@@ -16,6 +16,8 @@ export const commands = {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
export const help = [
|
|
19
|
-
" katman-mcp account [--refresh] Katman plan
|
|
20
|
-
"
|
|
19
|
+
" katman-mcp account [--refresh] your Katman plan (none / Audit Week / Pro), days left, and how to get a key or upgrade",
|
|
20
|
+
"",
|
|
21
|
+
"Every command except account, --help and --version needs KATMAN_KEY with an active plan: Audit Week or Katman Pro",
|
|
22
|
+
"(indexnow: Katman Pro only). Without it a command explains the plans and exits with code 4. Plans: https://katman.pro/pricing",
|
|
21
23
|
];
|