@rafaylabs/animation-mcp 0.0.0-stage → 1.0.1

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/bin/cli.js +50 -0
  4. package/dist/api/catalog-client.d.ts +35 -0
  5. package/dist/api/catalog-client.js +78 -0
  6. package/dist/api/server.d.ts +29 -0
  7. package/dist/api/server.js +55 -0
  8. package/dist/auth/cli-login.d.ts +3 -0
  9. package/dist/auth/cli-login.js +121 -0
  10. package/dist/auth/client.d.ts +45 -0
  11. package/dist/auth/client.js +170 -0
  12. package/dist/auth/config.d.ts +36 -0
  13. package/dist/auth/config.js +106 -0
  14. package/dist/auth/device-login.d.ts +2 -0
  15. package/dist/auth/device-login.js +63 -0
  16. package/dist/auth/product.d.ts +17 -0
  17. package/dist/auth/product.js +9 -0
  18. package/dist/index.d.ts +4 -0
  19. package/dist/index.js +267 -0
  20. package/dist/theming/classifier.d.ts +16 -0
  21. package/dist/theming/classifier.js +74 -0
  22. package/dist/theming/color-walker.d.ts +19 -0
  23. package/dist/theming/color-walker.js +303 -0
  24. package/dist/theming/engine.d.ts +29 -0
  25. package/dist/theming/engine.js +144 -0
  26. package/dist/theming/oklch.d.ts +63 -0
  27. package/dist/theming/oklch.js +147 -0
  28. package/dist/tools/code.d.ts +20 -0
  29. package/dist/tools/code.js +104 -0
  30. package/dist/tools/get.d.ts +13 -0
  31. package/dist/tools/get.js +10 -0
  32. package/dist/tools/save.d.ts +22 -0
  33. package/dist/tools/save.js +78 -0
  34. package/dist/tools/schemas.d.ts +140 -0
  35. package/dist/tools/schemas.js +51 -0
  36. package/dist/tools/search.d.ts +35 -0
  37. package/dist/tools/search.js +30 -0
  38. package/dist/tools/theme.d.ts +28 -0
  39. package/dist/tools/theme.js +66 -0
  40. package/dist/utils/lottie-file.d.ts +5 -0
  41. package/dist/utils/lottie-file.js +47 -0
  42. package/dist/utils/workspace.d.ts +20 -0
  43. package/dist/utils/workspace.js +116 -0
  44. package/package.json +56 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdul Rafay (RafayLabs)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @rafaylabs/animation-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **AI-Assisted On-Brand Lottie Animations & Code Generator** for Cursor, Claude Code, Windsurf, VS Code, and any Model Context Protocol (MCP) client.
4
+
5
+ Ask your AI coding assistant for an animation in plain English, and get back an **on-brand, recolored Lottie file plus ready-to-paste framework code** directly in your project.
6
+
7
+ ---
8
+
9
+ ## ⚡ Quick Start
10
+
11
+ ### 1. Connect to Cursor / Claude Code / Windsurf
12
+
13
+ Add this one-liner to your MCP settings file:
14
+
15
+ #### For Cursor (`~/.cursor/mcp.json` or project `.cursor/mcp.json`):
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "rafaylabs-animation": {
20
+ "command": "npx",
21
+ "args": ["-y", "@rafaylabs/animation-mcp"]
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ #### For Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "rafaylabs-animation": {
32
+ "command": "npx",
33
+ "args": ["-y", "@rafaylabs/animation-mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ #### For VS Code (Cline / Roo / Copilot MCP):
40
+ ```json
41
+ {
42
+ "name": "rafaylabs-animation",
43
+ "command": "npx",
44
+ "args": ["-y", "@rafaylabs/animation-mcp"]
45
+ }
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 🔑 Sign in, trial and licence
51
+
52
+ Searching and downloading from the catalogue needs a free account (GitHub sign-in). Theming your own local files works without signing in.
53
+
54
+ ```bash
55
+ npx @rafaylabs/animation-mcp login
56
+ ```
57
+
58
+ 1. Open the link shown in your terminal and **Continue with GitHub**. We read only your public profile.
59
+ 2. Your **7-day free trial** starts on first sign-in (up to 50 searches and 20 downloads a day).
60
+ 3. Have a Lifetime licence key? Answer **y** and paste it. It is linked to your GitHub account (up to 2 devices).
61
+
62
+ | Plan | Searches / day | Downloads / day |
63
+ |---|---|---|
64
+ | Trial (7 days) | 50 | 20 |
65
+ | Free | 10 | 3 |
66
+ | Lifetime licence | Unlimited | 500 |
67
+
68
+ Limits are applied by the server and reset at 00:00 UTC. Theming of local files is always unlimited. Pricing: https://getrafaylabs.com/pricing
69
+
70
+ ### Project folder
71
+ Files are only read from / written to your project folder (reported by your editor via MCP roots). If your editor does not report one, set `ANIMATION_MCP_ROOT` to the project path in the MCP config `env`.
72
+
73
+ Check status anytime:
74
+ ```bash
75
+ npx @rafaylabs/animation-mcp status
76
+ ```
77
+
78
+ Sign out (and free this device's licence slot):
79
+ ```bash
80
+ npx @rafaylabs/animation-mcp logout
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 🛠️ MCP Tools Exposed
86
+
87
+ | Tool | Description |
88
+ |---|---|
89
+ | `search_animations` | Search the 11,000+ animation catalogue using natural phrases (e.g. *"payment successful confetti"*, *"loading car delivery"*). Returns compact JSON with match scores and preview URLs. |
90
+ | `get_animation` | Fetch full Lottie JSON and metadata for a specific catalogue ID. |
91
+ | `apply_theme` | Recolor any animation into your brand colors (`primary`, `secondary`, `background`, `accent`) in perceptual OKLCH color space, keeping shadows and highlights intact. Works on catalogue items **or any local `.json` file in your project**. |
92
+ | `save_animation` | Save the themed animation directly into your project directory (e.g. `assets/animations/success.json`). |
93
+ | `get_code` | Generate ready-to-paste component code for **React**, **React Native (Expo)**, **Next.js**, or **HTML**. |
94
+
95
+ ---
96
+
97
+ ## 💬 What It Feels Like
98
+
99
+ In your AI chat (Cursor / Claude Code):
100
+ > *"Add a celebratory success animation to our checkout confirmation screen. Our theme is #0476FF and white, and this is an Expo React Native app."*
101
+
102
+ Your AI agent automatically:
103
+ 1. Calls `search_animations` → finds high-scoring celebratory checkmarks.
104
+ 2. Calls `apply_theme` → recolors it to `#0476FF`.
105
+ 3. Calls `save_animation` → saves to `assets/animations/checkout-success.json`.
106
+ 4. Calls `get_code` → generates the exact `<LottieView />` React Native component with imports.
107
+ 5. Injects the code directly into your screen. You never leave your editor!
108
+
109
+ ---
110
+
111
+ ## 📜 License
112
+ MIT © [Abdul Rafay / RafayLabs](https://getrafaylabs.com)
package/bin/cli.js ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * RafayLabs Animation MCP CLI
5
+ * Supports:
6
+ * - default (no args): Starts MCP stdio server for AI coding tools (Cursor, Claude, VS Code, Windsurf)
7
+ * - `animation-mcp login`: GitHub sign-in (device flow) + licence activation
8
+ * - `animation-mcp status`: Check daily quota and license tier
9
+ * - `animation-mcp logout`: Clear local session
10
+ */
11
+
12
+ import { startMcpServer } from '../dist/index.js';
13
+ import { runCliLogin, runCliStatus, runCliLogout } from '../dist/auth/cli-login.js';
14
+
15
+ const command = process.argv[2];
16
+
17
+ async function main() {
18
+ if (command === 'login') {
19
+ await runCliLogin();
20
+ } else if (command === 'status') {
21
+ await runCliStatus();
22
+ } else if (command === 'logout') {
23
+ const ok = await runCliLogout(process.argv.includes('--force'));
24
+ if (!ok) process.exitCode = 1;
25
+ } else if (command === '--help' || command === '-h' || command === 'help') {
26
+ console.log(`
27
+ RafayLabs Animation MCP v1.0.1
28
+ AI-driven on-brand Lottie animations and framework code generator.
29
+
30
+ Usage:
31
+ npx @rafaylabs/animation-mcp Start MCP server (stdio mode for AI agents)
32
+ npx @rafaylabs/animation-mcp login Sign in with GitHub (starts your 7-day trial)
33
+ npx @rafaylabs/animation-mcp status Check account tier and daily quota
34
+ npx @rafaylabs/animation-mcp logout Sign out, free this device's licence activation (--force: sign out even if offline)
35
+ `);
36
+ } else {
37
+ // Default: Start MCP Server over stdio
38
+ try {
39
+ await startMcpServer();
40
+ } catch (err) {
41
+ console.error('Fatal MCP Server error:', err);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ }
46
+
47
+ main().catch((err) => {
48
+ console.error('Command failed:', err);
49
+ process.exit(1);
50
+ });
@@ -0,0 +1,35 @@
1
+ export interface CatalogSearchParams {
2
+ query: string;
3
+ category?: string;
4
+ style?: string;
5
+ loop?: boolean;
6
+ max_size_kb?: number;
7
+ limit?: number;
8
+ page?: number;
9
+ }
10
+ export interface CatalogSearchResultItem {
11
+ id: string;
12
+ title: string;
13
+ description: string;
14
+ category: string;
15
+ preview_url: string | null;
16
+ duration_seconds: number;
17
+ size_kb: string;
18
+ match_score: number;
19
+ themeable_score: number;
20
+ }
21
+ export interface CatalogSearchResult {
22
+ results: CatalogSearchResultItem[];
23
+ total: number;
24
+ page: number;
25
+ totalPages: number;
26
+ hasMore: boolean;
27
+ }
28
+ /**
29
+ * Searches the 11K+ hosted Lottie animation catalogue
30
+ */
31
+ export declare function searchCatalog(params: CatalogSearchParams): Promise<CatalogSearchResult>;
32
+ /**
33
+ * Downloads and caches a Lottie JSON animation from the catalogue by ID
34
+ */
35
+ export declare function fetchAnimationById(id: string): Promise<Record<string, any>>;
@@ -0,0 +1,78 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { serverFetch } from './server.js';
5
+ /** Catalogue ids are used in file names and URLs, so only allow a safe character set. */
6
+ const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
7
+ const CACHE_DIR = path.join(os.homedir(), '.animation-mcp', 'cache');
8
+ /**
9
+ * Searches the 11K+ hosted Lottie animation catalogue
10
+ */
11
+ export async function searchCatalog(params) {
12
+ const limit = Math.min(params.limit || 5, 20);
13
+ const page = Math.max(1, Math.floor(params.page || 1));
14
+ const qs = new URLSearchParams({ search: params.query, limit: String(limit), page: String(page) });
15
+ if (params.category)
16
+ qs.set('category', params.category);
17
+ const res = await serverFetch(`/api/mcp/search?${qs.toString()}`);
18
+ if (!res.ok) {
19
+ throw new Error(`Catalogue search failed with HTTP status ${res.status}. Please try again in a moment.`);
20
+ }
21
+ const data = (await res.json());
22
+ const items = data.items || data.animations || [];
23
+ const results = items.map((item, idx) => {
24
+ const sizeBytes = item.file_size_bytes || item.size_bytes || 25000;
25
+ return {
26
+ id: String(item.id || item.slug),
27
+ title: item.name || item.title || 'Animated Icon',
28
+ description: item.description || `High quality ${item.category || 'vector'} animation`,
29
+ category: item.category || 'general',
30
+ preview_url: item.thumbnail_webp_url || item.preview_url || null,
31
+ duration_seconds: Number(item.duration_seconds || item.duration || 2.0),
32
+ size_kb: `${(sizeBytes / 1024).toFixed(1)} KB`,
33
+ // Real relevance from the server (0..1); older servers do not send it, so fall back to rank order.
34
+ match_score: typeof item.match_score === 'number' ? item.match_score : Number(Math.max(0, 1.0 - idx * 0.08).toFixed(2)),
35
+ themeable_score: 0.95,
36
+ };
37
+ });
38
+ const total = Number(data.total ?? results.length);
39
+ const totalPages = Number(data.totalPages ?? 1);
40
+ return {
41
+ results,
42
+ total,
43
+ page: Number(data.page ?? page),
44
+ totalPages,
45
+ hasMore: typeof data.has_more === 'boolean' ? data.has_more : page < totalPages,
46
+ };
47
+ }
48
+ /**
49
+ * Downloads and caches a Lottie JSON animation from the catalogue by ID
50
+ */
51
+ export async function fetchAnimationById(id) {
52
+ if (!SAFE_ID.test(id)) {
53
+ throw new Error(`Invalid animation id "${id}". Use an id returned by search_animations.`);
54
+ }
55
+ // Ensure local cache directory exists
56
+ if (!fs.existsSync(CACHE_DIR)) {
57
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
58
+ }
59
+ const cacheFile = path.join(CACHE_DIR, `${id}.json`);
60
+ if (fs.existsSync(cacheFile)) {
61
+ try {
62
+ return JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
63
+ }
64
+ catch (e) {
65
+ // Invalid cache, continue to fetch
66
+ }
67
+ }
68
+ const res = await serverFetch(`/api/mcp/download/${encodeURIComponent(id)}`, { timeoutMs: 30_000 });
69
+ if (res.status === 404) {
70
+ throw new Error(`Animation "${id}" was not found in the catalogue.`);
71
+ }
72
+ if (!res.ok) {
73
+ throw new Error(`Downloading animation "${id}" failed with HTTP status ${res.status}.`);
74
+ }
75
+ const json = (await res.json());
76
+ fs.writeFileSync(cacheFile, JSON.stringify(json), 'utf-8');
77
+ return json;
78
+ }
@@ -0,0 +1,29 @@
1
+ export declare const SERVER_URL: string;
2
+ export declare class ServerError extends Error {
3
+ status: number;
4
+ code?: string | undefined;
5
+ constructor(message: string, status: number, code?: string | undefined);
6
+ }
7
+ /** Calls the RafayLabs server as the signed-in user. Turns 401/429 into messages the AI client can show as they are. */
8
+ export declare function serverFetch(path: string, init?: {
9
+ method?: string;
10
+ body?: unknown;
11
+ timeoutMs?: number;
12
+ }): Promise<Response>;
13
+ export interface MeResponse {
14
+ github_login: string;
15
+ plan: 'paid' | 'trial' | 'free';
16
+ trial_days_left: number;
17
+ usage: {
18
+ searches: {
19
+ used: number;
20
+ limit: number | null;
21
+ };
22
+ downloads: {
23
+ used: number;
24
+ limit: number | null;
25
+ };
26
+ resetsAt: string;
27
+ };
28
+ }
29
+ export declare function fetchMe(): Promise<MeResponse>;
@@ -0,0 +1,55 @@
1
+ import { loadConfig } from '../auth/config.js';
2
+ export const SERVER_URL = (process.env.ANIMATION_MCP_SERVER_URL || 'https://getrafaylabs.com').replace(/\/+$/, '');
3
+ export class ServerError extends Error {
4
+ status;
5
+ code;
6
+ constructor(message, status, code) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+ /** Calls the RafayLabs server as the signed-in user. Turns 401/429 into messages the AI client can show as they are. */
13
+ export async function serverFetch(path, init = {}) {
14
+ const token = loadConfig().serverToken;
15
+ if (!token) {
16
+ throw new ServerError('You are not signed in. Run `npx @rafaylabs/animation-mcp login` in a terminal (it takes about 30 seconds, and your 7-day trial starts on first sign-in).', 401, 'unauthorized');
17
+ }
18
+ let res;
19
+ try {
20
+ res = await fetch(`${SERVER_URL}${path}`, {
21
+ method: init.method ?? 'GET',
22
+ headers: {
23
+ Accept: 'application/json',
24
+ Authorization: `Bearer ${token}`,
25
+ 'X-Client': 'animation-mcp',
26
+ ...(init.body ? { 'Content-Type': 'application/json' } : {}),
27
+ },
28
+ body: init.body ? JSON.stringify(init.body) : undefined,
29
+ signal: AbortSignal.timeout(init.timeoutMs ?? 20_000),
30
+ });
31
+ }
32
+ catch (err) {
33
+ throw new ServerError(`Could not reach RafayLabs (${err.message}). Check your internet connection and try again.`, 0, 'network');
34
+ }
35
+ if (res.status === 401 || res.status === 429) {
36
+ let message = '';
37
+ let code;
38
+ try {
39
+ const j = await res.json();
40
+ message = j?.message ?? '';
41
+ code = j?.error;
42
+ }
43
+ catch {
44
+ /* ignore */
45
+ }
46
+ throw new ServerError(message || (res.status === 401 ? 'Please run `animation-mcp login`.' : 'Daily limit reached.'), res.status, code);
47
+ }
48
+ return res;
49
+ }
50
+ export async function fetchMe() {
51
+ const res = await serverFetch('/api/mcp/me');
52
+ if (!res.ok)
53
+ throw new ServerError(`Could not load your account (HTTP ${res.status}).`, res.status);
54
+ return (await res.json());
55
+ }
@@ -0,0 +1,3 @@
1
+ export declare function runCliLogin(): Promise<void>;
2
+ export declare function runCliStatus(): Promise<void>;
3
+ export declare function runCliLogout(force?: boolean): Promise<boolean>;
@@ -0,0 +1,121 @@
1
+ import readline from 'readline';
2
+ import { loadConfig, saveConfig } from './config.js';
3
+ import { deactivateLicense } from './client.js';
4
+ import { deviceLogin } from './device-login.js';
5
+ import { serverFetch, fetchMe } from '../api/server.js';
6
+ function ask(question) {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+ return new Promise((resolve) => {
12
+ rl.question(question, (answer) => {
13
+ rl.close();
14
+ resolve(answer.trim());
15
+ });
16
+ });
17
+ }
18
+ function describePlan(me) {
19
+ if (me.plan === 'paid')
20
+ return 'LIFETIME';
21
+ if (me.plan === 'trial')
22
+ return `TRIAL (${me.trial_days_left} day${me.trial_days_left === 1 ? '' : 's'} left)`;
23
+ return 'FREE';
24
+ }
25
+ function fmtLimit(used, limit) {
26
+ return `${used} / ${limit === null ? 'unlimited' : limit} today`;
27
+ }
28
+ export async function runCliLogin() {
29
+ console.log('\n=================================================');
30
+ console.log('RafayLabs Animation MCP - Sign in with GitHub');
31
+ console.log('=================================================');
32
+ let login;
33
+ try {
34
+ login = await deviceLogin();
35
+ }
36
+ catch (err) {
37
+ console.error(`\n❌ ${err.message}\n`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ console.log(`\n✅ Signed in as ${login}.`);
42
+ const answer = await ask('\nDo you have a Lifetime licence key to activate? (y/N): ');
43
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
44
+ const key = await ask('🔑 Paste your licence key: ');
45
+ if (key) {
46
+ console.log('⏳ Activating...');
47
+ try {
48
+ const res = await serverFetch('/api/mcp/license/activate', { method: 'POST', body: { license_key: key }, timeoutMs: 30_000 });
49
+ const json = await res.json().catch(() => ({}));
50
+ if (!res.ok)
51
+ console.error(`\n❌ ${json.message || `Activation failed (HTTP ${res.status}).`}`);
52
+ else
53
+ console.log('\n🎉 Lifetime licence activated on your GitHub account.');
54
+ }
55
+ catch (err) {
56
+ console.error(`\n❌ ${err.message}`);
57
+ }
58
+ }
59
+ }
60
+ try {
61
+ const me = await fetchMe();
62
+ console.log(`\nPlan: ${describePlan(me)}`);
63
+ console.log(`Searches: ${fmtLimit(me.usage.searches.used, me.usage.searches.limit)}`);
64
+ console.log(`Downloads: ${fmtLimit(me.usage.downloads.used, me.usage.downloads.limit)}`);
65
+ }
66
+ catch {
67
+ /* status is optional here */
68
+ }
69
+ console.log('\nYour MCP client (Cursor, Claude Code, Windsurf) can now use the catalogue.\n');
70
+ }
71
+ export async function runCliStatus() {
72
+ const config = loadConfig();
73
+ console.log('\n=======================================');
74
+ console.log('RafayLabs Animation MCP Status');
75
+ console.log('=======================================');
76
+ if (!config.serverToken) {
77
+ console.log('Not signed in. Run `animation-mcp login`.');
78
+ console.log('Theming of local files works without signing in.');
79
+ console.log('=======================================\n');
80
+ return;
81
+ }
82
+ try {
83
+ const me = await fetchMe();
84
+ console.log(`GitHub: ${me.github_login}`);
85
+ console.log(`Plan: ${describePlan(me)}`);
86
+ console.log(`Searches: ${fmtLimit(me.usage.searches.used, me.usage.searches.limit)}`);
87
+ console.log(`Downloads: ${fmtLimit(me.usage.downloads.used, me.usage.downloads.limit)}`);
88
+ console.log('Theming: UNLIMITED (local files)');
89
+ }
90
+ catch (err) {
91
+ console.log(err.message);
92
+ }
93
+ console.log('=======================================\n');
94
+ }
95
+ export async function runCliLogout(force = false) {
96
+ const config = loadConfig();
97
+ // Free the Lemon Squeezy activation first, otherwise every logout + login would use up one of the device slots.
98
+ if (config.licenseKey && config.instanceId) {
99
+ const result = await deactivateLicense(config.licenseKey, config.instanceId);
100
+ if (!result.ok && !force) {
101
+ console.error(`\n❌ Could not free this device's licence activation: ${result.error}`);
102
+ console.error('You are still signed in. Check your connection and try again,');
103
+ console.error('or run `animation-mcp logout --force` to sign out anyway (the activation stays used until you remove it in your Lemon Squeezy order page or contact info@getrafaylabs.com).\n');
104
+ return false;
105
+ }
106
+ if (result.ok)
107
+ console.log('✅ This device was removed from your licence (activation freed).');
108
+ }
109
+ config.email = undefined;
110
+ config.licenseKey = undefined;
111
+ config.instanceId = undefined;
112
+ config.sig = undefined;
113
+ config.tier = 'free';
114
+ config.token = undefined;
115
+ config.serverToken = undefined;
116
+ config.githubLogin = undefined;
117
+ config.cachedUntil = undefined;
118
+ saveConfig(config);
119
+ console.log('\n🚪 Logged out. Session credentials cleared from ~/.animation-mcp/config.json.\n');
120
+ return true;
121
+ }
@@ -0,0 +1,45 @@
1
+ import { UserConfig } from './config.js';
2
+ import { ProductBinding } from './product.js';
3
+ export interface VerifyLicenseResult {
4
+ valid: boolean;
5
+ tier: 'free' | 'lifetime';
6
+ email?: string;
7
+ instanceId?: string;
8
+ error?: string;
9
+ }
10
+ /**
11
+ * Activates a Lemon Squeezy licence key on this machine.
12
+ * Fails closed: any problem (network, wrong product, not configured) means "not licensed".
13
+ */
14
+ export declare function verifyLicenseKey(key: string, instanceName?: string, expected?: ProductBinding): Promise<VerifyLicenseResult>;
15
+ /**
16
+ * Decides whether this run is a paid one. Used by every quota-limited tool.
17
+ * - A verified, unexpired, untampered cached licence counts.
18
+ * - ANIMATION_MCP_KEY is only a way to *supply* a key: it is always checked with Lemon Squeezy.
19
+ * - Anything else is the free tier.
20
+ */
21
+ export declare function resolveLicense(expected?: ProductBinding): Promise<boolean>;
22
+ /**
23
+ * Frees this machine's activation on Lemon Squeezy so the key can be used on another device.
24
+ * Returns ok:true when the activation is gone (or was already gone).
25
+ */
26
+ export declare function deactivateLicense(key: string, instanceId: string): Promise<{
27
+ ok: boolean;
28
+ error?: string;
29
+ }>;
30
+ /**
31
+ * Email magic-code sign-in. Requires the server endpoint; fails closed when it is missing.
32
+ */
33
+ export declare function requestEmailMagicCode(email: string): Promise<{
34
+ success: boolean;
35
+ message: string;
36
+ }>;
37
+ /**
38
+ * Verifies a magic code with the server. The paid tier is granted only from a server
39
+ * response that carries a token; there is no local fallback.
40
+ */
41
+ export declare function verifyEmailMagicCode(email: string, code: string): Promise<{
42
+ success: boolean;
43
+ config?: UserConfig;
44
+ error?: string;
45
+ }>;