@hasna/connectors 1.1.1 → 1.1.2

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
@@ -9465,6 +9465,7 @@ var require_cli_spinners = __commonJS((exports, module) => {
9465
9465
 
9466
9466
  // src/lib/installer.ts
9467
9467
  import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
9468
+ import { homedir } from "os";
9468
9469
  import { join as join2, dirname as dirname2 } from "path";
9469
9470
  import { fileURLToPath as fileURLToPath2 } from "url";
9470
9471
  function resolveConnectorsDir() {
@@ -9516,6 +9517,20 @@ function installConnector(name, options = {}) {
9516
9517
  mkdirSync(destDir, { recursive: true });
9517
9518
  }
9518
9519
  cpSync(sourcePath, destPath, { recursive: true });
9520
+ const homeCredDir = join2(homedir(), ".connectors", connectorName);
9521
+ if (existsSync2(homeCredDir)) {
9522
+ const filesToCopy = ["credentials.json", "current_profile"];
9523
+ for (const file of filesToCopy) {
9524
+ const src = join2(homeCredDir, file);
9525
+ if (existsSync2(src)) {
9526
+ cpSync(src, join2(destPath, file));
9527
+ }
9528
+ }
9529
+ const profilesDir = join2(homeCredDir, "profiles");
9530
+ if (existsSync2(profilesDir)) {
9531
+ cpSync(profilesDir, join2(destPath, "profiles"), { recursive: true });
9532
+ }
9533
+ }
9519
9534
  updateConnectorsIndex(destDir);
9520
9535
  return {
9521
9536
  connector: name,
@@ -9619,7 +9634,7 @@ var init_installer = __esm(() => {
9619
9634
  // src/server/auth.ts
9620
9635
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
9621
9636
  import { randomBytes } from "crypto";
9622
- import { homedir } from "os";
9637
+ import { homedir as homedir2 } from "os";
9623
9638
  import { join as join3 } from "path";
9624
9639
  function getAuthType(name) {
9625
9640
  const docs = getConnectorDocs(name);
@@ -9634,7 +9649,7 @@ function getAuthType(name) {
9634
9649
  }
9635
9650
  function getConnectorConfigDir(name) {
9636
9651
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
9637
- return join3(homedir(), ".connectors", connectorName);
9652
+ return join3(homedir2(), ".connectors", connectorName);
9638
9653
  }
9639
9654
  function getCurrentProfile(name) {
9640
9655
  const configDir = getConnectorConfigDir(name);
@@ -9994,7 +10009,7 @@ __export(exports_serve, {
9994
10009
  import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
9995
10010
  import { join as join5, dirname as dirname4, extname, basename } from "path";
9996
10011
  import { fileURLToPath as fileURLToPath4 } from "url";
9997
- import { homedir as homedir2 } from "os";
10012
+ import { homedir as homedir3 } from "os";
9998
10013
  function logActivity(action, connector, detail) {
9999
10014
  activityLog.unshift({ action, connector, timestamp: Date.now(), detail });
10000
10015
  if (activityLog.length > MAX_ACTIVITY_LOG) {
@@ -10271,7 +10286,7 @@ Dashboard not found at: ${dashboardDir}`);
10271
10286
  return json({ error: "Invalid connector name" }, 400, port);
10272
10287
  try {
10273
10288
  const profiles = listProfiles(name);
10274
- const configDir = join5(homedir2(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
10289
+ const configDir = join5(homedir3(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
10275
10290
  const currentProfileFile = join5(configDir, "current_profile");
10276
10291
  let current = "default";
10277
10292
  if (existsSync5(currentProfileFile)) {
@@ -10323,7 +10338,7 @@ Dashboard not found at: ${dashboardDir}`);
10323
10338
  }
10324
10339
  if (path === "/api/export" && method === "GET") {
10325
10340
  try {
10326
- const connectDir = join5(homedir2(), ".connectors");
10341
+ const connectDir = join5(homedir3(), ".connectors");
10327
10342
  const result = {};
10328
10343
  if (existsSync5(connectDir)) {
10329
10344
  const entries = readdirSync3(connectDir, { withFileTypes: true });
@@ -10383,7 +10398,7 @@ Dashboard not found at: ${dashboardDir}`);
10383
10398
  return json({ error: "Invalid import format: missing 'connectors' object" }, 400, port);
10384
10399
  }
10385
10400
  let imported = 0;
10386
- const connectDir = join5(homedir2(), ".connectors");
10401
+ const connectDir = join5(homedir3(), ".connectors");
10387
10402
  for (const [connectorName, data] of Object.entries(body.connectors)) {
10388
10403
  if (!isValidConnectorName(connectorName))
10389
10404
  continue;
@@ -12000,7 +12015,7 @@ init_registry();
12000
12015
  init_installer();
12001
12016
  init_auth();
12002
12017
  import { readdirSync as readdirSync4, existsSync as existsSync6, statSync as statSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
12003
- import { homedir as homedir3 } from "os";
12018
+ import { homedir as homedir4 } from "os";
12004
12019
  import { join as join6, relative } from "path";
12005
12020
 
12006
12021
  // src/lib/test-endpoints.ts
@@ -12346,7 +12361,7 @@ var PRESETS = {
12346
12361
  commerce: { description: "Commerce and finance", connectors: ["stripe", "shopify", "revolut", "mercury", "pandadoc"] }
12347
12362
  };
12348
12363
  var program2 = new Command;
12349
- program2.name("connectors").description("Install API connectors for your project").version("1.1.1").enablePositionalOptions();
12364
+ program2.name("connectors").description("Install API connectors for your project").version("1.1.2").enablePositionalOptions();
12350
12365
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
12351
12366
  if (!isTTY) {
12352
12367
  console.log(`Non-interactive environment detected. Use a subcommand:
@@ -12938,7 +12953,7 @@ Updating ${toUpdate.length} connector(s)...
12938
12953
  });
12939
12954
  program2.command("status").option("--json", "Output as JSON", false).description("Show auth status of all configured connectors (project + global)").action((options) => {
12940
12955
  const installed = getInstalledConnectors();
12941
- const configDir = join6(homedir3(), ".connectors");
12956
+ const configDir = join6(homedir4(), ".connectors");
12942
12957
  const seen = new Set;
12943
12958
  const allStatuses = [];
12944
12959
  function buildStatusEntry(name, source) {
@@ -13355,7 +13370,7 @@ program2.command("init").option("--json", "Output presets and suggestions as JSO
13355
13370
  { key: "commerce", emoji: "\uD83D\uDCB3", label: "Commerce", connectors: ["stripe", "shopify", "paypal", "revolut", "mercury"], description: "Commerce and finance" },
13356
13371
  { key: "google", emoji: "\uD83D\uDCC1", label: "Google Workspace", connectors: ["gmail", "googledrive", "googlecalendar", "googledocs", "googlesheets"], description: "Google Workspace suite" }
13357
13372
  ];
13358
- const connectorsHome = join6(homedir3(), ".connectors");
13373
+ const connectorsHome = join6(homedir4(), ".connectors");
13359
13374
  let configuredCount = 0;
13360
13375
  const configuredNames = [];
13361
13376
  try {
@@ -13461,7 +13476,7 @@ function redactSecrets(obj) {
13461
13476
  return obj;
13462
13477
  }
13463
13478
  program2.command("export").option("-o, --output <file>", "Write to file instead of stdout").option("--include-secrets", "Include secrets in plaintext (dangerous \u2014 use only for backup/restore)").description("Export all connector credentials as JSON backup").action((options) => {
13464
- const connectDir = join6(homedir3(), ".connectors");
13479
+ const connectDir = join6(homedir4(), ".connectors");
13465
13480
  const result = {};
13466
13481
  if (existsSync6(connectDir)) {
13467
13482
  for (const entry of readdirSync4(connectDir)) {
@@ -13565,7 +13580,7 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
13565
13580
  process.exit(1);
13566
13581
  return;
13567
13582
  }
13568
- const connectDir = join6(homedir3(), ".connectors");
13583
+ const connectDir = join6(homedir4(), ".connectors");
13569
13584
  let imported = 0;
13570
13585
  for (const [connectorName, connData] of Object.entries(data.connectors)) {
13571
13586
  if (!/^[a-z0-9-]+$/.test(connectorName))
@@ -13594,8 +13609,8 @@ program2.command("import").argument("<file>", "JSON backup file to import (use -
13594
13609
  }
13595
13610
  });
13596
13611
  program2.command("auth-import").option("--json", "Output as JSON", false).option("-d, --dry-run", "Preview what would be imported without copying", false).option("--force", "Overwrite existing files in ~/.connectors/", false).description("Migrate auth tokens from ~/.connect/ to ~/.connectors/").action((options) => {
13597
- const oldBase = join6(homedir3(), ".connect");
13598
- const newBase = join6(homedir3(), ".connectors");
13612
+ const oldBase = join6(homedir4(), ".connect");
13613
+ const newBase = join6(homedir4(), ".connectors");
13599
13614
  if (!existsSync6(oldBase)) {
13600
13615
  if (options.json) {
13601
13616
  console.log(JSON.stringify({ imported: [], skipped: [], error: null, message: "No ~/.connect/ directory found" }));
@@ -13919,7 +13934,7 @@ Available presets:
13919
13934
  `));
13920
13935
  });
13921
13936
  program2.command("whoami").option("--json", "Output as JSON", false).description("Show current setup: config dir, installed connectors, auth status").action((options) => {
13922
- const configDir = join6(homedir3(), ".connectors");
13937
+ const configDir = join6(homedir4(), ".connectors");
13923
13938
  const installed = getInstalledConnectors();
13924
13939
  const version = "0.3.1";
13925
13940
  let configured = 0;
@@ -14080,7 +14095,7 @@ Testing connector credentials...
14080
14095
  }
14081
14096
  }
14082
14097
  if (!apiKey) {
14083
- const connectorConfigDir = join6(homedir3(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
14098
+ const connectorConfigDir = join6(homedir4(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
14084
14099
  let currentProfile = "default";
14085
14100
  const currentProfileFile = join6(connectorConfigDir, "current_profile");
14086
14101
  if (existsSync6(currentProfileFile)) {
package/bin/mcp.js CHANGED
@@ -25345,6 +25345,7 @@ function loadConnectorVersions() {
25345
25345
 
25346
25346
  // src/lib/installer.ts
25347
25347
  import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
25348
+ import { homedir } from "os";
25348
25349
  import { join as join2, dirname as dirname2 } from "path";
25349
25350
  import { fileURLToPath as fileURLToPath2 } from "url";
25350
25351
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
@@ -25395,6 +25396,20 @@ function installConnector(name, options = {}) {
25395
25396
  mkdirSync(destDir, { recursive: true });
25396
25397
  }
25397
25398
  cpSync(sourcePath, destPath, { recursive: true });
25399
+ const homeCredDir = join2(homedir(), ".connectors", connectorName);
25400
+ if (existsSync2(homeCredDir)) {
25401
+ const filesToCopy = ["credentials.json", "current_profile"];
25402
+ for (const file of filesToCopy) {
25403
+ const src = join2(homeCredDir, file);
25404
+ if (existsSync2(src)) {
25405
+ cpSync(src, join2(destPath, file));
25406
+ }
25407
+ }
25408
+ const profilesDir = join2(homeCredDir, "profiles");
25409
+ if (existsSync2(profilesDir)) {
25410
+ cpSync(profilesDir, join2(destPath, "profiles"), { recursive: true });
25411
+ }
25412
+ }
25398
25413
  updateConnectorsIndex(destDir);
25399
25414
  return {
25400
25415
  connector: name,
@@ -25492,7 +25507,7 @@ function removeConnector(name, targetDir = process.cwd()) {
25492
25507
 
25493
25508
  // src/server/auth.ts
25494
25509
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
25495
- import { homedir } from "os";
25510
+ import { homedir as homedir2 } from "os";
25496
25511
  import { join as join3 } from "path";
25497
25512
  var oauthStateStore = new Map;
25498
25513
  var GOOGLE_SCOPES = {
@@ -25546,7 +25561,7 @@ function getAuthType(name) {
25546
25561
  }
25547
25562
  function getConnectorConfigDir(name) {
25548
25563
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
25549
- return join3(homedir(), ".connectors", connectorName);
25564
+ return join3(homedir2(), ".connectors", connectorName);
25550
25565
  }
25551
25566
  function getCurrentProfile(name) {
25552
25567
  const configDir = getConnectorConfigDir(name);
@@ -25814,7 +25829,7 @@ async function getConnectorCommandHelp(name, command) {
25814
25829
  loadConnectorVersions();
25815
25830
  var server = new McpServer({
25816
25831
  name: "connectors",
25817
- version: "1.1.1"
25832
+ version: "1.1.2"
25818
25833
  });
25819
25834
  server.registerTool("search_connectors", {
25820
25835
  title: "Search Connectors",
package/bin/serve.js CHANGED
@@ -6,7 +6,7 @@ var __require = import.meta.require;
6
6
  import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
7
7
  import { join as join4, dirname as dirname3, extname, basename } from "path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "url";
9
- import { homedir as homedir2 } from "os";
9
+ import { homedir as homedir3 } from "os";
10
10
 
11
11
  // src/lib/registry.ts
12
12
  import { existsSync, readFileSync } from "fs";
@@ -5937,6 +5937,7 @@ function loadConnectorVersions() {
5937
5937
 
5938
5938
  // src/lib/installer.ts
5939
5939
  import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
5940
+ import { homedir } from "os";
5940
5941
  import { join as join2, dirname as dirname2 } from "path";
5941
5942
  import { fileURLToPath as fileURLToPath2 } from "url";
5942
5943
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
@@ -5987,6 +5988,20 @@ function installConnector(name, options = {}) {
5987
5988
  mkdirSync(destDir, { recursive: true });
5988
5989
  }
5989
5990
  cpSync(sourcePath, destPath, { recursive: true });
5991
+ const homeCredDir = join2(homedir(), ".connectors", connectorName);
5992
+ if (existsSync2(homeCredDir)) {
5993
+ const filesToCopy = ["credentials.json", "current_profile"];
5994
+ for (const file of filesToCopy) {
5995
+ const src = join2(homeCredDir, file);
5996
+ if (existsSync2(src)) {
5997
+ cpSync(src, join2(destPath, file));
5998
+ }
5999
+ }
6000
+ const profilesDir = join2(homeCredDir, "profiles");
6001
+ if (existsSync2(profilesDir)) {
6002
+ cpSync(profilesDir, join2(destPath, "profiles"), { recursive: true });
6003
+ }
6004
+ }
5990
6005
  updateConnectorsIndex(destDir);
5991
6006
  return {
5992
6007
  connector: name,
@@ -6085,7 +6100,7 @@ function removeConnector(name, targetDir = process.cwd()) {
6085
6100
  // src/server/auth.ts
6086
6101
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
6087
6102
  import { randomBytes } from "crypto";
6088
- import { homedir } from "os";
6103
+ import { homedir as homedir2 } from "os";
6089
6104
  import { join as join3 } from "path";
6090
6105
  var FETCH_TIMEOUT = 1e4;
6091
6106
  var oauthStateStore = new Map;
@@ -6142,7 +6157,7 @@ function getAuthType(name) {
6142
6157
  }
6143
6158
  function getConnectorConfigDir(name) {
6144
6159
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
6145
- return join3(homedir(), ".connectors", connectorName);
6160
+ return join3(homedir2(), ".connectors", connectorName);
6146
6161
  }
6147
6162
  function getCurrentProfile(name) {
6148
6163
  const configDir = getConnectorConfigDir(name);
@@ -6743,7 +6758,7 @@ Dashboard not found at: ${dashboardDir}`);
6743
6758
  return json({ error: "Invalid connector name" }, 400, port);
6744
6759
  try {
6745
6760
  const profiles = listProfiles(name);
6746
- const configDir = join4(homedir2(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
6761
+ const configDir = join4(homedir3(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
6747
6762
  const currentProfileFile = join4(configDir, "current_profile");
6748
6763
  let current = "default";
6749
6764
  if (existsSync4(currentProfileFile)) {
@@ -6795,7 +6810,7 @@ Dashboard not found at: ${dashboardDir}`);
6795
6810
  }
6796
6811
  if (path === "/api/export" && method === "GET") {
6797
6812
  try {
6798
- const connectDir = join4(homedir2(), ".connectors");
6813
+ const connectDir = join4(homedir3(), ".connectors");
6799
6814
  const result = {};
6800
6815
  if (existsSync4(connectDir)) {
6801
6816
  const entries = readdirSync3(connectDir, { withFileTypes: true });
@@ -6855,7 +6870,7 @@ Dashboard not found at: ${dashboardDir}`);
6855
6870
  return json({ error: "Invalid import format: missing 'connectors' object" }, 400, port);
6856
6871
  }
6857
6872
  let imported = 0;
6858
- const connectDir = join4(homedir2(), ".connectors");
6873
+ const connectDir = join4(homedir3(), ".connectors");
6859
6874
  for (const [connectorName, data] of Object.entries(body.connectors)) {
6860
6875
  if (!isValidConnectorName(connectorName))
6861
6876
  continue;
@@ -46,6 +46,8 @@ import {
46
46
  } from '../utils/settings';
47
47
  import type { OutputFormat } from '../utils/output';
48
48
  import { success, error, info, print, warn } from '../utils/output';
49
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
50
+ import { join, resolve } from 'path';
49
51
 
50
52
  const program = new Command();
51
53
 
@@ -812,6 +814,20 @@ labelsCmd
812
814
  }
813
815
  });
814
816
 
817
+ labelsCmd
818
+ .command('rename <labelId> <newName>')
819
+ .description('Rename a label')
820
+ .action(async (labelId: string, newName: string) => {
821
+ try {
822
+ const gmail = requireAuth();
823
+ const label = await gmail.labels.update(labelId, { name: newName });
824
+ success(`Renamed to: ${label.name}`);
825
+ } catch (err) {
826
+ error(String(err));
827
+ process.exit(1);
828
+ }
829
+ });
830
+
815
831
  labelsCmd
816
832
  .command('delete <labelId>')
817
833
  .description('Delete a label')
@@ -1220,6 +1236,129 @@ attachmentsCmd
1220
1236
  info(`Attachments path: ${path}`);
1221
1237
  });
1222
1238
 
1239
+
1240
+ attachmentsCmd
1241
+ .command('bulk-download <query>')
1242
+ .description('Bulk-download attachments from messages matching a Gmail search query')
1243
+ .option('-o, --output <dir>', 'Output directory', './gmail-attachments')
1244
+ .option('--organize-by-date', 'Organize files into subdirs by email date (YYYY/MM/)')
1245
+ .option('-n, --max <number>', 'Maximum messages to process', '100')
1246
+ .action(async (query: string, opts) => {
1247
+ try {
1248
+ const gmail = requireAuth();
1249
+ const outputDir = resolve(opts.output);
1250
+ const organizeByDate = !!opts.organizeByDate;
1251
+ const maxMessages = parseInt(opts.max);
1252
+
1253
+ info(`Searching for messages matching: ${chalk.cyan(query)}`);
1254
+ info(`Output directory: ${chalk.cyan(outputDir)}`);
1255
+ if (organizeByDate) info('Organizing by date: enabled');
1256
+
1257
+ // Paginate through matching messages
1258
+ const allMessageIds: string[] = [];
1259
+ let pageToken: string | undefined;
1260
+
1261
+ do {
1262
+ const result = await gmail.messages.list({
1263
+ q: query,
1264
+ maxResults: Math.min(maxMessages - allMessageIds.length, 500),
1265
+ pageToken,
1266
+ });
1267
+
1268
+ if (result.messages) {
1269
+ for (const m of result.messages) {
1270
+ if (m.id) allMessageIds.push(m.id);
1271
+ }
1272
+ }
1273
+
1274
+ pageToken = result.nextPageToken;
1275
+ } while (pageToken && allMessageIds.length < maxMessages);
1276
+
1277
+ if (allMessageIds.length === 0) {
1278
+ info('No messages found matching the query.');
1279
+ return;
1280
+ }
1281
+
1282
+ info(`Found ${allMessageIds.length} message(s). Scanning for attachments...`);
1283
+
1284
+ let totalDownloaded = 0;
1285
+ let totalSkipped = 0;
1286
+ let totalErrors = 0;
1287
+
1288
+ for (let i = 0; i < allMessageIds.length; i++) {
1289
+ const messageId = allMessageIds[i];
1290
+ const progress = `[${i + 1}/${allMessageIds.length}]`;
1291
+
1292
+ try {
1293
+ const attachments = await gmail.attachments.list(messageId);
1294
+
1295
+ if (attachments.length === 0) continue;
1296
+
1297
+ // Determine destination directory
1298
+ let destDir = outputDir;
1299
+ if (organizeByDate) {
1300
+ // Fetch minimal message metadata to get internalDate
1301
+ const fullMsg = await (gmail as any).client.get(
1302
+ `/users/me/messages/${messageId}`,
1303
+ { format: 'minimal' }
1304
+ );
1305
+ const msgDate = fullMsg?.internalDate
1306
+ ? new Date(parseInt(fullMsg.internalDate))
1307
+ : new Date();
1308
+ const year = msgDate.getFullYear().toString();
1309
+ const month = (msgDate.getMonth() + 1).toString().padStart(2, '0');
1310
+ destDir = join(outputDir, year, month);
1311
+ }
1312
+
1313
+ if (!existsSync(destDir)) {
1314
+ mkdirSync(destDir, { recursive: true });
1315
+ }
1316
+
1317
+ for (const attachment of attachments) {
1318
+ try {
1319
+ const data = await gmail.attachments.get(messageId, attachment.attachmentId);
1320
+ const cleanFilename = attachment.filename.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, ' ');
1321
+
1322
+ // Avoid overwriting existing files
1323
+ let finalFilename = cleanFilename;
1324
+ let destPath = join(destDir, finalFilename);
1325
+ if (existsSync(destPath)) {
1326
+ const dotIdx = cleanFilename.lastIndexOf('.');
1327
+ const suffix = '_' + messageId.slice(-6);
1328
+ finalFilename = dotIdx !== -1
1329
+ ? cleanFilename.slice(0, dotIdx) + suffix + cleanFilename.slice(dotIdx)
1330
+ : cleanFilename + suffix;
1331
+ destPath = join(destDir, finalFilename);
1332
+ }
1333
+
1334
+ const buffer = Buffer.from(data.data, 'base64url');
1335
+ writeFileSync(destPath, buffer);
1336
+
1337
+ info(`${progress} ${chalk.green('✓')} ${finalFilename} (${(buffer.length / 1024).toFixed(1)} KB)`);
1338
+ totalDownloaded++;
1339
+ } catch (attachErr) {
1340
+ warn(`${progress} ${chalk.red('✗')} ${attachment.filename}: ${String(attachErr)}`);
1341
+ totalErrors++;
1342
+ }
1343
+ }
1344
+ } catch (msgErr) {
1345
+ warn(`${progress} ${chalk.yellow('!')} Message ${messageId}: ${String(msgErr)}`);
1346
+ totalSkipped++;
1347
+ }
1348
+ }
1349
+
1350
+ info('');
1351
+ success('Bulk download complete:');
1352
+ info(` Downloaded: ${totalDownloaded} attachment(s)`);
1353
+ if (totalSkipped > 0) info(` Skipped: ${totalSkipped} message(s) with errors`);
1354
+ if (totalErrors > 0) info(` Failed: ${totalErrors} attachment(s)`);
1355
+ info(` Saved to: ${outputDir}`);
1356
+ } catch (err) {
1357
+ error(String(err));
1358
+ process.exit(1);
1359
+ }
1360
+ });
1361
+
1223
1362
  // ============================================
1224
1363
  // Search Command (shortcut)
1225
1364
  // ============================================
@@ -496,6 +496,20 @@ filesCmd
496
496
  }
497
497
  });
498
498
 
499
+ filesCmd
500
+ .command('rename <fileId> <newName>')
501
+ .description('Rename a file')
502
+ .action(async (fileId: string, newName: string) => {
503
+ try {
504
+ const drive = requireAuth();
505
+ const file = await drive.files.update(fileId, { name: newName });
506
+ success('Renamed to: ' + file.name);
507
+ } catch (err) {
508
+ error(String(err));
509
+ process.exit(1);
510
+ }
511
+ });
512
+
499
513
  filesCmd
500
514
  .command('share <fileId>')
501
515
  .description('Share a file with a user')
@@ -516,6 +530,130 @@ filesCmd
516
530
  }
517
531
  });
518
532
 
533
+ filesCmd
534
+ .command('bulk-upload <directory>')
535
+ .description('Bulk-upload all files from a local directory to Google Drive')
536
+ .option('--folder <folderId>', 'Target Drive folder ID (uploads to root if omitted)')
537
+ .option('--recursive', 'Preserve subfolder structure (creates Drive folders for each subdir)')
538
+ .action(async (directory: string, opts) => {
539
+ try {
540
+ const drive = requireAuth();
541
+ const { readdirSync, statSync } = await import('fs');
542
+ const { resolve: resolvePath, relative, join: joinPath } = await import('path');
543
+
544
+ const rootDir = resolvePath(directory);
545
+
546
+ // Verify directory exists
547
+ try {
548
+ const stat = statSync(rootDir);
549
+ if (!stat.isDirectory()) {
550
+ error('"' + rootDir + '" is not a directory');
551
+ process.exit(1);
552
+ }
553
+ } catch {
554
+ error('Directory not found: ' + rootDir);
555
+ process.exit(1);
556
+ }
557
+
558
+ const rootFolderId: string | undefined = opts.folder;
559
+ const recursive = !!opts.recursive;
560
+
561
+ info('Uploading from: ' + rootDir);
562
+ if (rootFolderId) info('Target folder ID: ' + rootFolderId);
563
+ if (recursive) info('Recursive: enabled (preserving subfolder structure)');
564
+
565
+ // Track Drive folder IDs for local subdirectories
566
+ const folderIdMap = new Map<string, string>();
567
+ if (rootFolderId) {
568
+ folderIdMap.set('', rootFolderId);
569
+ }
570
+
571
+ let totalUploaded = 0;
572
+ let totalErrors = 0;
573
+
574
+ /**
575
+ * Get or create a Drive folder for a given local relative path.
576
+ * e.g. 'images/2024' -> creates 'images' under root, then '2024' under that.
577
+ */
578
+ async function getDriveFolderId(relPath: string): Promise<string | undefined> {
579
+ if (!relPath) return rootFolderId;
580
+
581
+ if (folderIdMap.has(relPath)) return folderIdMap.get(relPath);
582
+
583
+ // Ensure parent exists first
584
+ const parts = relPath.split('/');
585
+ const parentRelPath = parts.slice(0, -1).join('/');
586
+ const folderName = parts[parts.length - 1];
587
+ const parentId = await getDriveFolderId(parentRelPath);
588
+
589
+ const folder = await drive.folders.create(folderName, parentId);
590
+ info(' Created folder: ' + relPath + ' (ID: ' + folder.id + ')');
591
+ const folderId = folder.id as string;
592
+ folderIdMap.set(relPath, folderId);
593
+ return folderId;
594
+ }
595
+
596
+ /**
597
+ * Upload all files in a directory, optionally recursing into subdirs.
598
+ */
599
+ async function uploadDir(localDir: string, relDir: string): Promise<void> {
600
+ let entries: string[];
601
+ try {
602
+ entries = readdirSync(localDir);
603
+ } catch (readErr) {
604
+ warn('Cannot read directory ' + localDir + ': ' + String(readErr));
605
+ return;
606
+ }
607
+
608
+ for (const entry of entries) {
609
+ const localPath = joinPath(localDir, entry);
610
+ const relPath = relDir ? relDir + '/' + entry : entry;
611
+
612
+ let stat;
613
+ try {
614
+ stat = statSync(localPath);
615
+ } catch {
616
+ warn('Cannot stat: ' + localPath);
617
+ continue;
618
+ }
619
+
620
+ if (stat.isDirectory()) {
621
+ if (recursive) {
622
+ await uploadDir(localPath, relPath);
623
+ }
624
+ // skip directories when not recursive
625
+ } else if (stat.isFile()) {
626
+ try {
627
+ const parentFolderId = await getDriveFolderId(relDir);
628
+ info(' Uploading: ' + relPath + ' (' + (stat.size / 1024).toFixed(1) + ' KB)...');
629
+
630
+ const file = await drive.files.upload(localPath, {
631
+ folderId: parentFolderId,
632
+ });
633
+
634
+ success(' ✓ ' + relPath + ' -> ' + file.name + ' (ID: ' + file.id + ')');
635
+ totalUploaded++;
636
+ } catch (uploadErr) {
637
+ warn(' ✗ ' + relPath + ': ' + String(uploadErr));
638
+ totalErrors++;
639
+ }
640
+ }
641
+ }
642
+ }
643
+
644
+ await uploadDir(rootDir, '');
645
+
646
+ info('');
647
+ success('Bulk upload complete:');
648
+ info(' Uploaded: ' + totalUploaded + ' file(s)');
649
+ if (totalErrors > 0) info(' Failed: ' + totalErrors + ' file(s)');
650
+ } catch (err) {
651
+ error(String(err));
652
+ process.exit(1);
653
+ }
654
+ });
655
+
656
+
519
657
  // ============================================
520
658
  // Folders Commands
521
659
  // ============================================
@@ -582,6 +720,20 @@ foldersCmd
582
720
  }
583
721
  });
584
722
 
723
+ foldersCmd
724
+ .command('rename <folderId> <newName>')
725
+ .description('Rename a folder')
726
+ .action(async (folderId: string, newName: string) => {
727
+ try {
728
+ const drive = requireAuth();
729
+ const folder = await drive.files.update(folderId, { name: newName });
730
+ success('Renamed to: ' + folder.name);
731
+ } catch (err) {
732
+ error(String(err));
733
+ process.exit(1);
734
+ }
735
+ });
736
+
585
737
  foldersCmd
586
738
  .command('contents <folderId>')
587
739
  .description('List contents of a folder')
package/dist/index.js CHANGED
@@ -5955,6 +5955,7 @@ function loadConnectorVersions() {
5955
5955
  }
5956
5956
  // src/lib/installer.ts
5957
5957
  import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
5958
+ import { homedir } from "os";
5958
5959
  import { join as join2, dirname as dirname2 } from "path";
5959
5960
  import { fileURLToPath as fileURLToPath2 } from "url";
5960
5961
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
@@ -6008,6 +6009,20 @@ function installConnector(name, options = {}) {
6008
6009
  mkdirSync(destDir, { recursive: true });
6009
6010
  }
6010
6011
  cpSync(sourcePath, destPath, { recursive: true });
6012
+ const homeCredDir = join2(homedir(), ".connectors", connectorName);
6013
+ if (existsSync2(homeCredDir)) {
6014
+ const filesToCopy = ["credentials.json", "current_profile"];
6015
+ for (const file of filesToCopy) {
6016
+ const src = join2(homeCredDir, file);
6017
+ if (existsSync2(src)) {
6018
+ cpSync(src, join2(destPath, file));
6019
+ }
6020
+ }
6021
+ const profilesDir = join2(homeCredDir, "profiles");
6022
+ if (existsSync2(profilesDir)) {
6023
+ cpSync(profilesDir, join2(destPath, "profiles"), { recursive: true });
6024
+ }
6025
+ }
6011
6026
  updateConnectorsIndex(destDir);
6012
6027
  return {
6013
6028
  connector: name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {