@hasna/connectors 1.3.45 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/bin/index.js +1362 -11796
- package/bin/mcp.js +751 -10328
- package/bin/serve.js +2252 -11829
- package/connectors/cloudflare-workers/.env.example +5 -0
- package/connectors/cloudflare-workers/AGENTS.md +11 -0
- package/connectors/cloudflare-workers/CLAUDE.md +11 -0
- package/connectors/cloudflare-workers/README.md +45 -0
- package/connectors/cloudflare-workers/package.json +47 -0
- package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
- package/connectors/cloudflare-workers/src/api/client.ts +386 -0
- package/connectors/cloudflare-workers/src/api/index.ts +1 -0
- package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
- package/connectors/cloudflare-workers/src/index.ts +16 -0
- package/connectors/cloudflare-workers/src/types/index.ts +53 -0
- package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
- package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
- package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
- package/connectors/cloudflare-workers/tsconfig.json +14 -0
- package/connectors/tidio/.env.example +3 -2
- package/connectors/tidio/CLAUDE.md +21 -96
- package/connectors/tidio/README.md +22 -18
- package/connectors/tidio/package.json +4 -2
- package/connectors/tidio/src/api/client.ts +26 -15
- package/connectors/tidio/src/api/index.ts +185 -128
- package/connectors/tidio/src/api/tidio.test.ts +175 -44
- package/connectors/tidio/src/cli/index.ts +154 -388
- package/connectors/tidio/src/index.ts +5 -3
- package/connectors/tidio/src/types/index.ts +135 -82
- package/connectors/tidio/src/utils/config.ts +90 -44
- package/connectors/wandb/.env.example +7 -0
- package/connectors/wandb/AGENTS.md +43 -0
- package/connectors/wandb/CLAUDE.md +67 -0
- package/connectors/wandb/README.md +65 -0
- package/connectors/wandb/package.json +52 -0
- package/connectors/wandb/src/api/client.test.ts +116 -0
- package/connectors/wandb/src/api/client.ts +83 -0
- package/connectors/wandb/src/api/graphql.ts +9 -0
- package/connectors/wandb/src/api/index.ts +36 -0
- package/connectors/wandb/src/api/projects.ts +36 -0
- package/connectors/wandb/src/api/viewer.ts +20 -0
- package/connectors/wandb/src/cli/index.ts +265 -0
- package/connectors/wandb/src/index.ts +22 -0
- package/connectors/wandb/src/types/index.ts +75 -0
- package/connectors/wandb/src/utils/config.ts +164 -0
- package/connectors/wandb/src/utils/output.ts +119 -0
- package/connectors/wandb/tsconfig.json +16 -0
- package/connectors/weights-biases/.env.example +7 -0
- package/connectors/weights-biases/CLAUDE.md +65 -0
- package/connectors/weights-biases/README.md +62 -0
- package/connectors/weights-biases/package.json +53 -0
- package/connectors/weights-biases/src/api/client.test.ts +46 -0
- package/connectors/weights-biases/src/api/client.ts +119 -0
- package/connectors/weights-biases/src/api/events.ts +18 -0
- package/connectors/weights-biases/src/api/index.ts +56 -0
- package/connectors/weights-biases/src/api/runs.ts +28 -0
- package/connectors/weights-biases/src/api/search.ts +10 -0
- package/connectors/weights-biases/src/cli/index.ts +289 -0
- package/connectors/weights-biases/src/index.ts +19 -0
- package/connectors/weights-biases/src/types/index.ts +92 -0
- package/connectors/weights-biases/src/utils/config.ts +168 -0
- package/connectors/weights-biases/src/utils/output.ts +116 -0
- package/connectors/weights-biases/tsconfig.json +16 -0
- package/connectors/x/src/api/oauth.test.ts +205 -0
- package/connectors/zoominfo/.env.example +4 -0
- package/connectors/zoominfo/AGENTS.md +26 -0
- package/connectors/zoominfo/CLAUDE.md +20 -0
- package/connectors/zoominfo/README.md +69 -0
- package/connectors/zoominfo/package.json +52 -0
- package/connectors/zoominfo/src/api/client.test.ts +140 -0
- package/connectors/zoominfo/src/api/client.ts +228 -0
- package/connectors/zoominfo/src/api/index.ts +1 -0
- package/connectors/zoominfo/src/cli/index.ts +179 -0
- package/connectors/zoominfo/src/index.ts +3 -0
- package/connectors/zoominfo/src/types/index.ts +39 -0
- package/connectors/zoominfo/src/utils/config.ts +61 -0
- package/connectors/zoominfo/src/utils/output.ts +27 -0
- package/connectors/zoominfo/tsconfig.json +20 -0
- package/connectors/zotero/.env.example +7 -0
- package/connectors/zotero/.npmrc.example +2 -0
- package/connectors/zotero/AGENTS.md +40 -0
- package/connectors/zotero/CLAUDE.md +69 -0
- package/connectors/zotero/README.md +91 -0
- package/connectors/zotero/package.json +43 -0
- package/connectors/zotero/src/api/attachments.ts +130 -0
- package/connectors/zotero/src/api/client.test.ts +166 -0
- package/connectors/zotero/src/api/client.ts +169 -0
- package/connectors/zotero/src/api/collections.ts +26 -0
- package/connectors/zotero/src/api/index.ts +45 -0
- package/connectors/zotero/src/api/items.ts +64 -0
- package/connectors/zotero/src/cli/index.ts +386 -0
- package/connectors/zotero/src/index.ts +28 -0
- package/connectors/zotero/src/types/index.ts +129 -0
- package/connectors/zotero/src/utils/config.ts +245 -0
- package/connectors/zotero/src/utils/output.ts +119 -0
- package/connectors/zotero/tsconfig.json +16 -0
- package/connectors/zymbly/.env.example +2 -0
- package/connectors/zymbly/AGENTS.md +11 -0
- package/connectors/zymbly/CLAUDE.md +5 -0
- package/connectors/zymbly/README.md +35 -0
- package/connectors/zymbly/package.json +52 -0
- package/connectors/zymbly/src/api/client.test.ts +110 -0
- package/connectors/zymbly/src/api/client.ts +166 -0
- package/connectors/zymbly/src/api/index.ts +1 -0
- package/connectors/zymbly/src/cli/index.ts +156 -0
- package/connectors/zymbly/src/index.ts +12 -0
- package/connectors/zymbly/src/types/index.ts +34 -0
- package/connectors/zymbly/src/utils/config.ts +52 -0
- package/connectors/zymbly/src/utils/output.ts +17 -0
- package/connectors/zymbly/tsconfig.json +13 -0
- package/dist/db/database.d.ts +2 -1
- package/dist/db/sqlite-adapter.d.ts +43 -0
- package/dist/index.js +172 -9653
- package/dist/lib/compact-output.d.ts +40 -0
- package/dist/mcp/tools/jobs.d.ts +32 -0
- package/dist/mcp/tools/jobs.test.d.ts +1 -0
- package/dist/no-cloud-boundary.test.d.ts +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { HttpMethod, JsonObject, JsonValue, QueryValue, RequestOptions, ZoomInfoConfig } from "../types";
|
|
2
|
+
import { ZoomInfoApiError } from "../types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BASE_URL = "https://api.zoominfo.com";
|
|
5
|
+
|
|
6
|
+
export class ZoomInfoClient {
|
|
7
|
+
private readonly username?: string;
|
|
8
|
+
private readonly password?: string;
|
|
9
|
+
private readonly configuredJwt?: string;
|
|
10
|
+
private readonly baseUrl: string;
|
|
11
|
+
private cachedJwt?: string;
|
|
12
|
+
|
|
13
|
+
constructor(config: ZoomInfoConfig = {}) {
|
|
14
|
+
this.username = config.username;
|
|
15
|
+
this.password = config.password;
|
|
16
|
+
this.configuredJwt = config.jwt;
|
|
17
|
+
this.baseUrl = this.cleanBaseUrl(config.baseUrl || DEFAULT_BASE_URL);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async authenticate(): Promise<{ jwt: string }> {
|
|
21
|
+
const username = this.requireString(this.username, "username");
|
|
22
|
+
const password = this.requireString(this.password, "password");
|
|
23
|
+
|
|
24
|
+
const response = await fetch(`${this.baseUrl}/authenticate`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ username, password }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
const data = this.parseResponseBody(text, response.headers.get("content-type"));
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new ZoomInfoApiError(this.errorMessage(data, response.statusText), response.status, data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const jwt = this.extractJwt(data);
|
|
41
|
+
this.cachedJwt = jwt;
|
|
42
|
+
return { jwt };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async searchContacts(body: JsonValue = {}, params: Record<string, QueryValue> = {}) {
|
|
46
|
+
return this.request("/search/contact", { method: "POST", params, body });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async searchCompanies(body: JsonValue = {}, params: Record<string, QueryValue> = {}) {
|
|
50
|
+
return this.request("/search/company", { method: "POST", params, body });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async enrichContact(body: JsonValue) {
|
|
54
|
+
this.requireMatchPersonInput(body);
|
|
55
|
+
return this.request("/enrich/contact", { method: "POST", body });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async enrichCompany(body: JsonValue = {}) {
|
|
59
|
+
return this.request("/enrich/company", { method: "POST", body });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async lookupContact(contactId: string) {
|
|
63
|
+
return this.request(`/lookup/contact/${this.segment(contactId)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async listContactSearchOutputFields() {
|
|
67
|
+
return this.request("/lookup/outputfields/contact/search");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async listCompanySearchOutputFields() {
|
|
71
|
+
return this.request("/lookup/outputfields/company/search");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
rawRequest<T = unknown>(method: HttpMethod, path: string, options: Omit<RequestOptions, "method"> = {}) {
|
|
75
|
+
return this.request<T>(path, { ...options, method });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getBaseUrl(): string {
|
|
79
|
+
return this.baseUrl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async request<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
83
|
+
const method = options.method || "GET";
|
|
84
|
+
const url = this.buildUrl(path, options.params);
|
|
85
|
+
const headers = await this.prepareHeaders(options);
|
|
86
|
+
const body = this.prepareBody(method, options.body, headers);
|
|
87
|
+
|
|
88
|
+
const response = await fetch(url.toString(), {
|
|
89
|
+
method,
|
|
90
|
+
headers,
|
|
91
|
+
body,
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
const data = options.responseType === "text" ? text : this.parseResponseBody(text, response.headers.get("content-type"));
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new ZoomInfoApiError(this.errorMessage(data, response.statusText), response.status, data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async prepareHeaders(options: RequestOptions): Promise<Record<string, string>> {
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
...options.headers,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (options.auth !== false) {
|
|
110
|
+
const jwt = await this.resolveJwt();
|
|
111
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return headers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async resolveJwt(): Promise<string> {
|
|
118
|
+
if (this.configuredJwt) {
|
|
119
|
+
return this.configuredJwt;
|
|
120
|
+
}
|
|
121
|
+
if (this.cachedJwt) {
|
|
122
|
+
return this.cachedJwt;
|
|
123
|
+
}
|
|
124
|
+
const result = await this.authenticate();
|
|
125
|
+
return result.jwt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private extractJwt(data: unknown): string {
|
|
129
|
+
if (typeof data === "object" && data !== null) {
|
|
130
|
+
const record = data as Record<string, unknown>;
|
|
131
|
+
for (const key of ["jwt", "access_token", "token"]) {
|
|
132
|
+
const value = record[key];
|
|
133
|
+
if (typeof value === "string" && value.trim()) {
|
|
134
|
+
return value.trim();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new ZoomInfoApiError("ZoomInfo authenticate response did not include a JWT");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private requireMatchPersonInput(body: JsonValue): void {
|
|
142
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
143
|
+
throw new Error("ZoomInfo enrichContact body must be a JSON object with matchPersonInput");
|
|
144
|
+
}
|
|
145
|
+
const matchPersonInput = (body as JsonObject).matchPersonInput;
|
|
146
|
+
if (!Array.isArray(matchPersonInput) || matchPersonInput.length === 0) {
|
|
147
|
+
throw new Error("ZoomInfo enrichContact requires matchPersonInput");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private prepareBody(method: HttpMethod, body: JsonValue | string | undefined, headers: Record<string, string>): string | undefined {
|
|
152
|
+
if (body === undefined || body === null || method === "GET") return undefined;
|
|
153
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
154
|
+
return typeof body === "string" ? body : JSON.stringify(this.cleanObject(body));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private buildUrl(path: string, params?: Record<string, QueryValue>): URL {
|
|
158
|
+
const trimmedPath = path.trim();
|
|
159
|
+
if (!trimmedPath) throw new Error("ZoomInfo API path is required");
|
|
160
|
+
if (/^https?:\/\//i.test(trimmedPath)) throw new Error("ZoomInfo raw paths must be relative, not absolute URLs");
|
|
161
|
+
if (trimmedPath.includes("..")) throw new Error("ZoomInfo raw paths must not contain path traversal");
|
|
162
|
+
|
|
163
|
+
const normalizedPath = trimmedPath.startsWith("/") ? trimmedPath : `/${trimmedPath}`;
|
|
164
|
+
const url = new URL(`${this.baseUrl}${normalizedPath}`);
|
|
165
|
+
if (params) {
|
|
166
|
+
for (const [key, value] of Object.entries(params)) this.appendQueryValue(url, key, value);
|
|
167
|
+
}
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private appendQueryValue(url: URL, key: string, value: QueryValue): void {
|
|
172
|
+
if (value === undefined || value === null || value === "") return;
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
for (const item of value) url.searchParams.append(key, String(item));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
url.searchParams.append(key, String(value));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private parseResponseBody(text: string, contentType: string | null): unknown {
|
|
181
|
+
if (!text) return {};
|
|
182
|
+
if (contentType?.includes("json") || text.startsWith("{") || text.startsWith("[")) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(text);
|
|
185
|
+
} catch {
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return text;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private errorMessage(data: unknown, statusText: string): string {
|
|
193
|
+
if (typeof data === "object" && data !== null) {
|
|
194
|
+
const record = data as Record<string, unknown>;
|
|
195
|
+
if (typeof record.error_message === "string") return record.error_message;
|
|
196
|
+
if (typeof record.error === "string") return record.error;
|
|
197
|
+
if (typeof record.message === "string") return record.message;
|
|
198
|
+
}
|
|
199
|
+
if (typeof data === "string" && data.trim()) return data.slice(0, 500);
|
|
200
|
+
return statusText || "ZoomInfo API request failed";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private cleanBaseUrl(baseUrl: string): string {
|
|
204
|
+
const trimmed = baseUrl.trim();
|
|
205
|
+
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
206
|
+
return withProtocol.replace(/\/+$/, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private segment(value: string): string {
|
|
210
|
+
return encodeURIComponent(this.requireString(value, "path segment"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private requireString(value: string | undefined, label: string): string {
|
|
214
|
+
const trimmed = value?.trim();
|
|
215
|
+
if (!trimmed) throw new Error(`ZoomInfo ${label} is required`);
|
|
216
|
+
return trimmed;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private cleanObject(value: JsonValue): JsonValue {
|
|
220
|
+
if (Array.isArray(value)) return value.map((item) => this.cleanObject(item));
|
|
221
|
+
if (typeof value !== "object" || value === null) return value;
|
|
222
|
+
const cleaned: Record<string, JsonValue> = {};
|
|
223
|
+
for (const [key, child] of Object.entries(value)) {
|
|
224
|
+
if (child !== undefined) cleaned[key] = this.cleanObject(child);
|
|
225
|
+
}
|
|
226
|
+
return cleaned;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ZoomInfoClient, ZoomInfoClient as ZoomInfo } from "./client";
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { ZoomInfo } from "../api";
|
|
4
|
+
import type { HttpMethod, JsonObject, JsonValue, OutputFormat, QueryPrimitive, QueryValue } from "../types";
|
|
5
|
+
import {
|
|
6
|
+
clearConfig,
|
|
7
|
+
getBaseUrl,
|
|
8
|
+
getJwt,
|
|
9
|
+
getPassword,
|
|
10
|
+
getUsername,
|
|
11
|
+
loadConfig,
|
|
12
|
+
setBaseUrl,
|
|
13
|
+
setJwt,
|
|
14
|
+
setPassword,
|
|
15
|
+
setUsername,
|
|
16
|
+
} from "../utils/config";
|
|
17
|
+
import { error, info, print, success } from "../utils/output";
|
|
18
|
+
|
|
19
|
+
const CONNECTOR_NAME = "connect-zoominfo";
|
|
20
|
+
const VERSION = "0.0.1";
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name(CONNECTOR_NAME)
|
|
26
|
+
.description("ZoomInfo B2B sales intelligence API CLI")
|
|
27
|
+
.version(VERSION)
|
|
28
|
+
.option("-f, --format <format>", "Output format (json, pretty)", "pretty")
|
|
29
|
+
.option("--username <username>", "ZoomInfo API username")
|
|
30
|
+
.option("--password <password>", "ZoomInfo API password")
|
|
31
|
+
.option("--jwt <jwt>", "Preconfigured ZoomInfo JWT (skips /authenticate)")
|
|
32
|
+
.option("--base-url <url>", "Override ZoomInfo API base URL");
|
|
33
|
+
|
|
34
|
+
function getFormat(): OutputFormat {
|
|
35
|
+
return (program.opts().format || "pretty") as OutputFormat;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getCliOptions(): Record<string, unknown> {
|
|
39
|
+
return program.opts() as Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function client(): ZoomInfo {
|
|
43
|
+
const opts = getCliOptions();
|
|
44
|
+
return new ZoomInfo({
|
|
45
|
+
username: optionString(opts, "username") || getUsername(),
|
|
46
|
+
password: optionString(opts, "password") || getPassword(),
|
|
47
|
+
jwt: optionString(opts, "jwt") || getJwt(),
|
|
48
|
+
baseUrl: optionString(opts, "baseUrl") || getBaseUrl(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseJsonValue(value: string | undefined, label: string, fallback: JsonValue | undefined): JsonValue | undefined {
|
|
53
|
+
if (!value) return fallback;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(value) as JsonValue;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw new Error(`Invalid ${label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseJsonObject(value: string | undefined, label: string, fallback: JsonObject = {}): JsonObject {
|
|
62
|
+
const parsed = parseJsonValue(value, label, fallback);
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${label} must be a JSON object`);
|
|
64
|
+
return parsed as JsonObject;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toQueryValue(value: unknown, label: string): QueryValue {
|
|
68
|
+
if (value === undefined || value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
for (const item of value) {
|
|
71
|
+
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
72
|
+
throw new Error(`Invalid ${label}: arrays must contain strings, numbers, or booleans`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return value as QueryPrimitive[];
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Invalid ${label}: expected string, number, boolean, array, null, or undefined`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseParams(value: unknown): Record<string, QueryValue> {
|
|
81
|
+
const parsed = parseJsonObject(typeof value === "string" ? value : undefined, "params");
|
|
82
|
+
const params: Record<string, QueryValue> = {};
|
|
83
|
+
for (const [key, rawValue] of Object.entries(parsed)) params[key] = toQueryValue(rawValue, `params.${key}`);
|
|
84
|
+
return params;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseHeaders(value: unknown): Record<string, string> {
|
|
88
|
+
const parsed = parseJsonObject(typeof value === "string" ? value : undefined, "headers");
|
|
89
|
+
const headers: Record<string, string> = {};
|
|
90
|
+
for (const [key, rawValue] of Object.entries(parsed)) {
|
|
91
|
+
if (typeof rawValue !== "string") throw new Error(`headers.${key} must be a string`);
|
|
92
|
+
headers[key] = rawValue;
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function optionString(opts: Record<string, unknown>, key: string): string | undefined {
|
|
98
|
+
const value = opts[key];
|
|
99
|
+
return typeof value === "string" ? value : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function optionBody(opts: Record<string, unknown>, fallback: JsonValue = {}): JsonValue {
|
|
103
|
+
return parseJsonValue(optionString(opts, "body"), "body", fallback) ?? fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function run(action: () => Promise<unknown>): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
print(await action(), getFormat());
|
|
109
|
+
} catch (err) {
|
|
110
|
+
error(err instanceof Error ? err.message : String(err));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const configCmd = program.command("config").description("Manage CLI configuration");
|
|
116
|
+
|
|
117
|
+
configCmd.command("set-username <username>").description("Store a ZoomInfo API username").action((username: string) => {
|
|
118
|
+
setUsername(username);
|
|
119
|
+
success("Username saved");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
configCmd.command("set-password <password>").description("Store a ZoomInfo API password").action((password: string) => {
|
|
123
|
+
setPassword(password);
|
|
124
|
+
success("Password saved");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
configCmd.command("set-jwt <jwt>").description("Store a preconfigured ZoomInfo JWT").action((jwt: string) => {
|
|
128
|
+
setJwt(jwt);
|
|
129
|
+
success("JWT saved");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
configCmd.command("set-base-url <baseUrl>").description("Set a custom ZoomInfo API base URL").action((baseUrl: string) => {
|
|
133
|
+
setBaseUrl(baseUrl);
|
|
134
|
+
success(`Base URL set to ${baseUrl}`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
configCmd.command("show").description("Show current configuration").action(() => {
|
|
138
|
+
const api = client();
|
|
139
|
+
const config = loadConfig();
|
|
140
|
+
info(`Username: ${getUsername() ? "configured" : "not set"}`);
|
|
141
|
+
info(`Password: ${getPassword() ? "configured" : "not set"}`);
|
|
142
|
+
info(`JWT: ${getJwt() ? "configured" : "not set"}`);
|
|
143
|
+
info(`Base URL: ${api.getBaseUrl()}`);
|
|
144
|
+
if (Object.keys(config).length > 0) info("Stored config file is present");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
configCmd.command("clear").description("Clear saved configuration").action(() => {
|
|
148
|
+
clearConfig();
|
|
149
|
+
success("Configuration cleared");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program.command("authenticate").description("Authenticate with username/password and obtain a JWT").action(() => run(() => client().authenticate()));
|
|
153
|
+
program.command("search-contacts").description("Search ZoomInfo contacts").option("--body <json>", "JSON request body", "{}").option("--params <json>", "Query parameters as JSON object").action((opts: Record<string, unknown>) => run(() => client().searchContacts(optionBody(opts), parseParams(opts.params))));
|
|
154
|
+
program.command("search-companies").description("Search ZoomInfo companies").option("--body <json>", "JSON request body", "{}").option("--params <json>", "Query parameters as JSON object").action((opts: Record<string, unknown>) => run(() => client().searchCompanies(optionBody(opts), parseParams(opts.params))));
|
|
155
|
+
program.command("enrich-contact").description("Enrich contacts (requires matchPersonInput in body)").requiredOption("--body <json>", "JSON request body with matchPersonInput").action((opts: Record<string, unknown>) => run(() => client().enrichContact(optionBody(opts))));
|
|
156
|
+
program.command("enrich-company").description("Enrich companies").option("--body <json>", "JSON request body", "{}").action((opts: Record<string, unknown>) => run(() => client().enrichCompany(optionBody(opts))));
|
|
157
|
+
program.command("lookup-contact <contactId>").description("Look up a contact by ZoomInfo ID").action((contactId: string) => run(() => client().lookupContact(contactId)));
|
|
158
|
+
program.command("contact-search-output-fields").description("List available contact search output fields").action(() => run(() => client().listContactSearchOutputFields()));
|
|
159
|
+
program.command("company-search-output-fields").description("List available company search output fields").action(() => run(() => client().listCompanySearchOutputFields()));
|
|
160
|
+
|
|
161
|
+
program
|
|
162
|
+
.command("raw-request <method> <path>")
|
|
163
|
+
.description("Send a raw relative ZoomInfo request")
|
|
164
|
+
.option("--params <json>", "Query parameters as JSON object")
|
|
165
|
+
.option("--body <json>", "JSON request body")
|
|
166
|
+
.option("--headers <json>", "Additional headers as JSON object")
|
|
167
|
+
.option("--no-auth", "Do not attach Bearer JWT")
|
|
168
|
+
.action((method: string, path: string, opts: Record<string, unknown>) =>
|
|
169
|
+
run(() =>
|
|
170
|
+
client().rawRequest(method.toUpperCase() as HttpMethod, path, {
|
|
171
|
+
params: parseParams(opts.params),
|
|
172
|
+
body: parseJsonValue(optionString(opts, "body"), "body", undefined),
|
|
173
|
+
headers: parseHeaders(opts.headers),
|
|
174
|
+
auth: opts.auth !== false,
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
program.parse();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type OutputFormat = "json" | "pretty";
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
3
|
+
|
|
4
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
5
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
6
|
+
|
|
7
|
+
export interface JsonObject {
|
|
8
|
+
[key: string]: JsonValue | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type QueryPrimitive = string | number | boolean;
|
|
12
|
+
export type QueryValue = QueryPrimitive | QueryPrimitive[] | null | undefined;
|
|
13
|
+
|
|
14
|
+
export interface ZoomInfoConfig {
|
|
15
|
+
username?: string;
|
|
16
|
+
password?: string;
|
|
17
|
+
jwt?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RequestOptions {
|
|
22
|
+
method?: HttpMethod;
|
|
23
|
+
params?: Record<string, QueryValue>;
|
|
24
|
+
body?: JsonValue | string;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
responseType?: "json" | "text";
|
|
27
|
+
auth?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ZoomInfoApiError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
public readonly status?: number,
|
|
34
|
+
public readonly data?: unknown,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ZoomInfoApiError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
interface StoredConfig {
|
|
6
|
+
username?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
jwt?: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CONFIG_PATH = join(homedir(), ".hasna", "connectors", "zoominfo", "config.json");
|
|
13
|
+
|
|
14
|
+
export function loadConfig(): StoredConfig {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as StoredConfig;
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function saveConfig(config: StoredConfig): void {
|
|
23
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
24
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function clearConfig(): void {
|
|
28
|
+
rmSync(CONFIG_PATH, { force: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getUsername(): string | undefined {
|
|
32
|
+
return process.env.ZOOMINFO_USERNAME || loadConfig().username;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function setUsername(username: string): void {
|
|
36
|
+
saveConfig({ ...loadConfig(), username });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getPassword(): string | undefined {
|
|
40
|
+
return process.env.ZOOMINFO_PASSWORD || loadConfig().password;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setPassword(password: string): void {
|
|
44
|
+
saveConfig({ ...loadConfig(), password });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getJwt(): string | undefined {
|
|
48
|
+
return process.env.ZOOMINFO_JWT || loadConfig().jwt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function setJwt(jwt: string): void {
|
|
52
|
+
saveConfig({ ...loadConfig(), jwt });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getBaseUrl(): string | undefined {
|
|
56
|
+
return process.env.ZOOMINFO_BASE_URL || loadConfig().baseUrl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setBaseUrl(baseUrl: string): void {
|
|
60
|
+
saveConfig({ ...loadConfig(), baseUrl });
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OutputFormat } from "../types";
|
|
2
|
+
|
|
3
|
+
export function print(value: unknown, format: OutputFormat = "pretty"): void {
|
|
4
|
+
if (format === "json") {
|
|
5
|
+
console.log(JSON.stringify(value, null, 2));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
console.log(value);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(JSON.stringify(value, null, 2));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function success(message: string): void {
|
|
18
|
+
console.log(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function info(message: string): void {
|
|
22
|
+
console.log(message);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function error(message: string): void {
|
|
26
|
+
console.error(message);
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"types": ["bun-types"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "bin"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI agents working with connect-zotero.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Zotero Web API v3 connector with API-key auth, multi-profile config, and CLI/library entry points.
|
|
8
|
+
|
|
9
|
+
## Quick Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install
|
|
13
|
+
bun run dev
|
|
14
|
+
bun run build
|
|
15
|
+
bun run typecheck
|
|
16
|
+
bun test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Key Features
|
|
20
|
+
|
|
21
|
+
- Items: list, search, get, create, update, delete
|
|
22
|
+
- Collections: list, get, create
|
|
23
|
+
- Attachments: URL/file attachments and 3-step file upload
|
|
24
|
+
- Raw request escape hatch for unlisted endpoints
|
|
25
|
+
- Multi-profile config under `~/.hasna/connectors/connect-zotero/`
|
|
26
|
+
|
|
27
|
+
## Environment
|
|
28
|
+
|
|
29
|
+
| Variable | Description |
|
|
30
|
+
|----------|-------------|
|
|
31
|
+
| `ZOTERO_API_KEY` | API key |
|
|
32
|
+
| `ZOTERO_LIBRARY_ID` | Library ID |
|
|
33
|
+
| `ZOTERO_LIBRARY_TYPE` | `users` or `groups` |
|
|
34
|
+
| `ZOTERO_BASE_URL` | Optional API base URL |
|
|
35
|
+
|
|
36
|
+
## Security
|
|
37
|
+
|
|
38
|
+
- Never commit real API keys
|
|
39
|
+
- `.env.example` contains placeholders only
|
|
40
|
+
- No browser-use or scraper dependencies
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code when working with this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
connect-zotero is a TypeScript CLI and library for the Zotero Web API v3. It provides item, collection, and attachment management with multi-profile API key configuration.
|
|
8
|
+
|
|
9
|
+
## Build & Run Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install # Install dependencies
|
|
13
|
+
bun run dev # Run CLI in development
|
|
14
|
+
bun run build # Build for distribution
|
|
15
|
+
bun run typecheck # Type check
|
|
16
|
+
bun test # Run connector tests
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Code Style
|
|
20
|
+
|
|
21
|
+
- TypeScript with strict mode
|
|
22
|
+
- ESM modules (type: module)
|
|
23
|
+
- Async/await for all async operations
|
|
24
|
+
- Minimal dependencies: commander, chalk
|
|
25
|
+
|
|
26
|
+
## Project Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── api/
|
|
31
|
+
│ ├── client.ts # HTTP client with Zotero-API-Key auth
|
|
32
|
+
│ ├── items.ts # Items CRUD and search
|
|
33
|
+
│ ├── collections.ts # Collections list/create
|
|
34
|
+
│ ├── attachments.ts # Attachment create and file upload
|
|
35
|
+
│ └── index.ts # Main Zotero class
|
|
36
|
+
├── cli/
|
|
37
|
+
│ └── index.ts # CLI commands
|
|
38
|
+
├── types/
|
|
39
|
+
│ └── index.ts # TypeScript types
|
|
40
|
+
├── utils/
|
|
41
|
+
│ ├── config.ts # Multi-profile configuration
|
|
42
|
+
│ └── output.ts # CLI output formatting
|
|
43
|
+
└── index.ts # Library exports
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Environment Variables
|
|
47
|
+
|
|
48
|
+
| Variable | Description |
|
|
49
|
+
|----------|-------------|
|
|
50
|
+
| `ZOTERO_API_KEY` | Zotero API key (Zotero-API-Key header) |
|
|
51
|
+
| `ZOTERO_LIBRARY_ID` | User or group library ID |
|
|
52
|
+
| `ZOTERO_LIBRARY_TYPE` | `users` (default) or `groups` |
|
|
53
|
+
| `ZOTERO_BASE_URL` | Optional override (default https://api.zotero.org) |
|
|
54
|
+
|
|
55
|
+
## Authentication
|
|
56
|
+
|
|
57
|
+
Auth type: **apikey** (no OAuth). Config dir: `~/.hasna/connectors/connect-zotero/profiles/{name}/config.json`
|
|
58
|
+
|
|
59
|
+
## API Notes
|
|
60
|
+
|
|
61
|
+
- Base URL: https://api.zotero.org
|
|
62
|
+
- Required headers: `Zotero-API-Key`, `Zotero-API-Version: 3`
|
|
63
|
+
- Update/delete require `If-Unmodified-Since-Version` with the item version
|
|
64
|
+
- File upload uses a 3-step protocol: auth POST → S3 POST → register POST
|
|
65
|
+
|
|
66
|
+
## Dependencies
|
|
67
|
+
|
|
68
|
+
- commander: CLI framework
|
|
69
|
+
- chalk: Terminal styling
|