@hasna/connectors 1.2.1 → 1.3.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.
@@ -188,55 +188,167 @@ configCmd
188
188
  });
189
189
 
190
190
  // ============================================
191
- // Example API Commands - Replace with HuggingFace API commands
191
+ // Models Commands
192
192
  // ============================================
193
- const exampleCmd = program
194
- .command('example')
195
- .description('Example API commands (replace with HuggingFace commands)');
193
+ const modelsCmd = program.command('models').description('Search and browse HuggingFace models');
196
194
 
197
- exampleCmd
198
- .command('list')
199
- .description('List resources')
200
- .option('-n, --max <number>', 'Maximum results', '10')
201
- .action(async (opts) => {
195
+ modelsCmd
196
+ .command('search')
197
+ .description('Search models')
198
+ .argument('[query]', 'Search query')
199
+ .option('--task <task>', 'Filter by task (text-generation, text2text-generation, etc)')
200
+ .option('--library <lib>', 'Filter by library (transformers, gguf, pytorch)')
201
+ .option('--author <author>', 'Filter by author')
202
+ .option('--sort <field>', 'Sort by: likes, downloads, trending, lastModified', 'trending')
203
+ .option('--limit <n>', 'Max results', '20')
204
+ .action(async (query, opts) => {
202
205
  try {
203
206
  const client = getClient();
204
- const result = await client.example.list({ maxResults: parseInt(opts.max) });
205
- print(result, getFormat(exampleCmd));
206
- } catch (err) {
207
- error(String(err));
208
- process.exit(1);
209
- }
207
+ const results = await client.models.search({
208
+ search: query, filter: opts.task, library: opts.library,
209
+ author: opts.author, sort: opts.sort, limit: parseInt(opts.limit),
210
+ });
211
+ print(results.map(m => ({ id: m.id, task: m.pipeline_tag, library: m.library_name, downloads: m.downloads, likes: m.likes })), getFormat(modelsCmd));
212
+ } catch (err) { error(String(err)); process.exit(1); }
210
213
  });
211
214
 
212
- exampleCmd
215
+ modelsCmd
213
216
  .command('get <id>')
214
- .description('Get a resource by ID')
215
- .action(async (id: string) => {
217
+ .description('Get model details (e.g. meta-llama/Meta-Llama-3-8B)')
218
+ .action(async (id) => {
216
219
  try {
217
220
  const client = getClient();
218
- const result = await client.example.get(id);
219
- print(result, getFormat(exampleCmd));
220
- } catch (err) {
221
- error(String(err));
222
- process.exit(1);
223
- }
221
+ const result = await client.models.get(id);
222
+ print(result, getFormat(modelsCmd));
223
+ } catch (err) { error(String(err)); process.exit(1); }
224
224
  });
225
225
 
226
- exampleCmd
227
- .command('create')
228
- .description('Create a new resource')
229
- .requiredOption('-n, --name <name>', 'Resource name')
230
- .action(async (opts) => {
226
+ modelsCmd
227
+ .command('files <id>')
228
+ .description('List files in a model repo')
229
+ .action(async (id) => {
231
230
  try {
232
231
  const client = getClient();
233
- const result = await client.example.create({ name: opts.name });
234
- success('Resource created!');
235
- print(result, getFormat(exampleCmd));
236
- } catch (err) {
237
- error(String(err));
238
- process.exit(1);
239
- }
232
+ const files = await client.models.files(id);
233
+ print(files.map(f => ({ name: f.rfilename, size: f.lfs?.size ?? f.size ?? null })), getFormat(modelsCmd));
234
+ } catch (err) { error(String(err)); process.exit(1); }
235
+ });
236
+
237
+ // ============================================
238
+ // Inference Commands
239
+ // ============================================
240
+ const inferCmd = program.command('inference').description('Run model inference via HF Inference API');
241
+
242
+ inferCmd
243
+ .command('text-generation <model>')
244
+ .description('Generate text from a prompt')
245
+ .requiredOption('--prompt <text>', 'Input prompt')
246
+ .option('--max-tokens <n>', 'Max new tokens', '256')
247
+ .option('--temperature <t>', 'Temperature', '0.7')
248
+ .action(async (model, opts) => {
249
+ try {
250
+ const client = getClient();
251
+ const results = await client.inference.textGeneration(model, opts.prompt, {
252
+ max_new_tokens: parseInt(opts.maxTokens), temperature: parseFloat(opts.temperature),
253
+ });
254
+ print(results, getFormat(inferCmd));
255
+ } catch (err) { error(String(err)); process.exit(1); }
256
+ });
257
+
258
+ inferCmd
259
+ .command('chat <model>')
260
+ .description('Chat completion (for chat models)')
261
+ .requiredOption('--messages <json>', 'Messages JSON array')
262
+ .option('--max-tokens <n>', 'Max new tokens', '256')
263
+ .option('--temperature <t>', 'Temperature', '0.7')
264
+ .action(async (model, opts) => {
265
+ try {
266
+ const messages = JSON.parse(opts.messages);
267
+ const client = getClient();
268
+ const result = await client.inference.chat(model, messages, {
269
+ max_new_tokens: parseInt(opts.maxTokens), temperature: parseFloat(opts.temperature),
270
+ });
271
+ print(result, getFormat(inferCmd));
272
+ } catch (err) { error(String(err)); process.exit(1); }
273
+ });
274
+
275
+ // ============================================
276
+ // Datasets Commands
277
+ // ============================================
278
+ const datasetsCmd = program.command('datasets').description('Search and browse HuggingFace datasets');
279
+
280
+ datasetsCmd
281
+ .command('search')
282
+ .description('Search datasets')
283
+ .argument('[query]', 'Search query')
284
+ .option('--author <author>', 'Filter by author')
285
+ .option('--sort <field>', 'Sort by: likes, downloads, trending', 'trending')
286
+ .option('--limit <n>', 'Max results', '20')
287
+ .action(async (query, opts) => {
288
+ try {
289
+ const client = getClient();
290
+ const results = await client.datasets.search({
291
+ search: query, author: opts.author, sort: opts.sort, limit: parseInt(opts.limit),
292
+ });
293
+ print(results.map(d => ({ id: d.id, downloads: d.downloads, likes: d.likes, tags: d.tags?.slice(0, 5) })), getFormat(datasetsCmd));
294
+ } catch (err) { error(String(err)); process.exit(1); }
295
+ });
296
+
297
+ datasetsCmd
298
+ .command('get <id>')
299
+ .description('Get dataset details')
300
+ .action(async (id) => {
301
+ try {
302
+ const client = getClient();
303
+ const result = await client.datasets.get(id);
304
+ print(result, getFormat(datasetsCmd));
305
+ } catch (err) { error(String(err)); process.exit(1); }
306
+ });
307
+
308
+ datasetsCmd
309
+ .command('preview <id>')
310
+ .description('Preview first N rows of a dataset')
311
+ .option('--split <split>', 'Dataset split', 'train')
312
+ .option('--rows <n>', 'Number of rows', '10')
313
+ .action(async (id, opts) => {
314
+ try {
315
+ const client = getClient();
316
+ const result = await client.datasets.preview(id, 'default', opts.split, parseInt(opts.rows));
317
+ print(result, getFormat(datasetsCmd));
318
+ } catch (err) { error(String(err)); process.exit(1); }
319
+ });
320
+
321
+ // ============================================
322
+ // Spaces Commands
323
+ // ============================================
324
+ const spacesCmd = program.command('spaces').description('Search and browse HuggingFace Spaces');
325
+
326
+ spacesCmd
327
+ .command('search')
328
+ .description('Search spaces')
329
+ .argument('[query]', 'Search query')
330
+ .option('--author <author>', 'Filter by author')
331
+ .option('--sort <field>', 'Sort by: likes, trending', 'trending')
332
+ .option('--limit <n>', 'Max results', '20')
333
+ .action(async (query, opts) => {
334
+ try {
335
+ const client = getClient();
336
+ const results = await client.spaces.search({
337
+ search: query, author: opts.author, sort: opts.sort, limit: parseInt(opts.limit),
338
+ });
339
+ print(results.map(s => ({ id: s.id, sdk: s.sdk, likes: s.likes })), getFormat(spacesCmd));
340
+ } catch (err) { error(String(err)); process.exit(1); }
341
+ });
342
+
343
+ spacesCmd
344
+ .command('get <id>')
345
+ .description('Get space details')
346
+ .action(async (id) => {
347
+ try {
348
+ const client = getClient();
349
+ const result = await client.spaces.get(id);
350
+ print(result, getFormat(spacesCmd));
351
+ } catch (err) { error(String(err)); process.exit(1); }
240
352
  });
241
353
 
242
354
  // Parse and execute
@@ -5,8 +5,20 @@ import type {
5
5
  ImageOptions,
6
6
  } from '../types';
7
7
 
8
+ /** Models that use the new gpt-image API (different params than DALL-E) */
9
+ const GPT_IMAGE_MODELS = ['gpt-image-1'];
10
+
11
+ function isGptImage(model: string): boolean {
12
+ return GPT_IMAGE_MODELS.some((m) => model.startsWith(m));
13
+ }
14
+
8
15
  /**
9
- * Images API (DALL-E)
16
+ * Images API — supports both DALL-E 3 and gpt-image-1
17
+ *
18
+ * gpt-image-1 differences:
19
+ * - Uses `output_format` instead of `response_format`
20
+ * - Does NOT support `style` parameter
21
+ * - Does NOT support `response_format` parameter
10
22
  */
11
23
  export class ImagesApi {
12
24
  constructor(private readonly client: OpenAIClient) {}
@@ -18,13 +30,29 @@ export class ImagesApi {
18
30
  prompt: string,
19
31
  options: ImageOptions = {}
20
32
  ): Promise<ImageResponse> {
33
+ const model = options.model || 'dall-e-3';
34
+
35
+ if (isGptImage(model)) {
36
+ // gpt-image-1: different parameter set
37
+ const request: Record<string, unknown> = {
38
+ model,
39
+ prompt,
40
+ n: options.n || 1,
41
+ };
42
+ if (options.size !== undefined) request.size = options.size;
43
+ if (options.quality !== undefined) request.quality = options.quality;
44
+ // gpt-image-1 uses output_format, not response_format. No style param.
45
+ request.output_format = 'url';
46
+ return this.client.post<ImageResponse>('/images/generations', request);
47
+ }
48
+
49
+ // DALL-E 3: original parameter set
21
50
  const request: ImageGenerateRequest = {
22
- model: options.model || 'dall-e-3',
51
+ model,
23
52
  prompt,
24
53
  n: options.n || 1,
25
54
  response_format: 'url',
26
55
  };
27
-
28
56
  if (options.size !== undefined) request.size = options.size;
29
57
  if (options.quality !== undefined) request.quality = options.quality;
30
58
  if (options.style !== undefined) request.style = options.style;
@@ -54,22 +82,38 @@ export class ImagesApi {
54
82
  prompt: string,
55
83
  options: Omit<ImageOptions, 'n'> = {}
56
84
  ): Promise<string> {
85
+ const model = options.model || 'dall-e-3';
86
+
87
+ if (isGptImage(model)) {
88
+ const request: Record<string, unknown> = {
89
+ model,
90
+ prompt,
91
+ n: 1,
92
+ output_format: 'b64_json',
93
+ };
94
+ if (options.size !== undefined) request.size = options.size;
95
+ if (options.quality !== undefined) request.quality = options.quality;
96
+
97
+ const response = await this.client.post<ImageResponse>('/images/generations', request);
98
+ const b64 = response.data[0]?.b64_json;
99
+ if (!b64) throw new Error('No image data in response');
100
+ return b64;
101
+ }
102
+
103
+ // DALL-E 3
57
104
  const request: ImageGenerateRequest = {
58
- model: options.model || 'dall-e-3',
105
+ model,
59
106
  prompt,
60
107
  n: 1,
61
108
  response_format: 'b64_json',
62
109
  };
63
-
64
110
  if (options.size !== undefined) request.size = options.size;
65
111
  if (options.quality !== undefined) request.quality = options.quality;
66
112
  if (options.style !== undefined) request.style = options.style;
67
113
 
68
114
  const response = await this.client.post<ImageResponse>('/images/generations', request);
69
115
  const b64 = response.data[0]?.b64_json;
70
- if (!b64) {
71
- throw new Error('No image data in response');
72
- }
116
+ if (!b64) throw new Error('No image data in response');
73
117
  return b64;
74
118
  }
75
119
  }
@@ -0,0 +1,5 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export declare function promoteConnector(name: string, db?: Database): void;
3
+ export declare function demoteConnector(name: string, db?: Database): boolean;
4
+ export declare function getPromotedConnectors(db?: Database): string[];
5
+ export declare function isPromoted(name: string, db?: Database): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export declare function logUsage(connector: string, action: string, agentId?: string, db?: Database): void;
3
+ export interface UsageStats {
4
+ connector: string;
5
+ total: number;
6
+ last7d: number;
7
+ last24h: number;
8
+ }
9
+ export declare function getUsageStats(connector: string, db?: Database): UsageStats;
10
+ export interface TopConnector {
11
+ connector: string;
12
+ count: number;
13
+ }
14
+ export declare function getTopConnectors(limit?: number, days?: number, db?: Database): TopConnector[];
15
+ /** Get usage counts as a Map for search context */
16
+ export declare function getUsageMap(days?: number, db?: Database): Map<string, number>;
17
+ export declare function cleanOldUsage(days?: number, db?: Database): number;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * Or use the interactive CLI:
8
8
  * npx @hasna/connectors
9
9
  */
10
- export { CONNECTORS, CATEGORIES, getConnector, getConnectorsByCategory, searchConnectors, loadConnectorVersions, type ConnectorMeta, type Category, } from "./lib/registry.js";
10
+ export { CONNECTORS, CATEGORIES, getConnector, getConnectorsByCategory, searchConnectors, loadConnectorVersions, type ConnectorMeta, type Category, type ScoredResult, type SearchContext, } from "./lib/registry.js";
11
11
  export { installConnector, installConnectors, getInstalledConnectors, removeConnector, connectorExists, getConnectorPath, getConnectorDocs, type InstallResult, type InstallOptions, type ConnectorDocs, } from "./lib/installer.js";
12
12
  export { runConnectorCommand, getConnectorOperations, getConnectorCommandHelp, getConnectorCliPath, getConnectorsWithCli, type RunResult, } from "./lib/runner.js";
package/dist/index.js CHANGED
@@ -5,6 +5,94 @@ var __require = import.meta.require;
5
5
  import { existsSync, readFileSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
+
9
+ // src/lib/fuzzy.ts
10
+ function levenshtein(a, b) {
11
+ const m = a.length;
12
+ const n = b.length;
13
+ if (m === 0)
14
+ return n;
15
+ if (n === 0)
16
+ return m;
17
+ let prev = new Array(n + 1);
18
+ let curr = new Array(n + 1);
19
+ for (let j = 0;j <= n; j++)
20
+ prev[j] = j;
21
+ for (let i = 1;i <= m; i++) {
22
+ curr[0] = i;
23
+ for (let j = 1;j <= n; j++) {
24
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
25
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
26
+ }
27
+ [prev, curr] = [curr, prev];
28
+ }
29
+ return prev[n];
30
+ }
31
+ function bestFuzzyScore(token, candidates, maxDistance = 2) {
32
+ if (token.length < 3)
33
+ return 0;
34
+ let bestDist = maxDistance + 1;
35
+ for (const c of candidates) {
36
+ if (Math.abs(token.length - c.length) > maxDistance)
37
+ continue;
38
+ const d = levenshtein(token, c);
39
+ if (d < bestDist)
40
+ bestDist = d;
41
+ if (d === 0)
42
+ return maxDistance + 1;
43
+ }
44
+ if (bestDist > maxDistance)
45
+ return 0;
46
+ return maxDistance - bestDist + 1;
47
+ }
48
+
49
+ // src/lib/synonyms.ts
50
+ var SYNONYM_MAP = {
51
+ email: ["smtp", "mail", "inbox", "resend", "ses"],
52
+ chat: ["messaging", "im", "slack", "discord", "teams"],
53
+ sms: ["text", "twilio", "messaging"],
54
+ payment: ["billing", "invoicing", "commerce", "checkout", "stripe"],
55
+ payments: ["billing", "invoicing", "commerce", "checkout", "stripe"],
56
+ ecommerce: ["shop", "store", "commerce", "shopify"],
57
+ finance: ["banking", "accounting", "invoicing"],
58
+ crypto: ["blockchain", "web3", "wallet"],
59
+ ai: ["llm", "ml", "model", "gpt", "claude", "anthropic", "openai"],
60
+ llm: ["ai", "model", "gpt", "claude"],
61
+ auth: ["oauth", "sso", "login", "identity", "authentication"],
62
+ database: ["db", "sql", "nosql", "postgres", "mongo", "supabase"],
63
+ deploy: ["hosting", "infrastructure", "ci", "cd", "vercel"],
64
+ storage: ["files", "drive", "s3", "bucket", "upload"],
65
+ cloud: ["aws", "gcp", "azure", "infrastructure"],
66
+ api: ["rest", "graphql", "endpoint", "webhook"],
67
+ monitoring: ["logs", "observability", "alerting", "datadog", "sentry"],
68
+ ci: ["cd", "deploy", "pipeline", "github", "actions"],
69
+ crm: ["sales", "leads", "contacts", "hubspot", "salesforce"],
70
+ analytics: ["data", "metrics", "tracking", "mixpanel", "amplitude"],
71
+ project: ["task", "issue", "board", "jira", "linear", "asana"],
72
+ docs: ["documentation", "wiki", "notion", "confluence"],
73
+ design: ["figma", "sketch", "ui", "ux"],
74
+ security: ["auth", "encryption", "compliance", "vault"]
75
+ };
76
+ function expandQuery(tokens) {
77
+ const synonyms = new Set;
78
+ for (const token of tokens) {
79
+ const matches = SYNONYM_MAP[token];
80
+ if (matches) {
81
+ for (const syn of matches) {
82
+ if (!tokens.includes(syn))
83
+ synonyms.add(syn);
84
+ }
85
+ }
86
+ for (const [key, values] of Object.entries(SYNONYM_MAP)) {
87
+ if (values.includes(token) && !tokens.includes(key)) {
88
+ synonyms.add(key);
89
+ }
90
+ }
91
+ }
92
+ return { original: tokens, expanded: [...synonyms] };
93
+ }
94
+
95
+ // src/lib/registry.ts
8
96
  var CATEGORIES = [
9
97
  "AI & ML",
10
98
  "Developer Tools",
@@ -5923,9 +6011,152 @@ var CONNECTORS = [
5923
6011
  function getConnectorsByCategory(category) {
5924
6012
  return CONNECTORS.filter((c) => c.category === category);
5925
6013
  }
5926
- function searchConnectors(query) {
5927
- const q = query.toLowerCase();
5928
- return CONNECTORS.filter((c) => c.name.toLowerCase().includes(q) || c.displayName.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) || c.tags.some((t) => t.includes(q)));
6014
+ function searchConnectors(query, context) {
6015
+ const tokens = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
6016
+ if (tokens.length === 0)
6017
+ return [];
6018
+ const limit = context?.limit ?? 20;
6019
+ const installed = new Set(context?.installed ?? []);
6020
+ const promoted = new Set(context?.promoted ?? []);
6021
+ const usage = context?.usage ?? new Map;
6022
+ const results = [];
6023
+ for (const c of CONNECTORS) {
6024
+ const nameLow = c.name.toLowerCase();
6025
+ const displayLow = c.displayName.toLowerCase();
6026
+ const descLow = c.description.toLowerCase();
6027
+ const tagsLow = c.tags.map((t) => t.toLowerCase());
6028
+ let score = 0;
6029
+ const matchReasons = [];
6030
+ let allTokensMatch = true;
6031
+ for (const token of tokens) {
6032
+ let tokenMatched = false;
6033
+ if (nameLow === token) {
6034
+ score += 100;
6035
+ matchReasons.push(`name="${token}"`);
6036
+ tokenMatched = true;
6037
+ } else if (nameLow.includes(token)) {
6038
+ score += 10;
6039
+ matchReasons.push(`name~${token}`);
6040
+ tokenMatched = true;
6041
+ }
6042
+ if (tagsLow.includes(token)) {
6043
+ score += 8;
6044
+ if (!tokenMatched)
6045
+ matchReasons.push(`tag="${token}"`);
6046
+ tokenMatched = true;
6047
+ } else if (tagsLow.some((t) => t.includes(token))) {
6048
+ score += 5;
6049
+ if (!tokenMatched)
6050
+ matchReasons.push(`tag~${token}`);
6051
+ tokenMatched = true;
6052
+ }
6053
+ if (displayLow.includes(token)) {
6054
+ score += 3;
6055
+ if (!tokenMatched)
6056
+ matchReasons.push(`display~${token}`);
6057
+ tokenMatched = true;
6058
+ }
6059
+ if (descLow.includes(token)) {
6060
+ score += 1;
6061
+ if (!tokenMatched)
6062
+ matchReasons.push(`desc~${token}`);
6063
+ tokenMatched = true;
6064
+ }
6065
+ if (!tokenMatched && token.length >= 3) {
6066
+ const nameFuzzy = bestFuzzyScore(token, [nameLow], 1);
6067
+ if (nameFuzzy > 0) {
6068
+ score += nameFuzzy * 6;
6069
+ matchReasons.push(`fuzzy:name\u2248${token}`);
6070
+ tokenMatched = true;
6071
+ }
6072
+ if (!tokenMatched) {
6073
+ const tagFuzzy = bestFuzzyScore(token, tagsLow, 2);
6074
+ if (tagFuzzy > 0) {
6075
+ score += tagFuzzy * 3;
6076
+ matchReasons.push(`fuzzy:tag\u2248${token}`);
6077
+ tokenMatched = true;
6078
+ }
6079
+ }
6080
+ if (!tokenMatched) {
6081
+ const displayFuzzy = bestFuzzyScore(token, [displayLow], 2);
6082
+ if (displayFuzzy > 0) {
6083
+ score += displayFuzzy * 2;
6084
+ matchReasons.push(`fuzzy:display\u2248${token}`);
6085
+ tokenMatched = true;
6086
+ }
6087
+ }
6088
+ }
6089
+ if (!tokenMatched) {
6090
+ allTokensMatch = false;
6091
+ break;
6092
+ }
6093
+ }
6094
+ if (!allTokensMatch)
6095
+ continue;
6096
+ const badges = [];
6097
+ if (installed.has(c.name)) {
6098
+ score += 50;
6099
+ badges.push("installed");
6100
+ }
6101
+ if (promoted.has(c.name)) {
6102
+ score += 30;
6103
+ badges.push("promoted");
6104
+ }
6105
+ const usageCount = usage.get(c.name) ?? 0;
6106
+ if (usageCount > 0) {
6107
+ score += Math.min(usageCount * 2, 40);
6108
+ if (usageCount >= 5)
6109
+ badges.push("hot");
6110
+ }
6111
+ results.push({ ...c, score, matchReasons, badges });
6112
+ }
6113
+ const matchedNames = new Set(results.map((r) => r.name));
6114
+ if (results.length < limit) {
6115
+ const { expanded } = expandQuery(tokens);
6116
+ if (expanded.length > 0) {
6117
+ for (const c of CONNECTORS) {
6118
+ if (matchedNames.has(c.name))
6119
+ continue;
6120
+ const nameLow2 = c.name.toLowerCase();
6121
+ const tagsLow2 = c.tags.map((t) => t.toLowerCase());
6122
+ const descLow2 = c.description.toLowerCase();
6123
+ let synScore = 0;
6124
+ const synReasons = [];
6125
+ for (const syn of expanded) {
6126
+ if (nameLow2.includes(syn)) {
6127
+ synScore += 2;
6128
+ synReasons.push(`syn:name~${syn}`);
6129
+ } else if (tagsLow2.some((t) => t.includes(syn))) {
6130
+ synScore += 1;
6131
+ synReasons.push(`syn:tag~${syn}`);
6132
+ } else if (descLow2.includes(syn)) {
6133
+ synScore += 1;
6134
+ synReasons.push(`syn:desc~${syn}`);
6135
+ }
6136
+ }
6137
+ if (synScore > 0) {
6138
+ const badges = [];
6139
+ if (installed.has(c.name)) {
6140
+ synScore += 50;
6141
+ badges.push("installed");
6142
+ }
6143
+ if (promoted.has(c.name)) {
6144
+ synScore += 30;
6145
+ badges.push("promoted");
6146
+ }
6147
+ const usageCount = usage.get(c.name) ?? 0;
6148
+ if (usageCount > 0) {
6149
+ synScore += Math.min(usageCount * 2, 40);
6150
+ if (usageCount >= 5)
6151
+ badges.push("hot");
6152
+ }
6153
+ results.push({ ...c, score: synScore, matchReasons: synReasons, badges });
6154
+ }
6155
+ }
6156
+ }
6157
+ }
6158
+ results.sort((a, b) => b.score - a.score);
6159
+ return results.slice(0, limit);
5929
6160
  }
5930
6161
  function getConnector(name) {
5931
6162
  return CONNECTORS.find((c) => c.name === name);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Levenshtein distance for fuzzy connector search.
3
+ * No dependencies — pure implementation.
4
+ */
5
+ /** Compute edit distance between two strings */
6
+ export declare function levenshtein(a: string, b: string): number;
7
+ /**
8
+ * Check if token fuzzy-matches target within maxDistance.
9
+ * Only applies to tokens >= 3 chars to avoid false positives.
10
+ */
11
+ export declare function fuzzyMatch(token: string, target: string, maxDistance?: number): boolean;
12
+ /**
13
+ * Find best fuzzy match score for a token against a list of candidates.
14
+ * Returns 0 if no match within threshold.
15
+ */
16
+ export declare function bestFuzzyScore(token: string, candidates: string[], maxDistance?: number): number;
@@ -0,0 +1 @@
1
+ export {};
@@ -13,6 +13,27 @@ export declare const CATEGORIES: readonly ["AI & ML", "Developer Tools", "Design
13
13
  export type Category = (typeof CATEGORIES)[number];
14
14
  export declare const CONNECTORS: ConnectorMeta[];
15
15
  export declare function getConnectorsByCategory(category: Category): ConnectorMeta[];
16
- export declare function searchConnectors(query: string): ConnectorMeta[];
16
+ export interface SearchContext {
17
+ installed?: string[];
18
+ promoted?: string[];
19
+ usage?: Map<string, number>;
20
+ }
21
+ export interface ScoredResult extends ConnectorMeta {
22
+ score: number;
23
+ matchReasons: string[];
24
+ badges: string[];
25
+ }
26
+ /**
27
+ * Ranked connector search with multi-token AND, scoring, and context signals.
28
+ *
29
+ * Score formula:
30
+ * relevance (name/tag/displayName/description matches)
31
+ * + installed boost (+50)
32
+ * + promoted boost (+30)
33
+ * + usage boost (min(count * 2, 40))
34
+ */
35
+ export declare function searchConnectors(query: string, context?: SearchContext & {
36
+ limit?: number;
37
+ }): ScoredResult[];
17
38
  export declare function getConnector(name: string): ConnectorMeta | undefined;
18
39
  export declare function loadConnectorVersions(): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Synonym expansion for connector search.
3
+ * Domain-specific terms only — not a general thesaurus.
4
+ */
5
+ /**
6
+ * Expand search tokens with domain synonyms.
7
+ * Returns original tokens + synonym tokens (deduplicated).
8
+ */
9
+ export declare function expandQuery(tokens: string[]): {
10
+ original: string[];
11
+ expanded: string[];
12
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {