untitledui-mcp 0.1.0 → 0.1.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.
- package/package.json +4 -1
- package/.env.example +0 -1
- package/assets/cover.png +0 -0
- package/src/api/client.test.ts +0 -46
- package/src/api/client.ts +0 -117
- package/src/api/endpoints.ts +0 -12
- package/src/api/types.ts +0 -84
- package/src/cache/memory-cache.test.ts +0 -56
- package/src/cache/memory-cache.ts +0 -64
- package/src/index.ts +0 -96
- package/src/server.ts +0 -374
- package/src/utils/descriptions.ts +0 -65
- package/src/utils/license.test.ts +0 -45
- package/src/utils/license.ts +0 -35
- package/src/utils/search.test.ts +0 -39
- package/src/utils/search.ts +0 -71
- package/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untitledui-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "MCP server for UntitledUI Pro components - browse, search, and retrieve UI components via Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"untitledui-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
12
15
|
"url": "https://github.com/sbilde/untitledui-mcp"
|
package/.env.example
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
UNTITLEDUI_LICENSE_KEY=your_license_key_here
|
package/assets/cover.png
DELETED
|
Binary file
|
package/src/api/client.test.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { UntitledUIClient } from "./client.js";
|
|
3
|
-
|
|
4
|
-
describe("UntitledUIClient", () => {
|
|
5
|
-
describe("validateLicense", () => {
|
|
6
|
-
it("should return true for valid license", async () => {
|
|
7
|
-
const client = new UntitledUIClient("valid-key");
|
|
8
|
-
|
|
9
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
10
|
-
status: 200,
|
|
11
|
-
ok: true,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const result = await client.validateLicense();
|
|
15
|
-
expect(result).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("should return false for invalid license", async () => {
|
|
19
|
-
const client = new UntitledUIClient("invalid-key");
|
|
20
|
-
|
|
21
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
22
|
-
status: 401,
|
|
23
|
-
ok: false,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const result = await client.validateLicense();
|
|
27
|
-
expect(result).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("listComponentTypes", () => {
|
|
32
|
-
it("should return array of types", async () => {
|
|
33
|
-
const client = new UntitledUIClient("valid-key");
|
|
34
|
-
|
|
35
|
-
global.fetch = vi.fn().mockResolvedValue({
|
|
36
|
-
ok: true,
|
|
37
|
-
json: () => Promise.resolve({
|
|
38
|
-
types: ["application", "base", "foundations"]
|
|
39
|
-
}),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const result = await client.listComponentTypes();
|
|
43
|
-
expect(result).toEqual(["application", "base", "foundations"]);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
});
|
package/src/api/client.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { ENDPOINTS } from "./endpoints.js";
|
|
2
|
-
import type {
|
|
3
|
-
ComponentListItem,
|
|
4
|
-
ComponentListResponse,
|
|
5
|
-
ComponentsResponse,
|
|
6
|
-
ExampleResponse,
|
|
7
|
-
FetchedComponent,
|
|
8
|
-
} from "./types.js";
|
|
9
|
-
|
|
10
|
-
export class UntitledUIClient {
|
|
11
|
-
constructor(private licenseKey: string) {}
|
|
12
|
-
|
|
13
|
-
async validateLicense(): Promise<boolean> {
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(ENDPOINTS.validateKey(this.licenseKey));
|
|
16
|
-
return response.status === 200;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async listComponentTypes(): Promise<string[]> {
|
|
23
|
-
const response = await fetch(ENDPOINTS.listTypes(this.licenseKey));
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`API error: ${response.status}`);
|
|
26
|
-
}
|
|
27
|
-
const data = await response.json();
|
|
28
|
-
return data.types;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async listComponents(type: string, subfolder?: string): Promise<ComponentListItem[]> {
|
|
32
|
-
let url: string;
|
|
33
|
-
if (subfolder) {
|
|
34
|
-
url = ENDPOINTS.listSubfolder(this.licenseKey, type, [subfolder]);
|
|
35
|
-
} else {
|
|
36
|
-
url = ENDPOINTS.listComponents(this.licenseKey, type);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const response = await fetch(url);
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new Error(`API error: ${response.status}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const data: ComponentListResponse = await response.json();
|
|
45
|
-
|
|
46
|
-
// Handle subfolder response format
|
|
47
|
-
if (subfolder && Array.isArray(data.components)) {
|
|
48
|
-
const subfolderData = data.components[0];
|
|
49
|
-
if (subfolderData && typeof subfolderData === "object" && subfolder in subfolderData) {
|
|
50
|
-
return (subfolderData as Record<string, ComponentListItem[]>)[subfolder];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return data.components as ComponentListItem[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async fetchComponent(type: string, name: string): Promise<FetchedComponent | null> {
|
|
58
|
-
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: { "Content-Type": "application/json" },
|
|
61
|
-
body: JSON.stringify({
|
|
62
|
-
type,
|
|
63
|
-
components: [name],
|
|
64
|
-
key: this.licenseKey,
|
|
65
|
-
}),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(`API error: ${response.status}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const data: ComponentsResponse = await response.json();
|
|
73
|
-
|
|
74
|
-
if (data.pro && data.pro.length > 0) {
|
|
75
|
-
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return data.components[0] || null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async fetchComponents(type: string, names: string[]): Promise<FetchedComponent[]> {
|
|
82
|
-
const response = await fetch(ENDPOINTS.fetchComponents, {
|
|
83
|
-
method: "POST",
|
|
84
|
-
headers: { "Content-Type": "application/json" },
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
type,
|
|
87
|
-
components: names,
|
|
88
|
-
key: this.licenseKey,
|
|
89
|
-
}),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new Error(`API error: ${response.status}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const data: ComponentsResponse = await response.json();
|
|
97
|
-
|
|
98
|
-
if (data.pro && data.pro.length > 0) {
|
|
99
|
-
throw new Error(`PRO access required for: ${data.pro.join(", ")}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return data.components;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async fetchExample(name: string): Promise<ExampleResponse> {
|
|
106
|
-
const response = await fetch(ENDPOINTS.fetchExample, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
example: name,
|
|
111
|
-
key: this.licenseKey,
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return response.json();
|
|
116
|
-
}
|
|
117
|
-
}
|
package/src/api/endpoints.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const API_BASE = "https://www.untitledui.com/react/api";
|
|
2
|
-
|
|
3
|
-
export const ENDPOINTS = {
|
|
4
|
-
validateKey: (key: string) => `${API_BASE}/validate-key?key=${key}`,
|
|
5
|
-
listTypes: (key: string) => `${API_BASE}/components/list?key=${key}`,
|
|
6
|
-
listComponents: (key: string, type: string) =>
|
|
7
|
-
`${API_BASE}/components/list?key=${key}&type=${type}`,
|
|
8
|
-
listSubfolder: (key: string, type: string, subfolders: string[]) =>
|
|
9
|
-
`${API_BASE}/components/list?key=${key}&type=${type}&subfolders=${subfolders.join(",")}`,
|
|
10
|
-
fetchComponents: `${API_BASE}/components`,
|
|
11
|
-
fetchExample: `${API_BASE}/components/example`,
|
|
12
|
-
} as const;
|
package/src/api/types.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// UntitledUI API Response Types
|
|
2
|
-
|
|
3
|
-
export interface ComponentType {
|
|
4
|
-
types: string[];
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface ComponentListItem {
|
|
8
|
-
name: string;
|
|
9
|
-
type: "file" | "dir";
|
|
10
|
-
count?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ComponentListResponse {
|
|
14
|
-
components: ComponentListItem[] | ComponentListWithSubfolder[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ComponentListWithSubfolder {
|
|
18
|
-
[subfolder: string]: ComponentListItem[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ComponentFile {
|
|
22
|
-
path: string;
|
|
23
|
-
code: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface FetchedComponent {
|
|
27
|
-
name: string;
|
|
28
|
-
files: ComponentFile[];
|
|
29
|
-
dependencies?: string[];
|
|
30
|
-
devDependencies?: string[];
|
|
31
|
-
components?: BaseComponentRef[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface BaseComponentRef {
|
|
35
|
-
name: string;
|
|
36
|
-
path: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ComponentsResponse {
|
|
40
|
-
components: FetchedComponent[];
|
|
41
|
-
pro?: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ExampleResponse {
|
|
45
|
-
type: "json-file" | "directory" | "json-files" | "error";
|
|
46
|
-
content?: ExampleContent;
|
|
47
|
-
results?: string[];
|
|
48
|
-
status?: number;
|
|
49
|
-
message?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ExampleContent {
|
|
53
|
-
name: string;
|
|
54
|
-
files: ComponentFile[];
|
|
55
|
-
dependencies?: string[];
|
|
56
|
-
devDependencies?: string[];
|
|
57
|
-
components?: BaseComponentRef[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// MCP Tool Response Types
|
|
61
|
-
|
|
62
|
-
export interface MCPComponentResponse {
|
|
63
|
-
name: string;
|
|
64
|
-
type: string;
|
|
65
|
-
description: string;
|
|
66
|
-
files: ComponentFile[];
|
|
67
|
-
dependencies: string[];
|
|
68
|
-
devDependencies: string[];
|
|
69
|
-
baseComponents: string[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface MCPSearchResult {
|
|
73
|
-
name: string;
|
|
74
|
-
type: string;
|
|
75
|
-
fullPath: string;
|
|
76
|
-
matchType: "exact" | "partial";
|
|
77
|
-
score: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface MCPErrorResponse {
|
|
81
|
-
error: string;
|
|
82
|
-
code: "INVALID_LICENSE" | "NOT_FOUND" | "API_ERROR" | "NETWORK_ERROR";
|
|
83
|
-
suggestions?: string[];
|
|
84
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { MemoryCache } from "./memory-cache.js";
|
|
3
|
-
|
|
4
|
-
describe("MemoryCache", () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
vi.useFakeTimers();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.useRealTimers();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("should store and retrieve values", () => {
|
|
14
|
-
const cache = new MemoryCache();
|
|
15
|
-
cache.set("key1", "value1", 60);
|
|
16
|
-
expect(cache.get("key1")).toBe("value1");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("should return undefined for missing keys", () => {
|
|
20
|
-
const cache = new MemoryCache();
|
|
21
|
-
expect(cache.get("missing")).toBeUndefined();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should expire entries after TTL", () => {
|
|
25
|
-
const cache = new MemoryCache();
|
|
26
|
-
cache.set("key1", "value1", 60);
|
|
27
|
-
|
|
28
|
-
vi.advanceTimersByTime(61 * 1000);
|
|
29
|
-
|
|
30
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should clear all entries", () => {
|
|
34
|
-
const cache = new MemoryCache();
|
|
35
|
-
cache.set("key1", "value1", 60);
|
|
36
|
-
cache.set("key2", "value2", 60);
|
|
37
|
-
|
|
38
|
-
cache.clear();
|
|
39
|
-
|
|
40
|
-
expect(cache.get("key1")).toBeUndefined();
|
|
41
|
-
expect(cache.get("key2")).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("should clear entries matching pattern", () => {
|
|
45
|
-
const cache = new MemoryCache();
|
|
46
|
-
cache.set("component:button", "data1", 60);
|
|
47
|
-
cache.set("component:input", "data2", 60);
|
|
48
|
-
cache.set("search:button", "data3", 60);
|
|
49
|
-
|
|
50
|
-
const cleared = cache.clearPattern("component:");
|
|
51
|
-
|
|
52
|
-
expect(cleared).toBe(2);
|
|
53
|
-
expect(cache.get("component:button")).toBeUndefined();
|
|
54
|
-
expect(cache.get("search:button")).toBe("data3");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
interface CacheEntry<T> {
|
|
2
|
-
data: T;
|
|
3
|
-
expiresAt: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export class MemoryCache {
|
|
7
|
-
private cache = new Map<string, CacheEntry<unknown>>();
|
|
8
|
-
|
|
9
|
-
set<T>(key: string, data: T, ttlSeconds: number): void {
|
|
10
|
-
this.cache.set(key, {
|
|
11
|
-
data,
|
|
12
|
-
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
get<T>(key: string): T | undefined {
|
|
17
|
-
const entry = this.cache.get(key);
|
|
18
|
-
if (!entry) return undefined;
|
|
19
|
-
|
|
20
|
-
if (Date.now() > entry.expiresAt) {
|
|
21
|
-
this.cache.delete(key);
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return entry.data as T;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
has(key: string): boolean {
|
|
29
|
-
return this.get(key) !== undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
clear(): void {
|
|
33
|
-
this.cache.clear();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
clearPattern(pattern: string): number {
|
|
37
|
-
let cleared = 0;
|
|
38
|
-
for (const key of this.cache.keys()) {
|
|
39
|
-
if (key.startsWith(pattern)) {
|
|
40
|
-
this.cache.delete(key);
|
|
41
|
-
cleared++;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return cleared;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
size(): number {
|
|
48
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
49
|
-
if (Date.now() > entry.expiresAt) {
|
|
50
|
-
this.cache.delete(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return this.cache.size;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const CACHE_TTL = {
|
|
58
|
-
componentTypes: 3600,
|
|
59
|
-
componentList: 3600,
|
|
60
|
-
componentCode: 86400,
|
|
61
|
-
searchResults: 1800,
|
|
62
|
-
examples: 86400,
|
|
63
|
-
licenseValidation: 300,
|
|
64
|
-
} as const;
|
package/src/index.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { resolveLicenseKey } from "./utils/license.js";
|
|
4
|
-
import { runServer } from "./server.js";
|
|
5
|
-
import { UntitledUIClient } from "./api/client.js";
|
|
6
|
-
|
|
7
|
-
async function main() {
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
|
|
10
|
-
// Parse CLI arguments
|
|
11
|
-
let cliLicenseKey: string | undefined;
|
|
12
|
-
let testMode = false;
|
|
13
|
-
|
|
14
|
-
for (let i = 0; i < args.length; i++) {
|
|
15
|
-
if (args[i] === "--license-key" && args[i + 1]) {
|
|
16
|
-
cliLicenseKey = args[i + 1];
|
|
17
|
-
i++;
|
|
18
|
-
} else if (args[i] === "--test") {
|
|
19
|
-
testMode = true;
|
|
20
|
-
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
21
|
-
console.log(`
|
|
22
|
-
mcp-untitledui - MCP server for UntitledUI Pro components
|
|
23
|
-
|
|
24
|
-
Usage:
|
|
25
|
-
mcp-untitledui [options]
|
|
26
|
-
|
|
27
|
-
Options:
|
|
28
|
-
--license-key <key> Specify license key (overrides env/config)
|
|
29
|
-
--test Test connection and exit
|
|
30
|
-
--help, -h Show this help message
|
|
31
|
-
|
|
32
|
-
Environment:
|
|
33
|
-
UNTITLEDUI_LICENSE_KEY License key (if not using --license-key)
|
|
34
|
-
|
|
35
|
-
Config:
|
|
36
|
-
~/.untitledui/config.json Auto-detected from UntitledUI CLI login
|
|
37
|
-
`);
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Resolve license key (optional - some features work without it)
|
|
43
|
-
const licenseKey = resolveLicenseKey(cliLicenseKey) || "";
|
|
44
|
-
|
|
45
|
-
// Test mode
|
|
46
|
-
if (testMode) {
|
|
47
|
-
console.log("Testing connection...");
|
|
48
|
-
|
|
49
|
-
if (!licenseKey) {
|
|
50
|
-
console.error("✗ No license key configured");
|
|
51
|
-
console.error("");
|
|
52
|
-
console.error("Get your license key:");
|
|
53
|
-
console.error(" npx untitledui login");
|
|
54
|
-
console.error("");
|
|
55
|
-
console.error("Then configure it via:");
|
|
56
|
-
console.error(" • Environment: UNTITLEDUI_LICENSE_KEY=<key>");
|
|
57
|
-
console.error(" • MCP config env field");
|
|
58
|
-
console.error(" • Auto-detected from ~/.untitledui/config.json");
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const client = new UntitledUIClient(licenseKey);
|
|
63
|
-
|
|
64
|
-
const valid = await client.validateLicense();
|
|
65
|
-
if (!valid) {
|
|
66
|
-
console.error("✗ License key is invalid");
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
console.log("✓ License key is valid");
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const types = await client.listComponentTypes();
|
|
73
|
-
console.log(`✓ API connection successful`);
|
|
74
|
-
console.log(`✓ ${types.length} component types available`);
|
|
75
|
-
console.log("✓ Ready to serve");
|
|
76
|
-
process.exit(0);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
console.error("✗ API connection failed:", error);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Warn if no license key
|
|
84
|
-
if (!licenseKey) {
|
|
85
|
-
console.error("Warning: No license key configured. API calls will fail.");
|
|
86
|
-
console.error("Run 'npx untitledui login' to authenticate.");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Run server
|
|
90
|
-
await runServer(licenseKey);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
main().catch((error) => {
|
|
94
|
-
console.error("Fatal error:", error);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
});
|
package/src/server.ts
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
CallToolRequestSchema,
|
|
5
|
-
ListToolsRequestSchema,
|
|
6
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
|
|
8
|
-
import { UntitledUIClient } from "./api/client.js";
|
|
9
|
-
import { MemoryCache, CACHE_TTL } from "./cache/memory-cache.js";
|
|
10
|
-
import { fuzzySearch, type SearchableItem } from "./utils/search.js";
|
|
11
|
-
import { generateDescription } from "./utils/descriptions.js";
|
|
12
|
-
import type { ComponentListItem, MCPComponentResponse } from "./api/types.js";
|
|
13
|
-
|
|
14
|
-
export function createServer(licenseKey: string) {
|
|
15
|
-
const client = new UntitledUIClient(licenseKey);
|
|
16
|
-
const cache = new MemoryCache();
|
|
17
|
-
|
|
18
|
-
const server = new Server(
|
|
19
|
-
{
|
|
20
|
-
name: "untitledui-mcp",
|
|
21
|
-
version: "0.1.0",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
capabilities: {
|
|
25
|
-
tools: {},
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// Helper: Build searchable index
|
|
31
|
-
async function buildSearchIndex(): Promise<SearchableItem[]> {
|
|
32
|
-
const cacheKey = "search:index";
|
|
33
|
-
const cached = cache.get<SearchableItem[]>(cacheKey);
|
|
34
|
-
if (cached) return cached;
|
|
35
|
-
|
|
36
|
-
const items: SearchableItem[] = [];
|
|
37
|
-
const types = await client.listComponentTypes();
|
|
38
|
-
|
|
39
|
-
for (const type of types) {
|
|
40
|
-
const components = await client.listComponents(type);
|
|
41
|
-
for (const comp of components) {
|
|
42
|
-
if (comp.type === "dir" && comp.count) {
|
|
43
|
-
// Has variants - fetch them
|
|
44
|
-
const variants = await client.listComponents(type, comp.name);
|
|
45
|
-
for (const variant of variants) {
|
|
46
|
-
items.push({
|
|
47
|
-
name: variant.name,
|
|
48
|
-
type,
|
|
49
|
-
fullPath: `${type}/${comp.name}/${variant.name}`,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
items.push({
|
|
54
|
-
name: comp.name,
|
|
55
|
-
type,
|
|
56
|
-
fullPath: `${type}/${comp.name}`,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
cache.set(cacheKey, items, CACHE_TTL.componentList);
|
|
63
|
-
return items;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// List available tools
|
|
67
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
68
|
-
tools: [
|
|
69
|
-
{
|
|
70
|
-
name: "list_component_types",
|
|
71
|
-
description: "List all available component categories (application, base, marketing, etc.)",
|
|
72
|
-
inputSchema: { type: "object", properties: {} },
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: "list_components",
|
|
76
|
-
description: "List components in a category. Use subfolder for variants (e.g., type='application', subfolder='modals')",
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: "object",
|
|
79
|
-
properties: {
|
|
80
|
-
type: { type: "string", description: "Component type (application, base, foundations, marketing, shared-assets)" },
|
|
81
|
-
subfolder: { type: "string", description: "Optional subfolder for variants (e.g., 'modals', 'slideout-menus')" },
|
|
82
|
-
},
|
|
83
|
-
required: ["type"],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "search_components",
|
|
88
|
-
description: "Search for components by name across all categories",
|
|
89
|
-
inputSchema: {
|
|
90
|
-
type: "object",
|
|
91
|
-
properties: {
|
|
92
|
-
query: { type: "string", description: "Search query" },
|
|
93
|
-
},
|
|
94
|
-
required: ["query"],
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "get_component",
|
|
99
|
-
description: "Get a single component's code. Does NOT include dependencies - use get_component_with_deps for that.",
|
|
100
|
-
inputSchema: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
type: { type: "string", description: "Component type" },
|
|
104
|
-
name: { type: "string", description: "Component name (e.g., 'button' or 'modals/ai-assistant-modal')" },
|
|
105
|
-
},
|
|
106
|
-
required: ["type", "name"],
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
name: "get_component_with_deps",
|
|
111
|
-
description: "Get a component with all its base component dependencies included",
|
|
112
|
-
inputSchema: {
|
|
113
|
-
type: "object",
|
|
114
|
-
properties: {
|
|
115
|
-
type: { type: "string", description: "Component type" },
|
|
116
|
-
name: { type: "string", description: "Component name" },
|
|
117
|
-
},
|
|
118
|
-
required: ["type", "name"],
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: "list_examples",
|
|
123
|
-
description: "List available page examples (dashboards, marketing pages, etc.)",
|
|
124
|
-
inputSchema: { type: "object", properties: {} },
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: "get_example",
|
|
128
|
-
description: "Get a complete page example with all files",
|
|
129
|
-
inputSchema: {
|
|
130
|
-
type: "object",
|
|
131
|
-
properties: {
|
|
132
|
-
name: { type: "string", description: "Example name (e.g., 'application', 'marketing')" },
|
|
133
|
-
},
|
|
134
|
-
required: ["name"],
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: "validate_license",
|
|
139
|
-
description: "Verify that the license key is valid",
|
|
140
|
-
inputSchema: { type: "object", properties: {} },
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: "clear_cache",
|
|
144
|
-
description: "Clear cached data. Optionally specify a pattern to clear specific entries.",
|
|
145
|
-
inputSchema: {
|
|
146
|
-
type: "object",
|
|
147
|
-
properties: {
|
|
148
|
-
pattern: { type: "string", description: "Optional pattern to match (e.g., 'component:' clears all component cache)" },
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
}));
|
|
154
|
-
|
|
155
|
-
// Handle tool calls
|
|
156
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
157
|
-
const { name, arguments: args } = request.params;
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
switch (name) {
|
|
161
|
-
case "list_component_types": {
|
|
162
|
-
const cacheKey = "types";
|
|
163
|
-
let types = cache.get<string[]>(cacheKey);
|
|
164
|
-
if (!types) {
|
|
165
|
-
types = await client.listComponentTypes();
|
|
166
|
-
cache.set(cacheKey, types, CACHE_TTL.componentTypes);
|
|
167
|
-
}
|
|
168
|
-
return { content: [{ type: "text", text: JSON.stringify({ types }, null, 2) }] };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
case "list_components": {
|
|
172
|
-
const { type, subfolder } = args as { type: string; subfolder?: string };
|
|
173
|
-
const cacheKey = subfolder ? `list:${type}:${subfolder}` : `list:${type}`;
|
|
174
|
-
|
|
175
|
-
let components = cache.get<ComponentListItem[]>(cacheKey);
|
|
176
|
-
if (!components) {
|
|
177
|
-
components = await client.listComponents(type, subfolder);
|
|
178
|
-
cache.set(cacheKey, components, CACHE_TTL.componentList);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { content: [{ type: "text", text: JSON.stringify({ type, subfolder, components }, null, 2) }] };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
case "search_components": {
|
|
185
|
-
const { query } = args as { query: string };
|
|
186
|
-
const index = await buildSearchIndex();
|
|
187
|
-
const results = fuzzySearch(query, index);
|
|
188
|
-
return { content: [{ type: "text", text: JSON.stringify({ query, results }, null, 2) }] };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
case "get_component": {
|
|
192
|
-
const { type, name: componentName } = args as { type: string; name: string };
|
|
193
|
-
const cacheKey = `component:${type}:${componentName}`;
|
|
194
|
-
|
|
195
|
-
let component = cache.get<MCPComponentResponse>(cacheKey);
|
|
196
|
-
if (!component) {
|
|
197
|
-
const fetched = await client.fetchComponent(type, componentName);
|
|
198
|
-
if (!fetched) {
|
|
199
|
-
// Not found - suggest alternatives
|
|
200
|
-
const index = await buildSearchIndex();
|
|
201
|
-
const suggestions = fuzzySearch(componentName, index, 5).map(r => r.fullPath);
|
|
202
|
-
return {
|
|
203
|
-
content: [{
|
|
204
|
-
type: "text",
|
|
205
|
-
text: JSON.stringify({
|
|
206
|
-
error: `Component "${componentName}" not found in ${type}`,
|
|
207
|
-
code: "NOT_FOUND",
|
|
208
|
-
suggestions,
|
|
209
|
-
}, null, 2),
|
|
210
|
-
}],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
component = {
|
|
215
|
-
name: fetched.name,
|
|
216
|
-
type,
|
|
217
|
-
description: generateDescription(fetched.name, type),
|
|
218
|
-
files: fetched.files,
|
|
219
|
-
dependencies: fetched.dependencies || [],
|
|
220
|
-
devDependencies: fetched.devDependencies || [],
|
|
221
|
-
baseComponents: (fetched.components || []).map(c => c.name),
|
|
222
|
-
};
|
|
223
|
-
cache.set(cacheKey, component, CACHE_TTL.componentCode);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { content: [{ type: "text", text: JSON.stringify(component, null, 2) }] };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
case "get_component_with_deps": {
|
|
230
|
-
const { type, name: componentName } = args as { type: string; name: string };
|
|
231
|
-
|
|
232
|
-
const primary = await client.fetchComponent(type, componentName);
|
|
233
|
-
if (!primary) {
|
|
234
|
-
const index = await buildSearchIndex();
|
|
235
|
-
const suggestions = fuzzySearch(componentName, index, 5).map(r => r.fullPath);
|
|
236
|
-
return {
|
|
237
|
-
content: [{
|
|
238
|
-
type: "text",
|
|
239
|
-
text: JSON.stringify({
|
|
240
|
-
error: `Component "${componentName}" not found`,
|
|
241
|
-
code: "NOT_FOUND",
|
|
242
|
-
suggestions,
|
|
243
|
-
}, null, 2),
|
|
244
|
-
}],
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Fetch base components
|
|
249
|
-
const baseComponentNames = (primary.components || []).map(c => c.name);
|
|
250
|
-
const baseComponents = baseComponentNames.length > 0
|
|
251
|
-
? await client.fetchComponents("base", baseComponentNames)
|
|
252
|
-
: [];
|
|
253
|
-
|
|
254
|
-
// Deduplicate dependencies
|
|
255
|
-
const allDeps = new Set<string>();
|
|
256
|
-
const allDevDeps = new Set<string>();
|
|
257
|
-
|
|
258
|
-
[primary, ...baseComponents].forEach(c => {
|
|
259
|
-
c.dependencies?.forEach(d => allDeps.add(d));
|
|
260
|
-
c.devDependencies?.forEach(d => allDevDeps.add(d));
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const result = {
|
|
264
|
-
primary: {
|
|
265
|
-
name: primary.name,
|
|
266
|
-
type,
|
|
267
|
-
description: generateDescription(primary.name, type),
|
|
268
|
-
files: primary.files,
|
|
269
|
-
dependencies: primary.dependencies || [],
|
|
270
|
-
devDependencies: primary.devDependencies || [],
|
|
271
|
-
baseComponents: baseComponentNames,
|
|
272
|
-
},
|
|
273
|
-
baseComponents: baseComponents.map(c => ({
|
|
274
|
-
name: c.name,
|
|
275
|
-
type: "base",
|
|
276
|
-
description: generateDescription(c.name, "base"),
|
|
277
|
-
files: c.files,
|
|
278
|
-
dependencies: c.dependencies || [],
|
|
279
|
-
devDependencies: c.devDependencies || [],
|
|
280
|
-
})),
|
|
281
|
-
totalFiles: primary.files.length + baseComponents.reduce((sum, c) => sum + c.files.length, 0),
|
|
282
|
-
allDependencies: Array.from(allDeps),
|
|
283
|
-
allDevDependencies: Array.from(allDevDeps),
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
case "list_examples": {
|
|
290
|
-
return {
|
|
291
|
-
content: [{
|
|
292
|
-
type: "text",
|
|
293
|
-
text: JSON.stringify({
|
|
294
|
-
examples: [
|
|
295
|
-
{ name: "application", type: "application", description: "Dashboard application example" },
|
|
296
|
-
{ name: "marketing", type: "marketing", description: "Marketing landing page example" },
|
|
297
|
-
],
|
|
298
|
-
}, null, 2),
|
|
299
|
-
}],
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
case "get_example": {
|
|
304
|
-
const { name: exampleName } = args as { name: string };
|
|
305
|
-
const cacheKey = `example:${exampleName}`;
|
|
306
|
-
|
|
307
|
-
let example = cache.get(cacheKey);
|
|
308
|
-
if (!example) {
|
|
309
|
-
example = await client.fetchExample(exampleName);
|
|
310
|
-
if (example) {
|
|
311
|
-
cache.set(cacheKey, example, CACHE_TTL.examples);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return { content: [{ type: "text", text: JSON.stringify(example, null, 2) }] };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
case "validate_license": {
|
|
319
|
-
const valid = await client.validateLicense();
|
|
320
|
-
return {
|
|
321
|
-
content: [{
|
|
322
|
-
type: "text",
|
|
323
|
-
text: JSON.stringify({
|
|
324
|
-
valid,
|
|
325
|
-
message: valid ? "License key is valid" : "Invalid or missing license key",
|
|
326
|
-
}, null, 2),
|
|
327
|
-
}],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
case "clear_cache": {
|
|
332
|
-
const { pattern } = args as { pattern?: string };
|
|
333
|
-
let cleared: number;
|
|
334
|
-
if (pattern) {
|
|
335
|
-
cleared = cache.clearPattern(pattern);
|
|
336
|
-
} else {
|
|
337
|
-
cleared = cache.size();
|
|
338
|
-
cache.clear();
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
content: [{
|
|
342
|
-
type: "text",
|
|
343
|
-
text: JSON.stringify({ cleared, message: `Cleared ${cleared} cache entries` }, null, 2),
|
|
344
|
-
}],
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
default:
|
|
349
|
-
return {
|
|
350
|
-
content: [{
|
|
351
|
-
type: "text",
|
|
352
|
-
text: JSON.stringify({ error: `Unknown tool: ${name}`, code: "UNKNOWN_TOOL" }, null, 2),
|
|
353
|
-
}],
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
} catch (error) {
|
|
357
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
358
|
-
return {
|
|
359
|
-
content: [{
|
|
360
|
-
type: "text",
|
|
361
|
-
text: JSON.stringify({ error: message, code: "API_ERROR" }, null, 2),
|
|
362
|
-
}],
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
return server;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export async function runServer(licenseKey: string) {
|
|
371
|
-
const server = createServer(licenseKey);
|
|
372
|
-
const transport = new StdioServerTransport();
|
|
373
|
-
await server.connect(transport);
|
|
374
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Generated descriptions based on component name and type
|
|
2
|
-
// Since UntitledUI API doesn't provide descriptions, we generate them
|
|
3
|
-
|
|
4
|
-
const TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
5
|
-
application: "Application UI component for dashboards and web apps",
|
|
6
|
-
base: "Core UI primitive component",
|
|
7
|
-
foundations: "Foundational element (icon, logo, visual)",
|
|
8
|
-
marketing: "Marketing section component for landing pages",
|
|
9
|
-
"shared-assets": "Shared visual asset (illustration, pattern, mockup)",
|
|
10
|
-
icons: "Icon component",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const NAME_PATTERNS: [RegExp, string][] = [
|
|
14
|
-
[/modal/i, "Modal dialog component"],
|
|
15
|
-
[/button/i, "Interactive button component"],
|
|
16
|
-
[/input/i, "Form input component"],
|
|
17
|
-
[/select/i, "Selection/dropdown component"],
|
|
18
|
-
[/table/i, "Data table component"],
|
|
19
|
-
[/calendar/i, "Calendar/date component"],
|
|
20
|
-
[/date-picker/i, "Date selection component"],
|
|
21
|
-
[/sidebar/i, "Sidebar navigation component"],
|
|
22
|
-
[/header/i, "Header/navigation component"],
|
|
23
|
-
[/footer/i, "Footer section component"],
|
|
24
|
-
[/card/i, "Card container component"],
|
|
25
|
-
[/avatar/i, "User avatar component"],
|
|
26
|
-
[/badge/i, "Badge/label component"],
|
|
27
|
-
[/alert/i, "Alert/notification component"],
|
|
28
|
-
[/toast/i, "Toast notification component"],
|
|
29
|
-
[/dropdown/i, "Dropdown menu component"],
|
|
30
|
-
[/tabs/i, "Tabbed interface component"],
|
|
31
|
-
[/pagination/i, "Pagination component"],
|
|
32
|
-
[/carousel/i, "Carousel/slider component"],
|
|
33
|
-
[/chart/i, "Chart/visualization component"],
|
|
34
|
-
[/metric/i, "Metrics/statistics component"],
|
|
35
|
-
[/form/i, "Form component"],
|
|
36
|
-
[/pricing/i, "Pricing section component"],
|
|
37
|
-
[/testimonial/i, "Testimonial section component"],
|
|
38
|
-
[/feature/i, "Features section component"],
|
|
39
|
-
[/cta/i, "Call-to-action section component"],
|
|
40
|
-
[/hero/i, "Hero section component"],
|
|
41
|
-
[/faq/i, "FAQ section component"],
|
|
42
|
-
[/blog/i, "Blog section component"],
|
|
43
|
-
[/team/i, "Team section component"],
|
|
44
|
-
[/contact/i, "Contact section component"],
|
|
45
|
-
[/login/i, "Login/authentication component"],
|
|
46
|
-
[/signup/i, "Signup/registration component"],
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
export function generateDescription(name: string, type: string): string {
|
|
50
|
-
// Check name patterns first
|
|
51
|
-
for (const [pattern, description] of NAME_PATTERNS) {
|
|
52
|
-
if (pattern.test(name)) {
|
|
53
|
-
return description;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Fall back to type description
|
|
58
|
-
const typeDesc = TYPE_DESCRIPTIONS[type];
|
|
59
|
-
if (typeDesc) {
|
|
60
|
-
return `${typeDesc}: ${name.replace(/-/g, " ")}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Generic fallback
|
|
64
|
-
return `UI component: ${name.replace(/-/g, " ")}`;
|
|
65
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { resolveLicenseKey } from "./license.js";
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import * as os from "os";
|
|
5
|
-
|
|
6
|
-
vi.mock("fs");
|
|
7
|
-
vi.mock("os");
|
|
8
|
-
|
|
9
|
-
describe("resolveLicenseKey", () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
vi.resetAllMocks();
|
|
12
|
-
delete process.env.UNTITLEDUI_LICENSE_KEY;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("should prefer CLI argument over env var", () => {
|
|
16
|
-
process.env.UNTITLEDUI_LICENSE_KEY = "env-key";
|
|
17
|
-
const result = resolveLicenseKey("cli-key");
|
|
18
|
-
expect(result).toBe("cli-key");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("should use env var if no CLI argument", () => {
|
|
22
|
-
process.env.UNTITLEDUI_LICENSE_KEY = "env-key";
|
|
23
|
-
const result = resolveLicenseKey();
|
|
24
|
-
expect(result).toBe("env-key");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("should read from config file if no env var", () => {
|
|
28
|
-
vi.mocked(os.homedir).mockReturnValue("/home/user");
|
|
29
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
30
|
-
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
31
|
-
JSON.stringify({ license: "file-key" })
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const result = resolveLicenseKey();
|
|
35
|
-
expect(result).toBe("file-key");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("should return undefined if no key found", () => {
|
|
39
|
-
vi.mocked(os.homedir).mockReturnValue("/home/user");
|
|
40
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
41
|
-
|
|
42
|
-
const result = resolveLicenseKey();
|
|
43
|
-
expect(result).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
});
|
package/src/utils/license.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
|
|
5
|
-
function getConfigPath(): string {
|
|
6
|
-
return path.join(os.homedir(), ".untitledui", "config.json");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function resolveLicenseKey(cliArg?: string): string | undefined {
|
|
10
|
-
// Priority 1: CLI argument
|
|
11
|
-
if (cliArg) {
|
|
12
|
-
return cliArg;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Priority 2: Environment variable
|
|
16
|
-
const envKey = process.env.UNTITLEDUI_LICENSE_KEY;
|
|
17
|
-
if (envKey) {
|
|
18
|
-
return envKey;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Priority 3: Config file (~/.untitledui/config.json)
|
|
22
|
-
try {
|
|
23
|
-
const configPath = getConfigPath();
|
|
24
|
-
if (fs.existsSync(configPath)) {
|
|
25
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
26
|
-
if (config.license) {
|
|
27
|
-
return config.license;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// Ignore file read errors
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
package/src/utils/search.test.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { fuzzySearch, type SearchableItem } from "./search.js";
|
|
3
|
-
|
|
4
|
-
describe("fuzzySearch", () => {
|
|
5
|
-
const items: SearchableItem[] = [
|
|
6
|
-
{ name: "button", type: "base", fullPath: "base/button" },
|
|
7
|
-
{ name: "date-picker", type: "application", fullPath: "application/date-picker" },
|
|
8
|
-
{ name: "date-range-picker", type: "application", fullPath: "application/date-range-picker" },
|
|
9
|
-
{ name: "ai-assistant-modal", type: "application", fullPath: "application/modals/ai-assistant-modal" },
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
it("should find exact matches", () => {
|
|
13
|
-
const results = fuzzySearch("button", items);
|
|
14
|
-
expect(results[0].name).toBe("button");
|
|
15
|
-
expect(results[0].matchType).toBe("exact");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("should find partial matches", () => {
|
|
19
|
-
const results = fuzzySearch("date", items);
|
|
20
|
-
expect(results.length).toBe(2);
|
|
21
|
-
expect(results.every(r => r.name.includes("date"))).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should rank exact matches higher", () => {
|
|
25
|
-
const results = fuzzySearch("date-picker", items);
|
|
26
|
-
expect(results[0].name).toBe("date-picker");
|
|
27
|
-
expect(results[0].matchType).toBe("exact");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("should return empty array for no matches", () => {
|
|
31
|
-
const results = fuzzySearch("nonexistent", items);
|
|
32
|
-
expect(results).toEqual([]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should limit results", () => {
|
|
36
|
-
const results = fuzzySearch("a", items, 2);
|
|
37
|
-
expect(results.length).toBeLessThanOrEqual(2);
|
|
38
|
-
});
|
|
39
|
-
});
|
package/src/utils/search.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export interface SearchableItem {
|
|
2
|
-
name: string;
|
|
3
|
-
type: string;
|
|
4
|
-
fullPath: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface SearchResult extends SearchableItem {
|
|
8
|
-
matchType: "exact" | "partial";
|
|
9
|
-
score: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function fuzzySearch(
|
|
13
|
-
query: string,
|
|
14
|
-
items: SearchableItem[],
|
|
15
|
-
limit = 20
|
|
16
|
-
): SearchResult[] {
|
|
17
|
-
const queryLower = query.toLowerCase();
|
|
18
|
-
|
|
19
|
-
const results: SearchResult[] = [];
|
|
20
|
-
|
|
21
|
-
for (const item of items) {
|
|
22
|
-
const nameLower = item.name.toLowerCase();
|
|
23
|
-
const fullPathLower = item.fullPath.toLowerCase();
|
|
24
|
-
|
|
25
|
-
let score = 0;
|
|
26
|
-
let matchType: "exact" | "partial" = "partial";
|
|
27
|
-
|
|
28
|
-
// Exact match on name
|
|
29
|
-
if (nameLower === queryLower) {
|
|
30
|
-
score = 1.0;
|
|
31
|
-
matchType = "exact";
|
|
32
|
-
}
|
|
33
|
-
// Name starts with query
|
|
34
|
-
else if (nameLower.startsWith(queryLower)) {
|
|
35
|
-
score = 0.9;
|
|
36
|
-
}
|
|
37
|
-
// Name contains query
|
|
38
|
-
else if (nameLower.includes(queryLower)) {
|
|
39
|
-
score = 0.7;
|
|
40
|
-
}
|
|
41
|
-
// Full path contains query
|
|
42
|
-
else if (fullPathLower.includes(queryLower)) {
|
|
43
|
-
score = 0.5;
|
|
44
|
-
}
|
|
45
|
-
// Fuzzy: all query chars appear in order
|
|
46
|
-
else {
|
|
47
|
-
let queryIndex = 0;
|
|
48
|
-
for (const char of nameLower) {
|
|
49
|
-
if (char === queryLower[queryIndex]) {
|
|
50
|
-
queryIndex++;
|
|
51
|
-
}
|
|
52
|
-
if (queryIndex === queryLower.length) {
|
|
53
|
-
score = 0.3;
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (score > 0) {
|
|
60
|
-
results.push({
|
|
61
|
-
...item,
|
|
62
|
-
matchType,
|
|
63
|
-
score,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return results
|
|
69
|
-
.sort((a, b) => b.score - a.score)
|
|
70
|
-
.slice(0, limit);
|
|
71
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"outDir": "dist",
|
|
10
|
-
"rootDir": "src",
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"resolveJsonModule": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist"]
|
|
16
|
-
}
|