@hasna/connectors 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/serve.js CHANGED
@@ -135,6 +135,22 @@ function migrate(db) {
135
135
  created_at TEXT NOT NULL
136
136
  )
137
137
  `);
138
+ db.run(`
139
+ CREATE TABLE IF NOT EXISTS connector_usage (
140
+ id TEXT PRIMARY KEY,
141
+ connector TEXT NOT NULL,
142
+ action TEXT NOT NULL,
143
+ agent_id TEXT,
144
+ timestamp TEXT NOT NULL
145
+ )
146
+ `);
147
+ db.run(`CREATE INDEX IF NOT EXISTS idx_usage_connector ON connector_usage(connector, timestamp DESC)`);
148
+ db.run(`
149
+ CREATE TABLE IF NOT EXISTS connector_promotions (
150
+ connector TEXT UNIQUE NOT NULL,
151
+ promoted_at TEXT NOT NULL
152
+ )
153
+ `);
138
154
  }
139
155
  var DB_DIR, DB_PATH, _db = null;
140
156
  var init_database = __esm(() => {
@@ -483,6 +499,75 @@ var init_scheduler = __esm(() => {
483
499
  init_strip();
484
500
  });
485
501
 
502
+ // src/db/usage.ts
503
+ var exports_usage = {};
504
+ __export(exports_usage, {
505
+ logUsage: () => logUsage,
506
+ getUsageStats: () => getUsageStats,
507
+ getUsageMap: () => getUsageMap,
508
+ getTopConnectors: () => getTopConnectors,
509
+ cleanOldUsage: () => cleanOldUsage
510
+ });
511
+ function logUsage(connector, action, agentId, db) {
512
+ const d = db ?? getDatabase();
513
+ d.run("INSERT INTO connector_usage (id, connector, action, agent_id, timestamp) VALUES (?, ?, ?, ?, ?)", [shortUuid(), connector, action, agentId ?? null, now()]);
514
+ }
515
+ function getUsageStats(connector, db) {
516
+ const d = db ?? getDatabase();
517
+ const total = d.query("SELECT COUNT(*) as c FROM connector_usage WHERE connector = ?").get(connector).c;
518
+ const d7 = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
519
+ const last7d = d.query("SELECT COUNT(*) as c FROM connector_usage WHERE connector = ? AND timestamp > ?").get(connector, d7).c;
520
+ const d1 = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
521
+ const last24h = d.query("SELECT COUNT(*) as c FROM connector_usage WHERE connector = ? AND timestamp > ?").get(connector, d1).c;
522
+ return { connector, total, last7d, last24h };
523
+ }
524
+ function getTopConnectors(limit = 10, days = 7, db) {
525
+ const d = db ?? getDatabase();
526
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
527
+ return d.query("SELECT connector, COUNT(*) as count FROM connector_usage WHERE timestamp > ? GROUP BY connector ORDER BY count DESC LIMIT ?").all(since, limit);
528
+ }
529
+ function getUsageMap(days = 7, db) {
530
+ const top = getTopConnectors(100, days, db);
531
+ return new Map(top.map((t) => [t.connector, t.count]));
532
+ }
533
+ function cleanOldUsage(days = 30, db) {
534
+ const d = db ?? getDatabase();
535
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
536
+ return d.run("DELETE FROM connector_usage WHERE timestamp < ?", [cutoff]).changes;
537
+ }
538
+ var init_usage = __esm(() => {
539
+ init_database();
540
+ });
541
+
542
+ // src/db/promotions.ts
543
+ var exports_promotions = {};
544
+ __export(exports_promotions, {
545
+ promoteConnector: () => promoteConnector,
546
+ isPromoted: () => isPromoted,
547
+ getPromotedConnectors: () => getPromotedConnectors,
548
+ demoteConnector: () => demoteConnector
549
+ });
550
+ function promoteConnector(name, db) {
551
+ const d = db ?? getDatabase();
552
+ d.run("INSERT OR REPLACE INTO connector_promotions (connector, promoted_at) VALUES (?, ?)", [name, now()]);
553
+ }
554
+ function demoteConnector(name, db) {
555
+ const d = db ?? getDatabase();
556
+ return d.run("DELETE FROM connector_promotions WHERE connector = ?", [name]).changes > 0;
557
+ }
558
+ function getPromotedConnectors(db) {
559
+ const d = db ?? getDatabase();
560
+ return d.query("SELECT connector FROM connector_promotions ORDER BY promoted_at DESC").all().map((r) => r.connector);
561
+ }
562
+ function isPromoted(name, db) {
563
+ const d = db ?? getDatabase();
564
+ const row = d.query("SELECT 1 FROM connector_promotions WHERE connector = ?").get(name);
565
+ return !!row;
566
+ }
567
+ var init_promotions = __esm(() => {
568
+ init_database();
569
+ });
570
+
486
571
  // src/server/serve.ts
487
572
  import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync6 } from "fs";
488
573
 
@@ -6982,6 +7067,16 @@ function loadTokens(name) {
6982
7067
  return null;
6983
7068
  }
6984
7069
  }
7070
+ const profileConfig = loadProfileConfig(name);
7071
+ if (profileConfig.refreshToken || profileConfig.accessToken) {
7072
+ return {
7073
+ accessToken: profileConfig.accessToken,
7074
+ refreshToken: profileConfig.refreshToken,
7075
+ expiresAt: profileConfig.expiresAt,
7076
+ tokenType: profileConfig.tokenType,
7077
+ scope: profileConfig.scope
7078
+ };
7079
+ }
6985
7080
  return null;
6986
7081
  }
6987
7082
  function getAuthStatus(name) {
@@ -7560,6 +7655,30 @@ Dashboard not found at: ${dashboardDir}`);
7560
7655
  if (path === "/api/activity" && method === "GET") {
7561
7656
  return json(activityLog, 200, port);
7562
7657
  }
7658
+ if (path === "/api/hot" && method === "GET") {
7659
+ const { getTopConnectors: getTopConnectors2 } = await Promise.resolve().then(() => (init_usage(), exports_usage));
7660
+ const { getPromotedConnectors: getPromotedConnectors2 } = await Promise.resolve().then(() => (init_promotions(), exports_promotions));
7661
+ const limit = parseInt(url2.searchParams.get("limit") || "10", 10);
7662
+ const days = parseInt(url2.searchParams.get("days") || "7", 10);
7663
+ const db = getDatabase2();
7664
+ const top = getTopConnectors2(limit, days, db);
7665
+ const promoted = new Set(getPromotedConnectors2(db));
7666
+ return json(top.map((t) => ({ ...t, promoted: promoted.has(t.connector) })), 200, port);
7667
+ }
7668
+ const promoteMatch = path.match(/^\/api\/connectors\/([^/]+)\/promote$/);
7669
+ if (promoteMatch && method === "POST") {
7670
+ const name = promoteMatch[1];
7671
+ if (!getConnector(name))
7672
+ return json({ error: "Connector not found" }, 404, port);
7673
+ const { promoteConnector: promoteConnector2 } = await Promise.resolve().then(() => (init_promotions(), exports_promotions));
7674
+ promoteConnector2(name, getDatabase2());
7675
+ return json({ success: true, connector: name }, 200, port);
7676
+ }
7677
+ if (promoteMatch && method === "DELETE") {
7678
+ const { demoteConnector: demoteConnector2 } = await Promise.resolve().then(() => (init_promotions(), exports_promotions));
7679
+ const removed = demoteConnector2(promoteMatch[1], getDatabase2());
7680
+ return json({ success: removed, connector: promoteMatch[1] }, 200, port);
7681
+ }
7563
7682
  if (path === "/api/llm" && method === "GET") {
7564
7683
  const config = getLlmConfig();
7565
7684
  if (!config)
@@ -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.0",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {