@huskel/sdk 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,114 +1,96 @@
1
- # @huskel/sdk
2
-
3
- AI-powered search SDK for SPAs. Drop in, point at your product DOM, get vector search + LLM responses.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @huskel/sdk
9
- ```
10
-
11
- ## Setup (Next.js app layout)
12
-
13
- ```tsx
14
- // app/layout.tsx or _app.tsx
15
- 'use client';
16
- import { useHuskel } from '@huskel/sdk';
17
-
18
- export default function RootLayout({ children }) {
19
- useHuskel({
20
- siteId: 'your-site-id',
21
- apiUrl: 'https://your-huskel-backend.com',
22
- autoIngest: true,
23
- debounceMs: 600,
24
- selectors: {
25
- selectorContainer: '.product-card',
26
- selectorName: '.product-title',
27
- selectorPrice: '.product-price',
28
- selectorImage: '.product-image img',
29
- selectorUrl: 'a.product-link',
30
- selectorDescription: '.product-desc',
31
- selectorCategory: '.product-category',
32
- currency: 'KES',
33
- },
34
- });
35
-
36
- return <html><body>{children}</body></html>;
37
- }
38
- ```
39
-
40
- ## Search bar
41
-
42
- ```tsx
43
- import { SearchBar } from '@huskel/sdk';
44
-
45
- export function Header() {
46
- return (
47
- <SearchBar
48
- placeholder="Search for what you want — how you want"
49
- onSelect={(result) => {
50
- window.location.href = result.product.url;
51
- }}
52
- />
53
- );
54
- }
55
- ```
56
-
57
- ## Sparkle on product card
58
-
59
- ```tsx
60
- import { Sparkle } from '@huskel/sdk';
61
-
62
- export function ProductCard({ product }) {
63
- return (
64
- <div className="product-card">
65
- <img src={product.image} />
66
- <h3>{product.name}</h3>
67
- <p>{product.price}</p>
68
- <Sparkle
69
- productName={product.name}
70
- onResult={(similar) => console.log(similar)}
71
- />
72
- </div>
73
- );
74
- }
75
- ```
76
-
77
- ## useSearch hook (custom UI)
78
-
79
- ```tsx
80
- import { useSearch } from '@huskel/sdk';
81
-
82
- export function CustomSearch() {
83
- const { results, loading, search } = useSearch();
84
-
85
- return (
86
- <div>
87
- <input onChange={e => search(e.target.value)} />
88
- {loading && <p>Loading…</p>}
89
- {results.map(r => <div key={r.id}>{r.product.name}</div>)}
90
- </div>
91
- );
92
- }
93
- ```
94
-
95
- ## Manual ingest
96
-
97
- ```tsx
98
- import { getHuskelClient } from '@huskel/sdk';
99
-
100
- const client = getHuskelClient();
101
-
102
- // Single product
103
- await client.api.ingest({ name: 'Maize Flour 2kg', price: 'KES 180', url: '/products/maize-flour' });
104
-
105
- // Batch
106
- await client.api.ingestBatch([...products]);
107
- ```
108
-
109
- ## How it works
110
-
111
- 1. `useHuskel` pushes your CSS selector config to the backend on mount.
112
- 2. On every SPA route change, the SDK waits for the DOM to settle (debounce), then extracts products using your selectors.
113
- 3. Products are batch-ingested to your Go backend → Upstash (BAAI/BGE embeddings) → NeonDB.
114
- 4. `useSearch` / `SearchBar` hit `/search` for vector similarity results powered by the ingested data.
1
+ # @huskel/sdk
2
+
3
+ AI-powered vector search SDK. You own your data pass it in, we handle the rest.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @huskel/sdk
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```tsx
14
+ // app/layout.tsx (Next.js) or _app.tsx
15
+ 'use client';
16
+ import { useHuskel } from '@huskel/sdk';
17
+
18
+ export default function RootLayout({ children }) {
19
+ useHuskel({
20
+ siteId: 'your-site-id',
21
+ apiUrl: 'https://your-huskel-backend.com',
22
+ apiToken: 'your-api-token',
23
+ });
24
+ return <html><body>{children}</body></html>;
25
+ }
26
+ ```
27
+
28
+ ## Ingest products (you pass your own data)
29
+
30
+ ```tsx
31
+ import { useIngest } from '@huskel/sdk';
32
+
33
+ // Single product page
34
+ export function ProductPage({ product }) {
35
+ const { ingest } = useIngest();
36
+
37
+ useEffect(() => {
38
+ ingest({
39
+ name: product.title,
40
+ price: product.price,
41
+ url: window.location.href,
42
+ images: product.images,
43
+ category: product.category,
44
+ currency: 'KES',
45
+ });
46
+ }, [product.id]);
47
+ }
48
+
49
+ // Listing / category page
50
+ export function ProductGrid({ products }) {
51
+ const { ingestBatch } = useIngest();
52
+
53
+ useEffect(() => {
54
+ ingestBatch(products.map(p => ({
55
+ name: p.title,
56
+ price: p.price,
57
+ url: `/products/${p.slug}`,
58
+ images: [p.thumbnail],
59
+ currency: 'KES',
60
+ })));
61
+ }, [products]);
62
+ }
63
+ ```
64
+
65
+ ## Search
66
+
67
+ ```tsx
68
+ import { SearchBar } from '@huskel/sdk';
69
+
70
+ export function Header() {
71
+ return (
72
+ <SearchBar
73
+ onSelect={(result) => router.push(result.product.url)}
74
+ />
75
+ );
76
+ }
77
+ ```
78
+
79
+ ```tsx
80
+ // Headless
81
+ import { useSearch } from '@huskel/sdk';
82
+
83
+ const { results, loading, search } = useSearch();
84
+ <input onChange={e => search(e.target.value)} />
85
+ ```
86
+
87
+ ## Sparkle (similar products)
88
+
89
+ ```tsx
90
+ import { Sparkle } from '@huskel/sdk';
91
+
92
+ <Sparkle
93
+ productName={product.name}
94
+ onResult={(similar) => setSimilar(similar)}
95
+ />
96
+ ```
package/dist/index.d.mts CHANGED
@@ -22,31 +22,37 @@ interface Product {
22
22
  priceNumeric?: number;
23
23
  slug?: string;
24
24
  }
25
- interface SiteConfig {
26
- siteId: string;
27
- selectorContainer: string;
28
- selectorName: string;
29
- selectorPrice: string;
30
- selectorImage?: string;
31
- selectorUrl?: string;
32
- selectorDescription?: string;
33
- selectorCategory?: string;
34
- selectorBrand?: string;
35
- selectorAvailability?: string;
36
- selectorSku?: string;
37
- selectorOriginalPrice?: string;
38
- selectorDiscount?: string;
39
- selectorRating?: string;
40
- selectorNextPage?: string;
25
+ interface RawProductInput {
26
+ name?: string;
27
+ title?: string;
28
+ productName?: string;
29
+ price?: string | number;
30
+ priceNumeric?: number;
31
+ url?: string;
32
+ image?: string;
33
+ thumbnail?: string;
34
+ images?: string[];
35
+ slug?: string;
36
+ id?: string;
37
+ productId?: string;
38
+ brand?: string;
39
+ description?: string;
40
+ originalPrice?: string;
41
+ discount?: string;
41
42
  currency?: string;
42
- maxPages?: number;
43
+ stock?: string;
44
+ availability?: string;
45
+ rating?: string;
46
+ reviewCount?: number;
47
+ category?: string;
48
+ subCategory?: string;
49
+ tags?: string[];
50
+ specs?: Record<string, string>;
43
51
  }
44
52
  interface HuskelConfig {
45
- siteId: string;
46
- apiUrl: string;
47
- selectors: Omit<SiteConfig, 'siteId'>;
48
- autoIngest?: boolean;
49
- debounceMs?: number;
53
+ siteId?: string;
54
+ apiUrl?: string;
55
+ apiToken?: string;
50
56
  }
51
57
  interface SearchRequest {
52
58
  query: string;
@@ -67,36 +73,43 @@ interface IngestResponse {
67
73
  message?: string;
68
74
  count?: number;
69
75
  }
76
+ interface HuskelError {
77
+ status: number;
78
+ message: string;
79
+ }
70
80
 
71
81
  declare class HuskelAPI {
72
82
  private apiUrl;
73
83
  private siteId;
74
- constructor(apiUrl: string, siteId: string);
84
+ private apiToken;
85
+ constructor(apiUrl: string, siteId: string, apiToken: string);
75
86
  private post;
76
87
  ingest(product: Product): Promise<IngestResponse>;
77
88
  ingestBatch(products: Product[]): Promise<IngestResponse>;
78
89
  search(query: string, limit?: number): Promise<SearchResponse>;
79
- pushConfig(config: Omit<SiteConfig, 'siteId'>): Promise<void>;
80
90
  }
81
91
 
82
92
  declare class HuskelClient {
83
- private config;
84
93
  readonly api: HuskelAPI;
85
- private observer?;
86
- private unsubscribe?;
87
- private debounceTimer?;
94
+ private ingestQueue;
95
+ private ingestTimer;
96
+ private ingestedUrls;
97
+ private onlineHandler;
88
98
  constructor(config: HuskelConfig);
89
- /** Push selectors config to backend once (idempotent) */
90
- configure(): Promise<void>;
91
- /** Scrape current DOM and ingest all products on the page */
92
- ingestCurrentPage(): Promise<Product[]>;
93
- /** Start watching route changes and auto-ingesting */
94
- start(): void;
95
- stop(): void;
99
+ destroy(): void;
100
+ queueIngest(rawProduct: RawProductInput): Promise<void>;
101
+ queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
102
+ private scheduleFlush;
103
+ private flushQueue;
96
104
  }
97
105
  declare function initHuskel(config: HuskelConfig): HuskelClient;
98
106
  declare function getHuskelClient(): HuskelClient;
99
107
 
108
+ /**
109
+ * @deprecated Use <HuskelProvider> instead to avoid SSR issues.
110
+ */
111
+ declare function useHuskel(config: HuskelConfig): HuskelClient;
112
+
100
113
  interface UseSearchReturn {
101
114
  results: SearchResult[];
102
115
  loading: boolean;
@@ -106,11 +119,13 @@ interface UseSearchReturn {
106
119
  }
107
120
  declare function useSearch(): UseSearchReturn;
108
121
 
109
- interface UseHuskelOptions extends HuskelConfig {
110
- onIngest?: (products: Product[]) => void;
111
- onError?: (err: Error) => void;
122
+ interface UseIngestReturn {
123
+ ingest: (product: RawProductInput) => Promise<void>;
124
+ ingestBatch: (products: RawProductInput[]) => Promise<void>;
125
+ loading: boolean;
126
+ error: string | null;
112
127
  }
113
- declare function useHuskel(options: UseHuskelOptions): HuskelClient | null;
128
+ declare function useIngest(): UseIngestReturn;
114
129
 
115
130
  interface SearchBarProps {
116
131
  placeholder?: string;
@@ -118,15 +133,23 @@ interface SearchBarProps {
118
133
  debounceMs?: number;
119
134
  onSelect?: (result: SearchResult) => void;
120
135
  className?: string;
136
+ inputClassName?: string;
137
+ dropdownClassName?: string;
121
138
  renderResult?: (result: SearchResult) => React.ReactNode;
122
139
  }
123
- declare function SearchBar({ placeholder, limit, debounceMs, onSelect, className, renderResult, }: SearchBarProps): react_jsx_runtime.JSX.Element;
140
+ declare function SearchBar({ placeholder, limit, debounceMs, onSelect, className, inputClassName, dropdownClassName, renderResult, }: SearchBarProps): react_jsx_runtime.JSX.Element;
124
141
 
125
142
  interface SparkleProps {
126
143
  productName: string;
127
- className?: string;
144
+ limit?: number;
128
145
  onResult?: (results: SearchResult[]) => void;
146
+ className?: string;
147
+ }
148
+ declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
149
+
150
+ interface HuskelProviderProps extends HuskelConfig {
151
+ children: React.ReactNode;
129
152
  }
130
- declare function Sparkle({ productName, className, onResult }: SparkleProps): react_jsx_runtime.JSX.Element;
153
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
131
154
 
132
- export { HuskelAPI, HuskelClient, type HuskelConfig, type IngestResponse, type Product, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, type SiteConfig, Sparkle, getHuskelClient, initHuskel, useHuskel, useSearch };
155
+ export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };
package/dist/index.d.ts CHANGED
@@ -22,31 +22,37 @@ interface Product {
22
22
  priceNumeric?: number;
23
23
  slug?: string;
24
24
  }
25
- interface SiteConfig {
26
- siteId: string;
27
- selectorContainer: string;
28
- selectorName: string;
29
- selectorPrice: string;
30
- selectorImage?: string;
31
- selectorUrl?: string;
32
- selectorDescription?: string;
33
- selectorCategory?: string;
34
- selectorBrand?: string;
35
- selectorAvailability?: string;
36
- selectorSku?: string;
37
- selectorOriginalPrice?: string;
38
- selectorDiscount?: string;
39
- selectorRating?: string;
40
- selectorNextPage?: string;
25
+ interface RawProductInput {
26
+ name?: string;
27
+ title?: string;
28
+ productName?: string;
29
+ price?: string | number;
30
+ priceNumeric?: number;
31
+ url?: string;
32
+ image?: string;
33
+ thumbnail?: string;
34
+ images?: string[];
35
+ slug?: string;
36
+ id?: string;
37
+ productId?: string;
38
+ brand?: string;
39
+ description?: string;
40
+ originalPrice?: string;
41
+ discount?: string;
41
42
  currency?: string;
42
- maxPages?: number;
43
+ stock?: string;
44
+ availability?: string;
45
+ rating?: string;
46
+ reviewCount?: number;
47
+ category?: string;
48
+ subCategory?: string;
49
+ tags?: string[];
50
+ specs?: Record<string, string>;
43
51
  }
44
52
  interface HuskelConfig {
45
- siteId: string;
46
- apiUrl: string;
47
- selectors: Omit<SiteConfig, 'siteId'>;
48
- autoIngest?: boolean;
49
- debounceMs?: number;
53
+ siteId?: string;
54
+ apiUrl?: string;
55
+ apiToken?: string;
50
56
  }
51
57
  interface SearchRequest {
52
58
  query: string;
@@ -67,36 +73,43 @@ interface IngestResponse {
67
73
  message?: string;
68
74
  count?: number;
69
75
  }
76
+ interface HuskelError {
77
+ status: number;
78
+ message: string;
79
+ }
70
80
 
71
81
  declare class HuskelAPI {
72
82
  private apiUrl;
73
83
  private siteId;
74
- constructor(apiUrl: string, siteId: string);
84
+ private apiToken;
85
+ constructor(apiUrl: string, siteId: string, apiToken: string);
75
86
  private post;
76
87
  ingest(product: Product): Promise<IngestResponse>;
77
88
  ingestBatch(products: Product[]): Promise<IngestResponse>;
78
89
  search(query: string, limit?: number): Promise<SearchResponse>;
79
- pushConfig(config: Omit<SiteConfig, 'siteId'>): Promise<void>;
80
90
  }
81
91
 
82
92
  declare class HuskelClient {
83
- private config;
84
93
  readonly api: HuskelAPI;
85
- private observer?;
86
- private unsubscribe?;
87
- private debounceTimer?;
94
+ private ingestQueue;
95
+ private ingestTimer;
96
+ private ingestedUrls;
97
+ private onlineHandler;
88
98
  constructor(config: HuskelConfig);
89
- /** Push selectors config to backend once (idempotent) */
90
- configure(): Promise<void>;
91
- /** Scrape current DOM and ingest all products on the page */
92
- ingestCurrentPage(): Promise<Product[]>;
93
- /** Start watching route changes and auto-ingesting */
94
- start(): void;
95
- stop(): void;
99
+ destroy(): void;
100
+ queueIngest(rawProduct: RawProductInput): Promise<void>;
101
+ queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
102
+ private scheduleFlush;
103
+ private flushQueue;
96
104
  }
97
105
  declare function initHuskel(config: HuskelConfig): HuskelClient;
98
106
  declare function getHuskelClient(): HuskelClient;
99
107
 
108
+ /**
109
+ * @deprecated Use <HuskelProvider> instead to avoid SSR issues.
110
+ */
111
+ declare function useHuskel(config: HuskelConfig): HuskelClient;
112
+
100
113
  interface UseSearchReturn {
101
114
  results: SearchResult[];
102
115
  loading: boolean;
@@ -106,11 +119,13 @@ interface UseSearchReturn {
106
119
  }
107
120
  declare function useSearch(): UseSearchReturn;
108
121
 
109
- interface UseHuskelOptions extends HuskelConfig {
110
- onIngest?: (products: Product[]) => void;
111
- onError?: (err: Error) => void;
122
+ interface UseIngestReturn {
123
+ ingest: (product: RawProductInput) => Promise<void>;
124
+ ingestBatch: (products: RawProductInput[]) => Promise<void>;
125
+ loading: boolean;
126
+ error: string | null;
112
127
  }
113
- declare function useHuskel(options: UseHuskelOptions): HuskelClient | null;
128
+ declare function useIngest(): UseIngestReturn;
114
129
 
115
130
  interface SearchBarProps {
116
131
  placeholder?: string;
@@ -118,15 +133,23 @@ interface SearchBarProps {
118
133
  debounceMs?: number;
119
134
  onSelect?: (result: SearchResult) => void;
120
135
  className?: string;
136
+ inputClassName?: string;
137
+ dropdownClassName?: string;
121
138
  renderResult?: (result: SearchResult) => React.ReactNode;
122
139
  }
123
- declare function SearchBar({ placeholder, limit, debounceMs, onSelect, className, renderResult, }: SearchBarProps): react_jsx_runtime.JSX.Element;
140
+ declare function SearchBar({ placeholder, limit, debounceMs, onSelect, className, inputClassName, dropdownClassName, renderResult, }: SearchBarProps): react_jsx_runtime.JSX.Element;
124
141
 
125
142
  interface SparkleProps {
126
143
  productName: string;
127
- className?: string;
144
+ limit?: number;
128
145
  onResult?: (results: SearchResult[]) => void;
146
+ className?: string;
147
+ }
148
+ declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
149
+
150
+ interface HuskelProviderProps extends HuskelConfig {
151
+ children: React.ReactNode;
129
152
  }
130
- declare function Sparkle({ productName, className, onResult }: SparkleProps): react_jsx_runtime.JSX.Element;
153
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
131
154
 
132
- export { HuskelAPI, HuskelClient, type HuskelConfig, type IngestResponse, type Product, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, type SiteConfig, Sparkle, getHuskelClient, initHuskel, useHuskel, useSearch };
155
+ export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, useSearch };