@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 +31 -16
- package/bin/mcp.js +18 -3
- package/bin/serve.js +21 -6
- package/connectors/connect-gmail/src/cli/index.ts +139 -0
- package/connectors/connect-googledrive/src/cli/index.ts +152 -0
- package/dist/index.js +15 -0
- package/package.json +1 -1
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
13598
|
-
const newBase = join6(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|