@hasna/connectors 0.2.4 → 0.2.6

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
@@ -34,6 +34,8 @@ npx @hasna/connectors
34
34
 
35
35
  ```bash
36
36
  connectors install figma stripe github # Install connectors
37
+ connectors install --preset ai # Install a preset bundle
38
+ connectors install --category "AI & ML" # Install entire category
37
39
  connectors install figma --overwrite # Overwrite existing
38
40
  connectors remove figma # Remove a connector
39
41
  ```
@@ -49,6 +51,10 @@ connectors info stripe # Connector details
49
51
  connectors docs gmail # Auth, env vars, CLI docs
50
52
  connectors status # Auth status of installed connectors
51
53
  connectors doctor # Health check and troubleshooting
54
+ connectors test # Verify API keys with real requests
55
+ connectors whoami # Show setup summary
56
+ connectors env # Generate .env.example
57
+ connectors presets # List preset bundles
52
58
  ```
53
59
 
54
60
  ### Dashboard
package/bin/index.js CHANGED
@@ -6496,14 +6496,94 @@ function App({ initialConnectors, overwrite = false }) {
6496
6496
  init_registry();
6497
6497
  init_installer();
6498
6498
  init_auth();
6499
- import { readdirSync as readdirSync4, statSync as statSync3 } from "fs";
6499
+ import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
6500
+ import { homedir as homedir3 } from "os";
6500
6501
  import { join as join5, relative } from "path";
6502
+
6503
+ // src/lib/test-endpoints.ts
6504
+ var TEST_ENDPOINTS = {
6505
+ anthropic: {
6506
+ url: "https://api.anthropic.com/v1/models",
6507
+ headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" })
6508
+ },
6509
+ openai: {
6510
+ url: "https://api.openai.com/v1/models",
6511
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6512
+ },
6513
+ xai: {
6514
+ url: "https://api.x.ai/v1/models",
6515
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6516
+ },
6517
+ mistral: {
6518
+ url: "https://api.mistral.ai/v1/models",
6519
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6520
+ },
6521
+ github: {
6522
+ url: "https://api.github.com/user",
6523
+ headers: (key) => ({ Authorization: `Bearer ${key}`, "User-Agent": "connectors-cli" })
6524
+ },
6525
+ stripe: {
6526
+ url: "https://api.stripe.com/v1/balance",
6527
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6528
+ },
6529
+ figma: {
6530
+ url: "https://api.figma.com/v1/me",
6531
+ headers: (key) => ({ "X-Figma-Token": key })
6532
+ },
6533
+ discord: {
6534
+ url: "https://discord.com/api/v10/users/@me",
6535
+ headers: (key) => ({ Authorization: `Bot ${key}` })
6536
+ },
6537
+ resend: {
6538
+ url: "https://api.resend.com/domains",
6539
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6540
+ },
6541
+ notion: {
6542
+ url: "https://api.notion.com/v1/users/me",
6543
+ headers: (key) => ({ Authorization: `Bearer ${key}`, "Notion-Version": "2022-06-28" })
6544
+ },
6545
+ exa: {
6546
+ url: "https://api.exa.ai/search",
6547
+ method: "POST",
6548
+ headers: (key) => ({ "x-api-key": key, "Content-Type": "application/json" })
6549
+ },
6550
+ sentry: {
6551
+ url: "https://sentry.io/api/0/",
6552
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6553
+ },
6554
+ huggingface: {
6555
+ url: "https://huggingface.co/api/whoami-v2",
6556
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6557
+ },
6558
+ elevenlabs: {
6559
+ url: "https://api.elevenlabs.io/v1/user",
6560
+ headers: (key) => ({ "xi-api-key": key })
6561
+ },
6562
+ cloudflare: {
6563
+ url: "https://api.cloudflare.com/client/v4/user/tokens/verify",
6564
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6565
+ },
6566
+ mixpanel: {
6567
+ url: "https://mixpanel.com/api/app/me",
6568
+ headers: (key) => ({ Authorization: `Bearer ${key}` })
6569
+ }
6570
+ };
6571
+
6572
+ // src/cli/index.tsx
6501
6573
  import { createInterface } from "readline";
6502
6574
  import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
6503
6575
  loadConnectorVersions();
6504
6576
  var isTTY = process.stdout.isTTY ?? false;
6577
+ var PRESETS = {
6578
+ fullstack: { description: "Full-stack web app essentials", connectors: ["stripe", "github", "resend", "anthropic", "figma"] },
6579
+ ai: { description: "AI and ML models", connectors: ["anthropic", "openai", "xai", "mistral", "googlegemini", "elevenlabs"] },
6580
+ google: { description: "Google Workspace suite", connectors: ["gmail", "googledrive", "googledocs", "googlesheets", "googlecalendar", "googletasks", "googlecontacts"] },
6581
+ social: { description: "Social media platforms", connectors: ["x", "reddit", "youtube", "tiktok", "meta", "discord", "substack"] },
6582
+ devtools: { description: "Developer tooling", connectors: ["github", "docker", "sentry", "cloudflare", "e2b", "firecrawl"] },
6583
+ commerce: { description: "Commerce and finance", connectors: ["stripe", "shopify", "revolut", "mercury", "pandadoc"] }
6584
+ };
6505
6585
  var program2 = new Command;
6506
- program2.name("connectors").description("Install API connectors for your project").version("0.2.4");
6586
+ program2.name("connectors").description("Install API connectors for your project").version("0.2.6");
6507
6587
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
6508
6588
  if (!isTTY) {
6509
6589
  console.log(`Non-interactive environment detected. Use a subcommand:
@@ -6533,7 +6613,36 @@ function listFilesRecursive(dir, base = dir) {
6533
6613
  }
6534
6614
  return files;
6535
6615
  }
6536
- program2.command("install").alias("add").argument("[connectors...]", "Connectors to install").option("-o, --overwrite", "Overwrite existing connectors", false).option("-d, --dry-run", "Preview what would be installed without making changes", false).option("--json", "Output results as JSON", false).description("Install one or more connectors").action((connectors, options) => {
6616
+ program2.command("install").alias("add").argument("[connectors...]", "Connectors to install").option("-o, --overwrite", "Overwrite existing connectors", false).option("-d, --dry-run", "Preview what would be installed without making changes", false).option("-c, --category <category>", "Install all connectors in a category").option("--preset <preset>", "Install a preset bundle (e.g. ai, fullstack, google)").option("--json", "Output results as JSON", false).description("Install one or more connectors").action((connectors, options) => {
6617
+ if (options.category) {
6618
+ const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
6619
+ if (!category) {
6620
+ if (options.json) {
6621
+ console.log(JSON.stringify({ error: `Unknown category: ${options.category}. Available: ${CATEGORIES.join(", ")}` }));
6622
+ } else {
6623
+ console.log(chalk2.red(`Unknown category: ${options.category}`));
6624
+ console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
6625
+ }
6626
+ process.exit(1);
6627
+ return;
6628
+ }
6629
+ const categoryConnectors = getConnectorsByCategory(category).map((c) => c.name);
6630
+ connectors.push(...categoryConnectors);
6631
+ }
6632
+ if (options.preset) {
6633
+ const preset = PRESETS[options.preset.toLowerCase()];
6634
+ if (!preset) {
6635
+ if (options.json) {
6636
+ console.log(JSON.stringify({ error: `Unknown preset: ${options.preset}. Available: ${Object.keys(PRESETS).join(", ")}` }));
6637
+ } else {
6638
+ console.log(chalk2.red(`Unknown preset: ${options.preset}`));
6639
+ console.log(chalk2.dim(`Available: ${Object.keys(PRESETS).join(", ")}`));
6640
+ }
6641
+ process.exit(1);
6642
+ return;
6643
+ }
6644
+ connectors.push(...preset.connectors);
6645
+ }
6537
6646
  if (connectors.length === 0) {
6538
6647
  if (!isTTY) {
6539
6648
  console.error("Error: specify connectors to install. Example: connectors install figma stripe");
@@ -6665,7 +6774,41 @@ Next steps:`));
6665
6774
  }
6666
6775
  process.exit(results.every((r) => r.success) ? 0 : 1);
6667
6776
  });
6668
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available connectors", false).option("-i, --installed", "Show only installed connectors", false).option("--json", "Output as JSON", false).description("List available or installed connectors").action((options) => {
6777
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available connectors", false).option("-i, --installed", "Show only installed connectors", false).option("-b, --brief", "Output only connector names", false).option("--json", "Output as JSON", false).description("List available or installed connectors").action((options) => {
6778
+ if (options.brief) {
6779
+ if (options.installed) {
6780
+ const installed = getInstalledConnectors();
6781
+ if (options.json) {
6782
+ console.log(JSON.stringify(installed));
6783
+ } else {
6784
+ for (const name of installed)
6785
+ console.log(name);
6786
+ }
6787
+ } else if (options.category) {
6788
+ const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
6789
+ if (!category) {
6790
+ console.error(`Unknown category: ${options.category}`);
6791
+ process.exit(1);
6792
+ return;
6793
+ }
6794
+ const names = getConnectorsByCategory(category).map((c) => c.name);
6795
+ if (options.json) {
6796
+ console.log(JSON.stringify(names));
6797
+ } else {
6798
+ for (const n of names)
6799
+ console.log(n);
6800
+ }
6801
+ } else {
6802
+ const names = CONNECTORS.map((c) => c.name);
6803
+ if (options.json) {
6804
+ console.log(JSON.stringify(names));
6805
+ } else {
6806
+ for (const n of names)
6807
+ console.log(n);
6808
+ }
6809
+ }
6810
+ return;
6811
+ }
6669
6812
  if (options.installed) {
6670
6813
  const installed = getInstalledConnectors();
6671
6814
  if (installed.length === 0) {
@@ -7284,13 +7427,10 @@ Open this URL to authenticate:
7284
7427
  connector,
7285
7428
  authType,
7286
7429
  configured: statusAfter2.configured,
7287
- field: options.field || null
7430
+ field: options.field || "apiKey"
7288
7431
  }));
7289
7432
  } else {
7290
- console.log(chalk2.green(`\u2713 API key saved for ${meta.displayName}`));
7291
- if (options.field) {
7292
- console.log(chalk2.dim(` Field: ${options.field}`));
7293
- }
7433
+ console.log(chalk2.green(`\u2713 Saved ${options.field || "apiKey"} for ${meta.displayName}`));
7294
7434
  }
7295
7435
  process.exit(0);
7296
7436
  return;
@@ -7503,4 +7643,503 @@ Next steps:
7503
7643
  console.log();
7504
7644
  process.exit(results.every((r) => r.success) ? 0 : 1);
7505
7645
  });
7646
+ program2.command("export").option("-o, --output <file>", "Write to file instead of stdout").description("Export all connector credentials as JSON backup").action((options) => {
7647
+ const connectDir = join5(homedir3(), ".connectors");
7648
+ const result = {};
7649
+ if (existsSync5(connectDir)) {
7650
+ for (const entry of readdirSync4(connectDir)) {
7651
+ const entryPath = join5(connectDir, entry);
7652
+ if (!statSync3(entryPath).isDirectory() || !entry.startsWith("connect-"))
7653
+ continue;
7654
+ const connectorName = entry.replace(/^connect-/, "");
7655
+ const profilesDir = join5(entryPath, "profiles");
7656
+ if (!existsSync5(profilesDir))
7657
+ continue;
7658
+ const profiles = {};
7659
+ for (const pEntry of readdirSync4(profilesDir)) {
7660
+ const pPath = join5(profilesDir, pEntry);
7661
+ if (statSync3(pPath).isFile() && pEntry.endsWith(".json")) {
7662
+ try {
7663
+ profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync5(pPath, "utf-8"));
7664
+ } catch {}
7665
+ } else if (statSync3(pPath).isDirectory()) {
7666
+ const configPath = join5(pPath, "config.json");
7667
+ if (existsSync5(configPath)) {
7668
+ try {
7669
+ profiles[pEntry] = JSON.parse(readFileSync5(configPath, "utf-8"));
7670
+ } catch {}
7671
+ }
7672
+ }
7673
+ }
7674
+ if (Object.keys(profiles).length > 0)
7675
+ result[connectorName] = { profiles };
7676
+ }
7677
+ }
7678
+ const exportData = JSON.stringify({ connectors: result, exportedAt: new Date().toISOString() }, null, 2);
7679
+ if (options.output) {
7680
+ writeFileSync4(options.output, exportData);
7681
+ console.log(chalk2.green(`\u2713 Exported to ${options.output}`));
7682
+ } else {
7683
+ console.log(exportData);
7684
+ }
7685
+ });
7686
+ program2.command("import").argument("<file>", "JSON backup file to import (use - for stdin)").option("--json", "Output as JSON", false).description("Import connector credentials from a JSON backup").action(async (file, options) => {
7687
+ let raw;
7688
+ if (file === "-") {
7689
+ const chunks = [];
7690
+ for await (const chunk of process.stdin)
7691
+ chunks.push(chunk.toString());
7692
+ raw = chunks.join("");
7693
+ } else {
7694
+ if (!existsSync5(file)) {
7695
+ if (options.json) {
7696
+ console.log(JSON.stringify({ error: `File not found: ${file}` }));
7697
+ } else {
7698
+ console.log(chalk2.red(`File not found: ${file}`));
7699
+ }
7700
+ process.exit(1);
7701
+ return;
7702
+ }
7703
+ raw = readFileSync5(file, "utf-8");
7704
+ }
7705
+ let data;
7706
+ try {
7707
+ data = JSON.parse(raw);
7708
+ } catch {
7709
+ if (options.json) {
7710
+ console.log(JSON.stringify({ error: "Invalid JSON" }));
7711
+ } else {
7712
+ console.log(chalk2.red("Invalid JSON in import file"));
7713
+ }
7714
+ process.exit(1);
7715
+ return;
7716
+ }
7717
+ if (!data.connectors || typeof data.connectors !== "object") {
7718
+ if (options.json) {
7719
+ console.log(JSON.stringify({ error: "Invalid format: missing 'connectors' object" }));
7720
+ } else {
7721
+ console.log(chalk2.red("Invalid format: missing 'connectors' object"));
7722
+ }
7723
+ process.exit(1);
7724
+ return;
7725
+ }
7726
+ const connectDir = join5(homedir3(), ".connectors");
7727
+ let imported = 0;
7728
+ for (const [connectorName, connData] of Object.entries(data.connectors)) {
7729
+ if (!/^[a-z0-9-]+$/.test(connectorName))
7730
+ continue;
7731
+ if (!connData.profiles || typeof connData.profiles !== "object")
7732
+ continue;
7733
+ const profilesDir = join5(connectDir, `connect-${connectorName}`, "profiles");
7734
+ for (const [profileName, config] of Object.entries(connData.profiles)) {
7735
+ if (!config || typeof config !== "object")
7736
+ continue;
7737
+ mkdirSync4(profilesDir, { recursive: true });
7738
+ writeFileSync4(join5(profilesDir, `${profileName}.json`), JSON.stringify(config, null, 2));
7739
+ imported++;
7740
+ }
7741
+ }
7742
+ if (options.json) {
7743
+ console.log(JSON.stringify({ success: true, imported }));
7744
+ } else {
7745
+ console.log(chalk2.green(`\u2713 Imported ${imported} profile(s)`));
7746
+ }
7747
+ });
7748
+ program2.command("upgrade").alias("self-update").option("--check", "Only check for updates, don't install", false).option("--json", "Output as JSON", false).description("Check for updates and upgrade to the latest version").action(async (options) => {
7749
+ const currentVersion = "0.2.6";
7750
+ try {
7751
+ const res = await fetch("https://registry.npmjs.org/@hasna/connectors/latest");
7752
+ if (!res.ok)
7753
+ throw new Error(`npm registry returned ${res.status}`);
7754
+ const data = await res.json();
7755
+ const latestVersion = data.version;
7756
+ const isUpToDate = currentVersion === latestVersion;
7757
+ if (options.json) {
7758
+ console.log(JSON.stringify({ current: currentVersion, latest: latestVersion, upToDate: isUpToDate }));
7759
+ if (options.check) {
7760
+ process.exit(isUpToDate ? 0 : 1);
7761
+ return;
7762
+ }
7763
+ } else {
7764
+ console.log(`
7765
+ Current: ${chalk2.cyan(currentVersion)}`);
7766
+ console.log(` Latest: ${chalk2.cyan(latestVersion)}`);
7767
+ if (isUpToDate) {
7768
+ console.log(chalk2.green(`
7769
+ Already up to date!
7770
+ `));
7771
+ process.exit(0);
7772
+ return;
7773
+ }
7774
+ console.log(chalk2.yellow(`
7775
+ Update available: ${currentVersion} \u2192 ${latestVersion}`));
7776
+ }
7777
+ if (options.check) {
7778
+ if (!options.json)
7779
+ console.log(chalk2.dim(`
7780
+ Run 'connectors upgrade' to install.
7781
+ `));
7782
+ process.exit(isUpToDate ? 0 : 1);
7783
+ return;
7784
+ }
7785
+ if (!options.json)
7786
+ console.log(chalk2.dim(`
7787
+ Upgrading...`));
7788
+ const { execSync } = await import("child_process");
7789
+ try {
7790
+ execSync(`bun install -g @hasna/connectors@${latestVersion}`, { stdio: options.json ? "pipe" : "inherit" });
7791
+ } catch {
7792
+ try {
7793
+ execSync(`npm install -g @hasna/connectors@${latestVersion}`, { stdio: options.json ? "pipe" : "inherit" });
7794
+ } catch (e) {
7795
+ if (options.json) {
7796
+ console.log(JSON.stringify({ error: "Failed to upgrade. Try manually: bun install -g @hasna/connectors@latest" }));
7797
+ } else {
7798
+ console.log(chalk2.red(`
7799
+ Failed to upgrade. Try manually:`));
7800
+ console.log(chalk2.dim(` bun install -g @hasna/connectors@latest
7801
+ `));
7802
+ }
7803
+ process.exit(1);
7804
+ return;
7805
+ }
7806
+ }
7807
+ if (options.json) {
7808
+ console.log(JSON.stringify({ upgraded: true, from: currentVersion, to: latestVersion }));
7809
+ } else {
7810
+ console.log(chalk2.green(`
7811
+ Upgraded to ${latestVersion}!
7812
+ `));
7813
+ }
7814
+ } catch (e) {
7815
+ if (options.json) {
7816
+ console.log(JSON.stringify({ error: e instanceof Error ? e.message : "Failed to check for updates" }));
7817
+ } else {
7818
+ console.log(chalk2.red(`
7819
+ Failed to check for updates: ${e instanceof Error ? e.message : e}
7820
+ `));
7821
+ }
7822
+ process.exit(1);
7823
+ }
7824
+ });
7825
+ program2.command("completions").argument("<shell>", "Shell type: bash, zsh, or fish").description("Output shell completion script").action((shell) => {
7826
+ const commands = ["interactive", "install", "list", "search", "info", "docs", "remove", "categories", "serve", "update", "status", "doctor", "auth", "init", "export", "import", "upgrade", "completions"];
7827
+ const connectorNames = CONNECTORS.map((c) => c.name);
7828
+ const categoryNames = CATEGORIES.map((c) => `"${c}"`);
7829
+ if (shell === "zsh") {
7830
+ console.log(`#compdef connectors
7831
+ _connectors() {
7832
+ local -a commands connectors categories
7833
+ commands=(${commands.join(" ")})
7834
+ connectors=(${connectorNames.join(" ")})
7835
+ categories=(${categoryNames.map((c) => c.replace(/"/g, "\\\"")).join(" ")})
7836
+
7837
+ if (( CURRENT == 2 )); then
7838
+ _describe 'command' commands
7839
+ elif (( CURRENT == 3 )); then
7840
+ case "\${words[2]}" in
7841
+ install|add|info|docs|remove|rm|auth)
7842
+ _describe 'connector' connectors ;;
7843
+ search) _message 'search query' ;;
7844
+ list|ls) _arguments '--category[Filter by category]:category:(${CATEGORIES.join(" ").replace(/&/g, "\\&")})' '--installed' '--json' '--brief' ;;
7845
+ *) ;;
7846
+ esac
7847
+ fi
7848
+ }
7849
+ compdef _connectors connectors`);
7850
+ } else if (shell === "bash") {
7851
+ console.log(`_connectors() {
7852
+ local cur prev commands connectors
7853
+ COMPREPLY=()
7854
+ cur="\${COMP_WORDS[COMP_CWORD]}"
7855
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
7856
+ commands="${commands.join(" ")}"
7857
+ connectors="${connectorNames.join(" ")}"
7858
+
7859
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
7860
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
7861
+ elif [[ \${COMP_CWORD} -eq 2 ]]; then
7862
+ case "\${prev}" in
7863
+ install|add|info|docs|remove|rm|auth)
7864
+ COMPREPLY=( $(compgen -W "\${connectors}" -- "\${cur}") ) ;;
7865
+ esac
7866
+ fi
7867
+ }
7868
+ complete -F _connectors connectors`);
7869
+ } else if (shell === "fish") {
7870
+ let script = `# Fish completions for connectors
7871
+ `;
7872
+ for (const cmd of commands) {
7873
+ script += `complete -c connectors -n "__fish_use_subcommand" -a "${cmd}"
7874
+ `;
7875
+ }
7876
+ script += `# Connector names for install/info/docs/remove/auth
7877
+ `;
7878
+ for (const name of connectorNames) {
7879
+ script += `complete -c connectors -n "__fish_seen_subcommand_from install add info docs remove rm auth" -a "${name}"
7880
+ `;
7881
+ }
7882
+ console.log(script);
7883
+ } else {
7884
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
7885
+ process.exit(1);
7886
+ }
7887
+ });
7888
+ program2.command("env").option("-o, --output <file>", "Write to file instead of stdout").option("--json", "Output as JSON", false).description("Generate .env.example from installed connectors' required env vars").action((options) => {
7889
+ const installed = getInstalledConnectors();
7890
+ if (installed.length === 0) {
7891
+ if (options.json) {
7892
+ console.log(JSON.stringify({ vars: [], connectors: [] }));
7893
+ } else {
7894
+ console.log(chalk2.dim("No connectors installed. Run: connectors install <name>"));
7895
+ }
7896
+ return;
7897
+ }
7898
+ const vars = [];
7899
+ const seen = new Set;
7900
+ for (const name of installed) {
7901
+ const docs = getConnectorDocs(name);
7902
+ if (!docs?.envVars)
7903
+ continue;
7904
+ for (const v of docs.envVars) {
7905
+ if (!seen.has(v.variable)) {
7906
+ seen.add(v.variable);
7907
+ vars.push({ variable: v.variable, description: v.description, connector: name });
7908
+ }
7909
+ }
7910
+ }
7911
+ if (options.json) {
7912
+ console.log(JSON.stringify({ vars, connectors: installed }, null, 2));
7913
+ return;
7914
+ }
7915
+ const lines = [
7916
+ "# Environment Variables",
7917
+ `# Generated by connectors env (${installed.length} installed connectors)`,
7918
+ "#"
7919
+ ];
7920
+ let lastConnector = "";
7921
+ for (const v of vars) {
7922
+ if (v.connector !== lastConnector) {
7923
+ lines.push("");
7924
+ lines.push(`# ${v.connector}`);
7925
+ lastConnector = v.connector;
7926
+ }
7927
+ if (v.description)
7928
+ lines.push(`# ${v.description}`);
7929
+ lines.push(`${v.variable}=`);
7930
+ }
7931
+ const output = lines.join(`
7932
+ `) + `
7933
+ `;
7934
+ if (options.output) {
7935
+ writeFileSync4(options.output, output);
7936
+ console.log(chalk2.green(`\u2713 Written to ${options.output} (${vars.length} variables)`));
7937
+ } else {
7938
+ console.log(output);
7939
+ }
7940
+ });
7941
+ program2.command("presets").option("--json", "Output as JSON", false).description("List available connector preset bundles").action((options) => {
7942
+ if (options.json) {
7943
+ console.log(JSON.stringify(Object.entries(PRESETS).map(([name, p]) => ({
7944
+ name,
7945
+ description: p.description,
7946
+ connectors: p.connectors,
7947
+ count: p.connectors.length
7948
+ })), null, 2));
7949
+ return;
7950
+ }
7951
+ console.log(chalk2.bold(`
7952
+ Available presets:
7953
+ `));
7954
+ for (const [name, preset] of Object.entries(PRESETS)) {
7955
+ console.log(` ${chalk2.cyan(name.padEnd(12))} ${preset.description}`);
7956
+ console.log(chalk2.dim(` ${"".padEnd(12)} ${preset.connectors.join(", ")}`));
7957
+ console.log();
7958
+ }
7959
+ console.log(chalk2.dim(` Install with: connectors install --preset <name>
7960
+ `));
7961
+ });
7962
+ program2.command("whoami").option("--json", "Output as JSON", false).description("Show current setup: config dir, installed connectors, auth status").action((options) => {
7963
+ const configDir = join5(homedir3(), ".connectors");
7964
+ const installed = getInstalledConnectors();
7965
+ const version = "0.2.6";
7966
+ let configured = 0;
7967
+ let unconfigured = 0;
7968
+ const connectorDetails = [];
7969
+ for (const name of installed) {
7970
+ const auth = getAuthStatus(name);
7971
+ if (auth.configured)
7972
+ configured++;
7973
+ else
7974
+ unconfigured++;
7975
+ const connectorConfigDir = join5(configDir, name.startsWith("connect-") ? name : `connect-${name}`);
7976
+ const currentProfileFile = join5(connectorConfigDir, "current_profile");
7977
+ let profile = "default";
7978
+ if (existsSync5(currentProfileFile)) {
7979
+ try {
7980
+ profile = readFileSync5(currentProfileFile, "utf-8").trim() || "default";
7981
+ } catch {}
7982
+ }
7983
+ connectorDetails.push({ name, configured: auth.configured, authType: auth.type, profile });
7984
+ }
7985
+ if (options.json) {
7986
+ console.log(JSON.stringify({
7987
+ version,
7988
+ configDir,
7989
+ configDirExists: existsSync5(configDir),
7990
+ installed: installed.length,
7991
+ configured,
7992
+ unconfigured,
7993
+ connectors: connectorDetails
7994
+ }, null, 2));
7995
+ return;
7996
+ }
7997
+ console.log(chalk2.bold(`
7998
+ Connectors Setup
7999
+ `));
8000
+ console.log(` Version: ${chalk2.cyan(version)}`);
8001
+ console.log(` Config: ${configDir}${existsSync5(configDir) ? "" : chalk2.dim(" (not created yet)")}`);
8002
+ console.log(` Installed: ${installed.length} connector${installed.length !== 1 ? "s" : ""}`);
8003
+ console.log(` Configured: ${chalk2.green(String(configured))} ready, ${unconfigured > 0 ? chalk2.red(String(unconfigured)) : chalk2.dim("0")} need auth`);
8004
+ if (connectorDetails.length > 0) {
8005
+ console.log(chalk2.bold(`
8006
+ Connectors:
8007
+ `));
8008
+ const nameWidth = Math.max(10, ...connectorDetails.map((c) => c.name.length)) + 2;
8009
+ for (const c of connectorDetails) {
8010
+ const status = c.configured ? chalk2.green("\u2713") : chalk2.red("\u2717");
8011
+ const profileLabel = c.profile !== "default" ? chalk2.dim(` [${c.profile}]`) : "";
8012
+ console.log(` ${status} ${chalk2.cyan(c.name.padEnd(nameWidth))}${c.authType.padEnd(8)}${profileLabel}`);
8013
+ }
8014
+ }
8015
+ console.log();
8016
+ });
8017
+ program2.command("test").argument("[connector]", "Connector to test (default: all installed)").option("--json", "Output as JSON", false).option("--timeout <ms>", "Request timeout in milliseconds", "10000").description("Verify API credentials by making a real request to the connector's API").action(async (connector, options) => {
8018
+ const timeout = parseInt(options.timeout, 10) || 1e4;
8019
+ const installed = getInstalledConnectors();
8020
+ let toTest;
8021
+ if (connector) {
8022
+ if (!getConnector(connector)) {
8023
+ if (options.json) {
8024
+ console.log(JSON.stringify({ error: `Connector '${connector}' not found` }));
8025
+ } else {
8026
+ console.log(chalk2.red(`Connector '${connector}' not found`));
8027
+ }
8028
+ process.exit(1);
8029
+ return;
8030
+ }
8031
+ toTest = [connector];
8032
+ } else {
8033
+ if (installed.length === 0) {
8034
+ if (options.json) {
8035
+ console.log(JSON.stringify({ results: [], tested: 0 }));
8036
+ } else {
8037
+ console.log(chalk2.dim("No connectors installed. Run: connectors install <name>"));
8038
+ }
8039
+ return;
8040
+ }
8041
+ toTest = installed;
8042
+ }
8043
+ if (!options.json)
8044
+ console.log(chalk2.bold(`
8045
+ Testing connector credentials...
8046
+ `));
8047
+ const results = [];
8048
+ for (const name of toTest) {
8049
+ const auth = getAuthStatus(name);
8050
+ const endpoint = TEST_ENDPOINTS[name];
8051
+ if (!auth.configured) {
8052
+ results.push({ name, status: "no-key", message: "No credentials configured" });
8053
+ if (!options.json)
8054
+ console.log(` ${chalk2.dim("\u25CB")} ${chalk2.dim(name)} \u2014 ${chalk2.dim("no credentials configured")}`);
8055
+ continue;
8056
+ }
8057
+ if (!endpoint) {
8058
+ results.push({ name, status: "skip", message: "No test endpoint defined" });
8059
+ if (!options.json)
8060
+ console.log(` ${chalk2.dim("\u25CB")} ${chalk2.dim(name)} \u2014 ${chalk2.dim("no test endpoint (key exists)")}`);
8061
+ continue;
8062
+ }
8063
+ const docs = getConnectorDocs(name);
8064
+ const envVars = docs?.envVars || [];
8065
+ let apiKey;
8066
+ for (const v of envVars) {
8067
+ if (process.env[v.variable]) {
8068
+ apiKey = process.env[v.variable];
8069
+ break;
8070
+ }
8071
+ }
8072
+ if (!apiKey) {
8073
+ const connectorConfigDir = join5(homedir3(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
8074
+ const profileFile = join5(connectorConfigDir, "profiles", "default.json");
8075
+ if (existsSync5(profileFile)) {
8076
+ try {
8077
+ const config = JSON.parse(readFileSync5(profileFile, "utf-8"));
8078
+ apiKey = Object.values(config).find((v) => typeof v === "string" && v.length > 0);
8079
+ } catch {}
8080
+ }
8081
+ if (!apiKey) {
8082
+ const profileDirConfig = join5(connectorConfigDir, "profiles", "default", "config.json");
8083
+ if (existsSync5(profileDirConfig)) {
8084
+ try {
8085
+ const config = JSON.parse(readFileSync5(profileDirConfig, "utf-8"));
8086
+ apiKey = Object.values(config).find((v) => typeof v === "string" && v.length > 0);
8087
+ } catch {}
8088
+ }
8089
+ }
8090
+ }
8091
+ if (!apiKey) {
8092
+ results.push({ name, status: "no-key", message: "Credentials configured but could not extract key" });
8093
+ if (!options.json)
8094
+ console.log(` ${chalk2.yellow("\u26A0")} ${chalk2.yellow(name)} \u2014 ${chalk2.dim("could not extract key")}`);
8095
+ continue;
8096
+ }
8097
+ const start = Date.now();
8098
+ try {
8099
+ const res = await fetch(endpoint.url, {
8100
+ method: endpoint.method || "GET",
8101
+ headers: endpoint.headers(apiKey),
8102
+ body: endpoint.method === "POST" ? JSON.stringify({ query: "test", num_results: 1 }) : undefined,
8103
+ signal: AbortSignal.timeout(timeout)
8104
+ });
8105
+ const ms = Date.now() - start;
8106
+ const successCodes = endpoint.successCodes || [200];
8107
+ if (successCodes.includes(res.status) || res.status >= 200 && res.status < 300) {
8108
+ results.push({ name, status: "pass", message: `OK (${res.status})`, ms });
8109
+ if (!options.json)
8110
+ console.log(` ${chalk2.green("\u2713")} ${chalk2.green(name)} \u2014 ${chalk2.dim(`${res.status} OK`)} ${chalk2.dim(`(${ms}ms)`)}`);
8111
+ } else {
8112
+ const body = await res.text().catch(() => "");
8113
+ const msg = res.status === 401 ? "Invalid or expired credentials" : `HTTP ${res.status}`;
8114
+ results.push({ name, status: "fail", message: msg, ms });
8115
+ if (!options.json)
8116
+ console.log(` ${chalk2.red("\u2717")} ${chalk2.red(name)} \u2014 ${chalk2.red(msg)} ${chalk2.dim(`(${ms}ms)`)}`);
8117
+ }
8118
+ } catch (e) {
8119
+ const ms = Date.now() - start;
8120
+ const msg = e instanceof Error ? e.message : String(e);
8121
+ results.push({ name, status: "fail", message: msg, ms });
8122
+ if (!options.json)
8123
+ console.log(` ${chalk2.red("\u2717")} ${chalk2.red(name)} \u2014 ${chalk2.red(msg)}`);
8124
+ }
8125
+ }
8126
+ if (options.json) {
8127
+ console.log(JSON.stringify({ results, tested: results.length, passed: results.filter((r) => r.status === "pass").length }, null, 2));
8128
+ } else {
8129
+ const passed = results.filter((r) => r.status === "pass").length;
8130
+ const failed = results.filter((r) => r.status === "fail").length;
8131
+ const skipped = results.filter((r) => r.status === "skip" || r.status === "no-key").length;
8132
+ console.log();
8133
+ const parts = [];
8134
+ if (passed > 0)
8135
+ parts.push(chalk2.green(`${passed} passed`));
8136
+ if (failed > 0)
8137
+ parts.push(chalk2.red(`${failed} failed`));
8138
+ if (skipped > 0)
8139
+ parts.push(chalk2.dim(`${skipped} skipped`));
8140
+ console.log(` ${parts.join(", ")}
8141
+ `);
8142
+ }
8143
+ process.exit(results.some((r) => r.status === "fail") ? 1 : 0);
8144
+ });
7506
8145
  program2.parse();
package/bin/mcp.js CHANGED
@@ -20275,7 +20275,7 @@ function guessKeyField(name) {
20275
20275
  loadConnectorVersions();
20276
20276
  var server = new McpServer({
20277
20277
  name: "connectors",
20278
- version: "0.2.4"
20278
+ version: "0.2.6"
20279
20279
  });
20280
20280
  server.registerTool("search_connectors", {
20281
20281
  title: "Search Connectors",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Test endpoint definitions for verifying API credentials.
3
+ * Each entry maps a connector name to its health-check endpoint.
4
+ */
5
+ export interface TestEndpoint {
6
+ url: string;
7
+ method?: string;
8
+ headers: (key: string) => Record<string, string>;
9
+ /** Expected successful status codes */
10
+ successCodes?: number[];
11
+ }
12
+ export declare const TEST_ENDPOINTS: Record<string, TestEndpoint>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "build": "cd dashboard && bun run build && cd .. && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/serve.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --outDir ./dist",
28
28
  "build:dashboard": "cd dashboard && bun run build",
29
- "postinstall": "cd dashboard && bun install",
29
+ "postinstall": "[ \"$SKIP_DASHBOARD\" = \"1\" ] || [ -d dashboard/node_modules ] || (cd dashboard && bun install)",
30
30
  "dev": "bun run ./src/cli/index.tsx",
31
31
  "typecheck": "tsc --noEmit",
32
32
  "test": "bun test",