@hasna/connectors 1.3.20 → 1.3.22

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/index.js CHANGED
@@ -1909,7 +1909,7 @@ var package_default;
1909
1909
  var init_package = __esm(() => {
1910
1910
  package_default = {
1911
1911
  name: "@hasna/connectors",
1912
- version: "1.3.20",
1912
+ version: "1.3.21",
1913
1913
  description: "Open source connector library - Install API connectors with a single command",
1914
1914
  type: "module",
1915
1915
  bin: {
@@ -33029,8 +33029,29 @@ function redactSecrets(obj) {
33029
33029
  }
33030
33030
  return obj;
33031
33031
  }
33032
+ function getOAuthTokenState(name) {
33033
+ const tokens = loadTokens(name);
33034
+ if (!tokens?.accessToken && !tokens?.refreshToken) {
33035
+ return { hasTokens: false, expired: false };
33036
+ }
33037
+ if (!tokens.expiresAt) {
33038
+ return { hasTokens: true, expired: false };
33039
+ }
33040
+ const now3 = Date.now();
33041
+ const isExpired = now3 >= tokens.expiresAt - 60000;
33042
+ const expiresIn = isExpired ? undefined : (() => {
33043
+ const ms = tokens.expiresAt - now3;
33044
+ const mins = Math.floor(ms / 60000);
33045
+ if (mins < 1)
33046
+ return "less than a minute";
33047
+ if (mins < 60)
33048
+ return `${mins}m`;
33049
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
33050
+ })();
33051
+ return { hasTokens: true, expired: isExpired, expiresIn };
33052
+ }
33032
33053
  function registerCommands4(program2) {
33033
- program2.command("auth").argument("<connector>", "Connector name to configure auth for").option("-k, --key <value>", "API key or bearer token value (non-interactive)").option("-f, --field <field>", "Which field to set (for multi-field connectors)").option("--json", "Output as JSON", false).description("Configure authentication for a connector").action(async (connector, options) => {
33054
+ program2.command("auth").argument("<connector>", "Connector name to configure auth for").option("-k, --key <value>", "API key or bearer token value (non-interactive)").option("-f, --field <field>", "Which field to set (for multi-field connectors)").option("--json", "Output as JSON", false).option("--no-browser", "Print OAuth URL without opening a browser (agent-friendly)", false).option("--refresh", "Refresh expired OAuth tokens", false).option("--port <port>", "OAuth server port (default: 9876)", "9876").description("Configure authentication for a connector").action(async (connector, options) => {
33034
33055
  const meta = getConnector(connector);
33035
33056
  if (!meta) {
33036
33057
  if (options.json) {
@@ -33044,6 +33065,39 @@ function registerCommands4(program2) {
33044
33065
  }
33045
33066
  const authType = getAuthType(connector);
33046
33067
  const statusBefore = getAuthStatus(connector);
33068
+ if (options.refresh) {
33069
+ if (authType !== "oauth") {
33070
+ if (options.json) {
33071
+ console.log(JSON.stringify({ error: `${connector} does not use OAuth. Refresh is only available for OAuth connectors.` }));
33072
+ } else {
33073
+ console.log(chalk5.red(`${meta.displayName} does not use OAuth. Refresh is only available for OAuth connectors.`));
33074
+ }
33075
+ process.exit(1);
33076
+ return;
33077
+ }
33078
+ try {
33079
+ const tokens = await refreshOAuthToken(connector);
33080
+ if (options.json) {
33081
+ console.log(JSON.stringify({ success: true, connector, tokenType: tokens.tokenType, scope: tokens.scope, expiresAt: tokens.expiresAt }));
33082
+ } else {
33083
+ console.log(chalk5.green(`
33084
+ \u2713 Refreshed ${meta.displayName} tokens.`));
33085
+ console.log(chalk5.dim(` Expires: ${new Date(tokens.expiresAt).toLocaleString()}`));
33086
+ }
33087
+ process.exit(0);
33088
+ return;
33089
+ } catch (err) {
33090
+ if (options.json) {
33091
+ console.log(JSON.stringify({ error: `Refresh failed: ${err instanceof Error ? err.message : String(err)}` }));
33092
+ } else {
33093
+ console.log(chalk5.red(`
33094
+ \u2717 Refresh failed: ${err instanceof Error ? err.message : String(err)}`));
33095
+ console.log(chalk5.dim("You may need to re-authenticate."));
33096
+ }
33097
+ process.exit(1);
33098
+ return;
33099
+ }
33100
+ }
33047
33101
  if (!options.json) {
33048
33102
  const statusLabel = statusBefore.configured ? chalk5.green("configured") : chalk5.red("not configured");
33049
33103
  console.log(chalk5.bold(`
@@ -33051,6 +33105,14 @@ ${meta.displayName} \u2014 Auth Configuration
33051
33105
  `));
33052
33106
  console.log(` Auth type: ${authType === "oauth" ? "OAuth" : authType === "apikey" ? "API Key" : "Bearer Token"}`);
33053
33107
  console.log(` Status: ${statusLabel}`);
33108
+ if (authType === "oauth" && statusBefore.configured) {
33109
+ const tokenState = getOAuthTokenState(connector);
33110
+ if (tokenState.hasTokens && !tokenState.expired) {
33111
+ console.log(` Token: ${chalk5.green("valid")} (expires in ${tokenState.expiresIn})`);
33112
+ } else if (tokenState.hasTokens && tokenState.expired) {
33113
+ console.log(` Token: ${chalk5.yellow("expired")} (run 'connectors auth ${connector} --refresh' or re-authenticate)`);
33114
+ }
33115
+ }
33054
33116
  const envVars2 = getEnvVars(connector);
33055
33117
  if (envVars2.length > 0) {
33056
33118
  console.log(` Fields: ${envVars2.map((v) => v.variable).join(", ")}`);
@@ -33058,23 +33120,115 @@ ${meta.displayName} \u2014 Auth Configuration
33058
33120
  console.log();
33059
33121
  }
33060
33122
  if (authType === "oauth") {
33123
+ const tokenState = getOAuthTokenState(connector);
33124
+ if (tokenState.hasTokens && !tokenState.expired) {
33125
+ if (options.json) {
33126
+ console.log(JSON.stringify({ connector, authType: "oauth", status: "authenticated", message: `Tokens are valid and not expired.` }));
33127
+ } else {
33128
+ console.log(chalk5.green(`
33129
+ \u2713 ${meta.displayName} is already authenticated (tokens valid, expires in ${tokenState.expiresIn}).`));
33130
+ console.log(chalk5.dim(`Use --refresh to renew tokens, or re-run auth to re-authenticate.`));
33131
+ }
33132
+ process.exit(0);
33133
+ return;
33134
+ }
33135
+ if (tokenState.hasTokens && tokenState.expired) {
33136
+ if (!options.json) {
33137
+ console.log(chalk5.dim("Tokens expired \u2014 attempting auto-refresh..."));
33138
+ }
33139
+ try {
33140
+ const tokens = await refreshOAuthToken(connector);
33141
+ if (options.json) {
33142
+ console.log(JSON.stringify({ connector, authType: "oauth", status: "refreshed", expiresAt: tokens.expiresAt }));
33143
+ } else {
33144
+ console.log(chalk5.green(`
33145
+ \u2713 Refreshed ${meta.displayName} tokens.`));
33146
+ console.log(chalk5.dim(` Expires: ${new Date(tokens.expiresAt).toLocaleString()}`));
33147
+ }
33148
+ process.exit(0);
33149
+ return;
33150
+ } catch {
33151
+ if (!options.json) {
33152
+ console.log(chalk5.yellow("Auto-refresh failed. Proceeding with OAuth flow..."));
33153
+ console.log();
33154
+ }
33155
+ }
33156
+ }
33061
33157
  if (options.json) {
33158
+ const port2 = parseInt(options.port, 10) || 9876;
33159
+ const oauthUrl2 = `http://localhost:${port2}/oauth/${connector}/start`;
33062
33160
  console.log(JSON.stringify({
33063
33161
  connector,
33064
33162
  authType: "oauth",
33065
- message: "OAuth connectors require browser-based authentication. Use 'connectors serve' or pass --key to set tokens manually."
33163
+ message: "OAuth connectors require browser-based authentication. Use 'connectors serve' or pass --key to set tokens manually.",
33164
+ oauthUrl: oauthUrl2
33066
33165
  }));
33067
33166
  process.exit(0);
33068
33167
  return;
33069
33168
  }
33070
33169
  console.log(chalk5.yellow("OAuth connectors require browser-based authentication."));
33071
33170
  console.log();
33171
+ const port = parseInt(options.port, 10) || 9876;
33172
+ const oauthUrl = `http://localhost:${port}/oauth/${connector}/start`;
33072
33173
  try {
33073
- const port = 9876;
33174
+ if (!options.browser) {
33175
+ console.log(chalk5.dim(`Starting OAuth server on port ${port}...`));
33176
+ const { spawn: spawn4 } = await import("child_process");
33177
+ const scriptPath = process.argv[1];
33178
+ const serverProc = spawn4("node", [scriptPath, "serve", "--port", String(port)], {
33179
+ detached: true,
33180
+ stdio: "ignore"
33181
+ });
33182
+ serverProc.unref();
33183
+ await new Promise((resolve) => setTimeout(resolve, 2000));
33184
+ try {
33185
+ await fetch(`http://localhost:${port}/api/connectors`);
33186
+ } catch {
33187
+ console.log(chalk5.red(`OAuth server failed to start on port ${port}. Is the port already in use?`));
33188
+ console.log(chalk5.dim("Free the port and try again, or use 'connectors serve' for the full dashboard."));
33189
+ process.exit(1);
33190
+ return;
33191
+ }
33192
+ console.log(chalk5.bold(`Open this URL to authenticate:
33193
+ `));
33194
+ console.log(` ${chalk5.cyan(oauthUrl)}
33195
+ `);
33196
+ console.log(chalk5.dim("Waiting for authentication to complete..."));
33197
+ const connectorsHome = getConnectorsHome();
33198
+ const connectorDirName = connector.startsWith("connect-") ? connector : `connect-${connector}`;
33199
+ const tokensPath = join20(connectorsHome, connectorDirName, "profiles", "default", "tokens.json");
33200
+ let attempts = 0;
33201
+ const maxAttempts = 360;
33202
+ while (attempts < maxAttempts) {
33203
+ await new Promise((resolve) => setTimeout(resolve, 500));
33204
+ if (existsSync19(tokensPath)) {
33205
+ break;
33206
+ }
33207
+ attempts++;
33208
+ if (attempts % 6 === 0) {
33209
+ process.stdout.write(".");
33210
+ }
33211
+ }
33212
+ try {
33213
+ serverProc.kill("SIGTERM");
33214
+ } catch {}
33215
+ if (attempts >= maxAttempts) {
33216
+ console.log();
33217
+ console.log(chalk5.yellow("Timed out waiting for OAuth callback. The auth may still be in progress."));
33218
+ console.log(chalk5.dim(`Check ${tokensPath} for tokens.`));
33219
+ console.log(chalk5.dim("You can stop the server manually: lsof -ti :${port} | xargs kill"));
33220
+ process.exit(1);
33221
+ return;
33222
+ }
33223
+ console.log();
33224
+ console.log(chalk5.green(`
33225
+ \u2713 Connected! ${meta.displayName} is now authenticated.`));
33226
+ process.exit(0);
33227
+ return;
33228
+ }
33074
33229
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
33075
33230
  console.log(chalk5.dim(`Starting temporary server on port ${port}...`));
33076
33231
  await startServer2(port, { open: false, strict: true });
33077
- const oauthUrl = `http://localhost:${port}/oauth/${connector}/start`;
33078
33232
  console.log(chalk5.bold(`
33079
33233
  Open this URL to authenticate:
33080
33234
  `));
@@ -34271,7 +34425,7 @@ ${meta.displayName} operations:
34271
34425
  }
34272
34426
  process.exit(result.exitCode);
34273
34427
  });
34274
- program2.command("setup").argument("<name>", "Connector name to set up").option("-k, --key <value>", "API key or bearer token value").option("-f, --field <field>", "Which field to set (for multi-field connectors)").option("-o, --overwrite", "Overwrite existing installation", false).option("--json", "Output as JSON", false).description("Install, configure auth, and verify a connector in one step").action(async (name, options) => {
34428
+ program2.command("setup").argument("<name>", "Connector name to set up").option("-k, --key <value>", "API key or bearer token value").option("-f, --field <field>", "Which field to set (for multi-field connectors)").option("-o, --overwrite", "Overwrite existing installation", false).option("--json", "Output as JSON", false).option("--no-browser", "Print OAuth URL without opening a browser (agent-friendly)", false).description("Install, configure auth, and verify a connector in one step").action(async (name, options) => {
34275
34429
  const meta = getConnector(name);
34276
34430
  if (!meta) {
34277
34431
  if (options.json) {
@@ -34346,11 +34500,63 @@ Setting up ${meta.displayName}...
34346
34500
  return;
34347
34501
  }
34348
34502
  console.log(` ${chalk7.yellow("\u27F3")} OAuth authentication required \u2014 starting server...`);
34503
+ const port = 9876;
34504
+ const oauthUrl = `http://localhost:${port}/oauth/${name}/start`;
34505
+ if (!options.browser) {
34506
+ console.log(`
34507
+ ${chalk7.bold("Open this URL to authenticate:")}`);
34508
+ console.log(` ${chalk7.cyan(oauthUrl)}
34509
+ `);
34510
+ const { spawn: spawn4 } = await import("child_process");
34511
+ const { getConnectorsHome: getConnectorsHome2 } = await Promise.resolve().then(() => (init_database(), exports_database));
34512
+ const { existsSync: existsSync8 } = await import("fs");
34513
+ const { join: join8 } = await import("path");
34514
+ const scriptPath = process.argv[1];
34515
+ const serverProc = spawn4("node", [scriptPath, "serve", "--port", String(port)], {
34516
+ detached: true,
34517
+ stdio: "ignore"
34518
+ });
34519
+ serverProc.unref();
34520
+ await new Promise((resolve) => setTimeout(resolve, 2000));
34521
+ try {
34522
+ await fetch(`http://localhost:${port}/api/connectors`);
34523
+ } catch {
34524
+ console.log(` ${chalk7.red("\u2717")} OAuth server failed to start on port ${port}.`);
34525
+ process.exit(1);
34526
+ return;
34527
+ }
34528
+ console.log(chalk7.dim(" Waiting for authentication to complete..."));
34529
+ const connectorsHome = getConnectorsHome2();
34530
+ const connectorDirName = name.startsWith("connect-") ? name : `connect-${name}`;
34531
+ const tokensPath = join8(connectorsHome, connectorDirName, "profiles", "default", "tokens.json");
34532
+ let attempts = 0;
34533
+ const maxAttempts = 360;
34534
+ while (attempts < maxAttempts) {
34535
+ await new Promise((resolve) => setTimeout(resolve, 500));
34536
+ if (existsSync8(tokensPath))
34537
+ break;
34538
+ attempts++;
34539
+ if (attempts % 6 === 0)
34540
+ process.stdout.write(".");
34541
+ }
34542
+ try {
34543
+ serverProc.kill("SIGTERM");
34544
+ } catch {}
34545
+ if (attempts >= maxAttempts) {
34546
+ console.log();
34547
+ console.log(chalk7.yellow(" Timed out waiting for OAuth callback."));
34548
+ process.exit(1);
34549
+ return;
34550
+ }
34551
+ console.log();
34552
+ console.log(` ${chalk7.green("\u2713")} ${meta.displayName} is now authenticated.`);
34553
+ authConfigured = true;
34554
+ process.exit(0);
34555
+ return;
34556
+ }
34349
34557
  try {
34350
- const port = 9876;
34351
34558
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
34352
34559
  await startServer2(port, { open: false, strict: true });
34353
- const oauthUrl = `http://localhost:${port}/oauth/${name}/start`;
34354
34560
  console.log(`
34355
34561
  ${chalk7.bold("Open this URL to authenticate:")}`);
34356
34562
  console.log(` ${chalk7.cyan(oauthUrl)}
package/bin/mcp.js CHANGED
@@ -11443,7 +11443,7 @@ var package_default;
11443
11443
  var init_package = __esm(() => {
11444
11444
  package_default = {
11445
11445
  name: "@hasna/connectors",
11446
- version: "1.3.20",
11446
+ version: "1.3.21",
11447
11447
  description: "Open source connector library - Install API connectors with a single command",
11448
11448
  type: "module",
11449
11449
  bin: {
@@ -24166,16 +24166,16 @@ __export(exports_runner, {
24166
24166
  getConnectorCliPath: () => getConnectorCliPath,
24167
24167
  buildEnvWithCredentials: () => buildEnvWithCredentials
24168
24168
  });
24169
- import { existsSync as existsSync13, readdirSync as readdirSync8 } from "fs";
24170
- import { join as join14, dirname as dirname7 } from "path";
24169
+ import { existsSync as existsSync14, readdirSync as readdirSync8 } from "fs";
24170
+ import { join as join15, dirname as dirname7 } from "path";
24171
24171
  import { fileURLToPath as fileURLToPath5 } from "url";
24172
- import { spawn } from "child_process";
24172
+ import { spawn as spawn2 } from "child_process";
24173
24173
  function resolveConnectorsDir2() {
24174
- const fromBin = join14(__dirname5, "..", "connectors");
24175
- if (existsSync13(fromBin))
24174
+ const fromBin = join15(__dirname5, "..", "connectors");
24175
+ if (existsSync14(fromBin))
24176
24176
  return fromBin;
24177
- const fromSrc = join14(__dirname5, "..", "..", "connectors");
24178
- if (existsSync13(fromSrc))
24177
+ const fromSrc = join15(__dirname5, "..", "..", "connectors");
24178
+ if (existsSync14(fromSrc))
24179
24179
  return fromSrc;
24180
24180
  return fromBin;
24181
24181
  }
@@ -24222,9 +24222,9 @@ function buildEnvWithCredentials(connectorName, baseEnv) {
24222
24222
  }
24223
24223
  function getConnectorCliPath(name) {
24224
24224
  const safeName = name.replace(/[^a-z0-9-]/g, "");
24225
- const connectorDir = join14(CONNECTORS_DIR2, `connect-${safeName}`);
24226
- const cliPath = join14(connectorDir, "src", "cli", "index.ts");
24227
- if (existsSync13(cliPath))
24225
+ const connectorDir = join15(CONNECTORS_DIR2, `connect-${safeName}`);
24226
+ const cliPath = join15(connectorDir, "src", "cli", "index.ts");
24227
+ if (existsSync14(cliPath))
24228
24228
  return cliPath;
24229
24229
  return null;
24230
24230
  }
@@ -24263,7 +24263,7 @@ function runLegacyConnectorCommand(name, args, timeoutMs = 30000) {
24263
24263
  });
24264
24264
  }
24265
24265
  return new Promise((resolve) => {
24266
- const proc = spawn("bun", ["run", cliPath, ...args], {
24266
+ const proc = spawn2("bun", ["run", cliPath, ...args], {
24267
24267
  timeout: timeoutMs,
24268
24268
  env: buildEnvWithCredentials(name, process.env),
24269
24269
  stdio: ["pipe", "pipe", "pipe"]
@@ -39916,7 +39916,9 @@ async function withWriteLock(connector, fn) {
39916
39916
 
39917
39917
  // src/server/auth.ts
39918
39918
  init_database();
39919
+ var FETCH_TIMEOUT = 1e4;
39919
39920
  var oauthStateStore = new Map;
39921
+ var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
39920
39922
  var GOOGLE_SCOPES = {
39921
39923
  gmail: [
39922
39924
  "https://www.googleapis.com/auth/gmail.readonly",
@@ -40125,6 +40127,67 @@ function guessKeyField(name) {
40125
40127
  }
40126
40128
  return "apiKey";
40127
40129
  }
40130
+ function getOAuthConfig(name) {
40131
+ const configDir = getConnectorConfigDir(name);
40132
+ const credentialsFile = join13(configDir, "credentials.json");
40133
+ if (existsSync12(credentialsFile)) {
40134
+ try {
40135
+ const creds = JSON.parse(readFileSync6(credentialsFile, "utf-8"));
40136
+ return { clientId: creds.clientId, clientSecret: creds.clientSecret };
40137
+ } catch {}
40138
+ }
40139
+ const config2 = loadProfileConfig(name);
40140
+ return {
40141
+ clientId: config2.clientId,
40142
+ clientSecret: config2.clientSecret
40143
+ };
40144
+ }
40145
+ function saveOAuthTokens(name, tokens) {
40146
+ const configDir = getConnectorConfigDir(name);
40147
+ const profile = getCurrentProfile2(name);
40148
+ const profileDir = join13(configDir, "profiles", profile);
40149
+ mkdirSync9(profileDir, { recursive: true });
40150
+ const tokensFile = join13(profileDir, "tokens.json");
40151
+ writeFileSync5(tokensFile, JSON.stringify(tokens, null, 2), { mode: 384 });
40152
+ }
40153
+ async function refreshOAuthToken(name) {
40154
+ return withWriteLock(name, () => _refreshOAuthToken(name));
40155
+ }
40156
+ async function _refreshOAuthToken(name) {
40157
+ const oauthConfig = getOAuthConfig(name);
40158
+ const currentTokens = loadTokens(name);
40159
+ if (!oauthConfig.clientId || !oauthConfig.clientSecret) {
40160
+ throw new Error("OAuth credentials not configured for " + name);
40161
+ }
40162
+ if (!currentTokens?.refreshToken) {
40163
+ throw new Error("No refresh token available. Please re-authenticate.");
40164
+ }
40165
+ const response = await fetch(GOOGLE_TOKEN_URL, {
40166
+ method: "POST",
40167
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
40168
+ body: new URLSearchParams({
40169
+ client_id: oauthConfig.clientId,
40170
+ client_secret: oauthConfig.clientSecret,
40171
+ refresh_token: currentTokens.refreshToken,
40172
+ grant_type: "refresh_token"
40173
+ }),
40174
+ signal: AbortSignal.timeout(FETCH_TIMEOUT)
40175
+ });
40176
+ if (!response.ok) {
40177
+ const error2 = await response.json().catch(() => ({ error: response.statusText }));
40178
+ throw new Error(`Token refresh failed: ${error2.error_description || error2.error}`);
40179
+ }
40180
+ const data = await response.json();
40181
+ const tokens = {
40182
+ accessToken: data.access_token,
40183
+ refreshToken: currentTokens.refreshToken,
40184
+ expiresAt: Date.now() + data.expires_in * 1000,
40185
+ tokenType: data.token_type,
40186
+ scope: data.scope || currentTokens.scope
40187
+ };
40188
+ saveOAuthTokens(name, tokens);
40189
+ return tokens;
40190
+ }
40128
40191
 
40129
40192
  // src/mcp/tools/management.ts
40130
40193
  function registerManagementTools(server, stripped) {
@@ -40269,6 +40332,10 @@ function registerManagementTools(server, stripped) {
40269
40332
  }
40270
40333
  // src/mcp/tools/auth.ts
40271
40334
  init_zod();
40335
+ init_database();
40336
+ import { existsSync as existsSync13, readFileSync as readFileSync7 } from "fs";
40337
+ import { join as join14 } from "path";
40338
+ import { spawn } from "child_process";
40272
40339
  function registerAuthTools(server, stripped) {
40273
40340
  server.registerTool("connector_auth_status", {
40274
40341
  title: "Connector Auth Status",
@@ -40343,6 +40410,146 @@ function registerAuthTools(server, stripped) {
40343
40410
  };
40344
40411
  }
40345
40412
  });
40413
+ server.registerTool("connector_oauth", {
40414
+ title: "Connector OAuth Flow",
40415
+ description: "Start an OAuth authentication flow for a connector. Returns an authorization URL to open in a browser. For non-interactive/agent use, pass noBrowser: true to start a temporary server and wait for tokens automatically.",
40416
+ inputSchema: {
40417
+ name: exports_external.string(),
40418
+ noBrowser: exports_external.boolean().optional().describe("If true, starts a temporary OAuth server and waits for tokens. If false/omitted, returns the URL for browser-based auth."),
40419
+ port: exports_external.number().optional().describe("OAuth server port (default: 9876)"),
40420
+ refresh: exports_external.boolean().optional().describe("If true, attempt to refresh existing OAuth tokens instead of starting a new flow.")
40421
+ }
40422
+ }, async ({ name, noBrowser, port, refresh }) => {
40423
+ const meta = getConnector(name);
40424
+ if (!meta) {
40425
+ return {
40426
+ content: [{ type: "text", text: `Connector '${name}' not found. Use search_connectors or list_connectors to find available connectors.` }],
40427
+ isError: true
40428
+ };
40429
+ }
40430
+ const authType = getAuthType(name);
40431
+ if (authType !== "oauth") {
40432
+ return {
40433
+ content: [{ type: "text", text: `${meta.displayName} does not use OAuth. Use configure_auth instead to set an API key.` }],
40434
+ isError: true
40435
+ };
40436
+ }
40437
+ if (refresh) {
40438
+ try {
40439
+ const tokens2 = await refreshOAuthToken(name);
40440
+ return {
40441
+ content: [{
40442
+ type: "text",
40443
+ text: JSON.stringify({ success: true, connector: name, action: "refreshed", expiresAt: tokens2.expiresAt, scope: tokens2.scope }, null, 2)
40444
+ }]
40445
+ };
40446
+ } catch (err) {
40447
+ return {
40448
+ content: [{
40449
+ type: "text",
40450
+ text: `Refresh failed: ${err instanceof Error ? err.message : String(err)}. You may need to re-authenticate with a new OAuth flow.`
40451
+ }],
40452
+ isError: true
40453
+ };
40454
+ }
40455
+ }
40456
+ const tokens = loadTokens(name);
40457
+ if (tokens?.accessToken && tokens?.refreshToken) {
40458
+ if (tokens.expiresAt && Date.now() < tokens.expiresAt - 60000) {
40459
+ const mins = Math.floor((tokens.expiresAt - Date.now()) / 60000);
40460
+ const expiresIn = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h ${mins % 60}m`;
40461
+ return {
40462
+ content: [{
40463
+ type: "text",
40464
+ text: JSON.stringify({ connector: name, status: "already_authenticated", expiresIn, message: "Tokens are valid. Use refresh: true to renew or re-authenticate to get new tokens." }, null, 2)
40465
+ }]
40466
+ };
40467
+ }
40468
+ }
40469
+ const serverPort = port || 9876;
40470
+ const oauthUrl = `http://localhost:${serverPort}/oauth/${name}/start`;
40471
+ if (noBrowser) {
40472
+ const connectorsHome = getConnectorsHome();
40473
+ const connectorDirName = name.startsWith("connect-") ? name : `connect-${name}`;
40474
+ const tokensPath = join14(connectorsHome, connectorDirName, "profiles", "default", "tokens.json");
40475
+ let serverRunning = false;
40476
+ try {
40477
+ await fetch(`http://localhost:${serverPort}/api/connectors`);
40478
+ serverRunning = true;
40479
+ } catch {}
40480
+ if (!serverRunning) {
40481
+ const scriptPath = process.argv[1];
40482
+ const serverProc = spawn("node", [scriptPath, "serve", "--port", String(serverPort)], {
40483
+ detached: true,
40484
+ stdio: "ignore"
40485
+ });
40486
+ serverProc.unref();
40487
+ await new Promise((resolve) => setTimeout(resolve, 2000));
40488
+ }
40489
+ let attempts = 0;
40490
+ const maxAttempts = 120;
40491
+ while (attempts < maxAttempts) {
40492
+ await new Promise((resolve) => setTimeout(resolve, 500));
40493
+ if (existsSync13(tokensPath)) {
40494
+ break;
40495
+ }
40496
+ attempts++;
40497
+ }
40498
+ if (attempts >= maxAttempts) {
40499
+ return {
40500
+ content: [{
40501
+ type: "text",
40502
+ text: JSON.stringify({
40503
+ connector: name,
40504
+ status: "waiting",
40505
+ oauthUrl,
40506
+ message: `OAuth server is running on port ${serverPort}. Open the URL to authenticate. Tokens will be saved to ${tokensPath}.`
40507
+ }, null, 2)
40508
+ }]
40509
+ };
40510
+ }
40511
+ try {
40512
+ const tokenData = JSON.parse(readFileSync7(tokensPath, "utf-8"));
40513
+ return {
40514
+ content: [{
40515
+ type: "text",
40516
+ text: JSON.stringify({
40517
+ success: true,
40518
+ connector: name,
40519
+ status: "authenticated",
40520
+ tokenType: tokenData.tokenType || "Bearer",
40521
+ scope: tokenData.scope
40522
+ }, null, 2)
40523
+ }]
40524
+ };
40525
+ } catch {
40526
+ return {
40527
+ content: [{
40528
+ type: "text",
40529
+ text: JSON.stringify({
40530
+ connector: name,
40531
+ status: "tokens_file_found",
40532
+ message: "Token file was created but could not be parsed."
40533
+ }, null, 2)
40534
+ }],
40535
+ isError: true
40536
+ };
40537
+ }
40538
+ } else {
40539
+ return {
40540
+ content: [{
40541
+ type: "text",
40542
+ text: JSON.stringify({
40543
+ connector: name,
40544
+ status: "auth_required",
40545
+ authType: "oauth",
40546
+ oauthUrl,
40547
+ message: `Open this URL in a browser to authenticate. After completing the OAuth flow, tokens will be saved automatically.`
40548
+ }, null, 2)
40549
+ }]
40550
+ };
40551
+ }
40552
+ });
40346
40553
  }
40347
40554
  // src/mcp/tools/operations.ts
40348
40555
  init_zod();
@@ -40621,7 +40828,7 @@ function listWorkflows(db) {
40621
40828
  }
40622
40829
 
40623
40830
  // src/lib/workflow-runner.ts
40624
- import { spawn as spawn2 } from "child_process";
40831
+ import { spawn as spawn3 } from "child_process";
40625
40832
  async function runStep(step, previousOutput) {
40626
40833
  return new Promise((resolve) => {
40627
40834
  const args = [...step.args ?? []];
@@ -40629,7 +40836,7 @@ async function runStep(step, previousOutput) {
40629
40836
  args.push("--input", previousOutput.trim().slice(0, 4096));
40630
40837
  }
40631
40838
  const cmdArgs = ["run", step.connector, step.command, ...args, "--format", "json"];
40632
- const proc = spawn2("connectors", cmdArgs, { shell: false });
40839
+ const proc = spawn3("connectors", cmdArgs, { shell: false });
40633
40840
  let output = "";
40634
40841
  proc.stdout.on("data", (d) => {
40635
40842
  output += d.toString();
@@ -40671,11 +40878,11 @@ async function runWorkflow(workflow) {
40671
40878
  }
40672
40879
 
40673
40880
  // src/lib/scheduler.ts
40674
- import { spawn as spawn3 } from "child_process";
40881
+ import { spawn as spawn4 } from "child_process";
40675
40882
  async function runConnectorCommand2(connector, command, args) {
40676
40883
  return new Promise((resolve) => {
40677
40884
  const cmdArgs = [connector, command, ...args, "--format", "json"];
40678
- const proc = spawn3("connectors", ["run", ...cmdArgs], { shell: false });
40885
+ const proc = spawn4("connectors", ["run", ...cmdArgs], { shell: false });
40679
40886
  let output = "";
40680
40887
  proc.stdout.on("data", (d) => {
40681
40888
  output += d.toString();
package/bin/serve.js CHANGED
@@ -15160,7 +15160,7 @@ var githubConnector = defineConnector({
15160
15160
  // package.json
15161
15161
  var package_default = {
15162
15162
  name: "@hasna/connectors",
15163
- version: "1.3.20",
15163
+ version: "1.3.21",
15164
15164
  description: "Open source connector library - Install API connectors with a single command",
15165
15165
  type: "module",
15166
15166
  bin: {
package/dist/index.js CHANGED
@@ -4969,7 +4969,7 @@ var githubConnector = defineConnector({
4969
4969
  // package.json
4970
4970
  var package_default = {
4971
4971
  name: "@hasna/connectors",
4972
- version: "1.3.20",
4972
+ version: "1.3.21",
4973
4973
  description: "Open source connector library - Install API connectors with a single command",
4974
4974
  type: "module",
4975
4975
  bin: {
@@ -28,6 +28,10 @@ export interface OAuthTokens {
28
28
  * Get the auth type for a connector by parsing its CLAUDE.md
29
29
  */
30
30
  export declare function getAuthType(name: string): AuthType;
31
+ /**
32
+ * Load OAuth tokens for a connector
33
+ */
34
+ export declare function loadTokens(name: string): OAuthTokens | null;
31
35
  /**
32
36
  * Get the full auth status for a connector
33
37
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {