argorant 0.1.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.
Files changed (3) hide show
  1. package/README.md +55 -0
  2. package/bin/argorant.js +406 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # argorant
2
+
3
+ Search, count, reveal, and export verified B2B contacts from the Argorant
4
+ database — from your terminal, scripts, or coding agent. No install required.
5
+
6
+ ```sh
7
+ npx argorant count "fintech CFOs in germany"
8
+ ```
9
+
10
+ Counts and searches are **free**. Reveals and exports draw on your Argorant
11
+ workspace quota/credits — the same pool as the app, API, and MCP server.
12
+
13
+ ## Authenticate
14
+
15
+ Create an API key at **app.argorant.com/profile** (API keys), then:
16
+
17
+ ```sh
18
+ npx argorant login # paste your ag_live_ key (stored in ~/.argorant)
19
+ ```
20
+
21
+ Or set `ARGORANT_API_KEY=ag_live_…` in your environment — ideal for scripts,
22
+ CI, and agents.
23
+
24
+ ## Commands
25
+
26
+ | Command | What it does | Cost |
27
+ | --- | --- | --- |
28
+ | `login [key]` | Save an API key | — |
29
+ | `whoami` | Account, scopes, daily quota | free |
30
+ | `count "<query>"` | Count matching contacts | free |
31
+ | `search "<query>" -n 10` | Preview matches (details redacted) | free |
32
+ | `reveal "<query>" -n 25` | Reveal full contact details | quota |
33
+ | `export "<query>" -n 1000 -o leads.csv` | Verified CSV export, polled until ready | quota |
34
+
35
+ ## Filters
36
+
37
+ Combine free text with structured filters:
38
+
39
+ ```
40
+ --title --seniority --department --industry
41
+ --country --state --city --company --domain
42
+ --verify-status --has-phone --has-linkedin --has-email
43
+ ```
44
+
45
+ Options: `-n/--limit`, `-o/--output`, `--json`, `-y/--yes`, `--base`.
46
+
47
+ ## Built for agents
48
+
49
+ ```sh
50
+ export ARGORANT_API_KEY=ag_live_...
51
+ npx argorant count --industry logistics --country "United States" --seniority vp --json
52
+ npx argorant reveal "heads of procurement" --country Germany -n 25 --json --yes
53
+ ```
54
+
55
+ Full docs: https://argorant.com/docs/cli
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Argorant CLI — a thin, dependency-free wrapper over the Argorant REST API.
5
+ // Search, count, reveal, and export verified B2B contacts from the terminal.
6
+ // Auth: an Argorant API key (ag_live_*) via `argorant login`, or ARGORANT_API_KEY.
7
+
8
+ const https = require("https");
9
+ const http = require("http");
10
+ const fs = require("fs");
11
+ const os = require("os");
12
+ const path = require("path");
13
+ const readline = require("readline");
14
+ const { URL } = require("url");
15
+
16
+ const VERSION = require("../package.json").version;
17
+ const DEFAULT_BASE = process.env.ARGORANT_API_BASE || "https://argorant.com";
18
+ const CONFIG_DIR = path.join(os.homedir(), ".argorant");
19
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
20
+
21
+ // ---- tiny ANSI helpers (auto-disabled when not a TTY) ----
22
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
23
+ const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
24
+ const bold = (s) => c("1", s);
25
+ const dim = (s) => c("2", s);
26
+ const green = (s) => c("32", s);
27
+ const red = (s) => c("31", s);
28
+ const cyan = (s) => c("36", s);
29
+
30
+ function die(msg, code = 1) {
31
+ process.stderr.write(red("error: ") + msg + "\n");
32
+ process.exit(code);
33
+ }
34
+
35
+ // ---- config / key storage ----
36
+ function loadConfig() {
37
+ try {
38
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+ function saveConfig(cfg) {
44
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
45
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
46
+ }
47
+ function resolveKey() {
48
+ return process.env.ARGORANT_API_KEY || loadConfig().apiKey || "";
49
+ }
50
+
51
+ // ---- arg parsing ----
52
+ // Flags that map to API filter params. Value flags take the next token.
53
+ const VALUE_FLAGS = {
54
+ "--title": "title",
55
+ "--seniority": "seniority",
56
+ "--department": "departments",
57
+ "--departments": "departments",
58
+ "--industry": "industry",
59
+ "--country": "country",
60
+ "--state": "state",
61
+ "--city": "city",
62
+ "--company": "company_name",
63
+ "--domain": "company_domain",
64
+ "--verify-status": "verify_status",
65
+ };
66
+ // Boolean filter flags (presence => "true").
67
+ const BOOL_FLAGS = {
68
+ "--has-phone": "has_phone",
69
+ "--has-linkedin": "has_linkedin",
70
+ "--has-email": "has_email",
71
+ };
72
+
73
+ function parseArgs(argv) {
74
+ const out = { _: [], filters: {}, limit: null, output: null, json: false, yes: false, base: DEFAULT_BASE };
75
+ for (let i = 0; i < argv.length; i++) {
76
+ const a = argv[i];
77
+ if (a === "--json") out.json = true;
78
+ else if (a === "--yes" || a === "-y") out.yes = true;
79
+ else if (a === "-n" || a === "--limit") out.limit = parseInt(argv[++i], 10);
80
+ else if (a === "-o" || a === "--output") out.output = argv[++i];
81
+ else if (a === "--base") out.base = argv[++i];
82
+ else if (a in VALUE_FLAGS) out.filters[VALUE_FLAGS[a]] = argv[++i];
83
+ else if (a in BOOL_FLAGS) out.filters[BOOL_FLAGS[a]] = "true";
84
+ else if (a.startsWith("--") && a.includes("=")) {
85
+ const [k, v] = [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)];
86
+ if (k in VALUE_FLAGS) out.filters[VALUE_FLAGS[k]] = v;
87
+ else die(`unknown flag: ${k}`);
88
+ } else if (a.startsWith("-") && a !== "-") {
89
+ die(`unknown flag: ${a}`);
90
+ } else {
91
+ out._.push(a);
92
+ }
93
+ }
94
+ // Free-text positional → q
95
+ if (out._.length) out.filters.q = out._.join(" ");
96
+ return out;
97
+ }
98
+
99
+ // ---- HTTP ----
100
+ function request(method, base, urlPath, { key, query, body } = {}) {
101
+ return new Promise((resolve, reject) => {
102
+ let u;
103
+ try {
104
+ u = new URL(urlPath, base);
105
+ } catch (e) {
106
+ return reject(new Error(`bad URL: ${urlPath}`));
107
+ }
108
+ if (query) {
109
+ for (const [k, v] of Object.entries(query)) {
110
+ if (v !== undefined && v !== null && v !== "") u.searchParams.set(k, v);
111
+ }
112
+ }
113
+ const payload = body ? Buffer.from(JSON.stringify(body)) : null;
114
+ const headers = { Accept: "application/json", "User-Agent": `argorant-cli/${VERSION}` };
115
+ if (key) headers["Authorization"] = `Bearer ${key}`;
116
+ if (payload) {
117
+ headers["Content-Type"] = "application/json";
118
+ headers["Content-Length"] = payload.length;
119
+ }
120
+ const lib = u.protocol === "http:" ? http : https;
121
+ const req = lib.request(
122
+ u,
123
+ { method, headers },
124
+ (res) => {
125
+ const chunks = [];
126
+ res.on("data", (d) => chunks.push(d));
127
+ res.on("end", () => {
128
+ const raw = Buffer.concat(chunks).toString("utf8");
129
+ let json = null;
130
+ try {
131
+ json = raw ? JSON.parse(raw) : null;
132
+ } catch {
133
+ /* non-JSON (e.g. CSV download) */
134
+ }
135
+ resolve({ status: res.statusCode, json, raw, res });
136
+ });
137
+ }
138
+ );
139
+ req.on("error", reject);
140
+ if (payload) req.write(payload);
141
+ req.end();
142
+ });
143
+ }
144
+
145
+ function downloadTo(base, urlPath, key, dest) {
146
+ return new Promise((resolve, reject) => {
147
+ const u = new URL(urlPath, base);
148
+ const lib = u.protocol === "http:" ? http : https;
149
+ const req = lib.request(
150
+ u,
151
+ { method: "GET", headers: { Authorization: `Bearer ${key}`, "User-Agent": `argorant-cli/${VERSION}` } },
152
+ (res) => {
153
+ if (res.statusCode !== 200) {
154
+ const chunks = [];
155
+ res.on("data", (d) => chunks.push(d));
156
+ res.on("end", () => reject(new Error(`download failed (HTTP ${res.statusCode}): ${Buffer.concat(chunks).toString("utf8").slice(0, 300)}`)));
157
+ return;
158
+ }
159
+ const file = fs.createWriteStream(dest);
160
+ res.pipe(file);
161
+ file.on("finish", () => file.close(() => resolve(dest)));
162
+ file.on("error", reject);
163
+ }
164
+ );
165
+ req.on("error", reject);
166
+ req.end();
167
+ });
168
+ }
169
+
170
+ function need(res, what) {
171
+ if (res.status === 401) die("not authenticated. Run `argorant login` or set ARGORANT_API_KEY.", 2);
172
+ if (res.status === 403) die((res.json && res.json.detail) || `forbidden — your key lacks the scope for ${what}.`, 3);
173
+ if (res.status === 429) die((res.json && res.json.detail) || "rate limit / daily quota reached.", 4);
174
+ if (res.status >= 400) die((res.json && res.json.detail) || `${what} failed (HTTP ${res.status}).`);
175
+ return res.json || {};
176
+ }
177
+
178
+ function requireKey() {
179
+ const k = resolveKey();
180
+ if (!k) die("no API key. Run `argorant login` or set ARGORANT_API_KEY.", 2);
181
+ return k;
182
+ }
183
+
184
+ function prompt(question, { hidden = false } = {}) {
185
+ return new Promise((resolve) => {
186
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
187
+ if (hidden) {
188
+ // best-effort masking
189
+ const onData = () => {
190
+ readline.clearLine(process.stdout, 0);
191
+ readline.cursorTo(process.stdout, 0);
192
+ process.stdout.write(question);
193
+ };
194
+ process.stdin.on("data", onData);
195
+ rl.question(question, (ans) => {
196
+ process.stdin.removeListener("data", onData);
197
+ rl.close();
198
+ process.stdout.write("\n");
199
+ resolve(ans.trim());
200
+ });
201
+ } else {
202
+ rl.question(question, (ans) => {
203
+ rl.close();
204
+ resolve(ans.trim());
205
+ });
206
+ }
207
+ });
208
+ }
209
+
210
+ // ---- commands ----
211
+ async function cmdLogin(args) {
212
+ let key = args._[0];
213
+ if (!key) key = await prompt("Paste your Argorant API key (ag_live_…): ", { hidden: true });
214
+ if (!key) die("no key provided.");
215
+ if (!/^ag_(live|test)_/.test(key)) process.stderr.write(dim("note: keys normally start with ag_live_ — continuing anyway.\n"));
216
+ const res = await request("GET", args.base, "/api/mcp/account", { key });
217
+ if (res.status === 401) die("that key was rejected (401). Double-check you copied the whole ag_live_ key.", 2);
218
+ const acct = need(res, "login");
219
+ saveConfig({ apiKey: key, base: args.base !== DEFAULT_BASE ? args.base : undefined });
220
+ console.log(green("✓") + ` Logged in as ${bold(acct.email || "your account")} ${dim("(" + (acct.role || "member") + ")")}`);
221
+ console.log(dim(`Key saved to ${CONFIG_PATH}`));
222
+ }
223
+
224
+ async function cmdWhoami(args) {
225
+ const key = requireKey();
226
+ const res = await request("GET", args.base, "/api/mcp/account", { key });
227
+ const a = need(res, "whoami");
228
+ if (args.json) return console.log(JSON.stringify(a, null, 2));
229
+ console.log(`${bold("Account")} ${a.email || "—"} ${dim("(" + (a.role || "member") + ")")}`);
230
+ console.log(`${bold("Scopes")} ${(a.scopes || []).join(", ") || "—"}`);
231
+ const u = a.usage || {};
232
+ const line = (label, k) => {
233
+ const x = u[k];
234
+ if (!x) return;
235
+ const lim = x.daily_limit == null ? "unlimited" : x.daily_limit;
236
+ console.log(` ${label.padEnd(8)} ${x.used ?? 0}/${lim} today`);
237
+ };
238
+ if (!u.unlimited) {
239
+ console.log(bold("Quota (today)"));
240
+ line("count", "count");
241
+ line("preview", "preview");
242
+ line("reveal", "reveal");
243
+ line("export", "export");
244
+ } else {
245
+ console.log(dim("Quota: unlimited"));
246
+ }
247
+ }
248
+
249
+ async function cmdCount(args) {
250
+ const key = requireKey();
251
+ const res = await request("GET", args.base, "/api/mcp/people/count", { key, query: args.filters });
252
+ const r = need(res, "count");
253
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
254
+ console.log(bold(Number(r.count).toLocaleString()) + dim(" matching contacts"));
255
+ }
256
+
257
+ async function cmdSearch(args) {
258
+ const key = requireKey();
259
+ const query = { ...args.filters, limit: args.limit || 5 };
260
+ const res = await request("GET", args.base, "/api/mcp/people/preview", { key, query });
261
+ const r = need(res, "search");
262
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
263
+ console.log(dim(`${Number(r.total).toLocaleString()} total · showing ${r.returned} (details redacted — use \`reveal\` or \`export\`)`));
264
+ for (const p of r.results || []) {
265
+ const who = [p.name, p.title].filter(Boolean).join(" · ");
266
+ const where = [p.company || p.company_name, p.country].filter(Boolean).join(", ");
267
+ console.log(` ${bold(who || "—")}${where ? dim(" " + where) : ""}`);
268
+ }
269
+ }
270
+
271
+ async function cmdReveal(args) {
272
+ const key = requireKey();
273
+ const limit = args.limit || 10;
274
+ if (!args.yes && !args.json && process.stdin.isTTY) {
275
+ const ans = await prompt(`Reveal up to ${bold(limit)} contacts? This uses your quota/credits. [y/N] `);
276
+ if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
277
+ }
278
+ const query = { ...args.filters, limit };
279
+ const res = await request("GET", args.base, "/api/mcp/people/reveal", { key, query });
280
+ const r = need(res, "reveal");
281
+ if (args.json) return console.log(JSON.stringify(r, null, 2));
282
+ console.log(dim(`${Number(r.total).toLocaleString()} total · revealed ${r.returned}`));
283
+ for (const p of r.results || []) {
284
+ const who = [p.name, p.title].filter(Boolean).join(" · ");
285
+ console.log(` ${bold(who || "—")}`);
286
+ const bits = [p.email && cyan(p.email), p.phone, p.linkedin_url, [p.company || p.company_name, p.country].filter(Boolean).join(", ")].filter(Boolean);
287
+ if (bits.length) console.log(" " + bits.join(dim(" · ")));
288
+ }
289
+ }
290
+
291
+ async function cmdExport(args) {
292
+ const key = requireKey();
293
+ const limit = args.limit || 1000;
294
+ const dest = args.output || "argorant-leads.csv";
295
+ if (!args.yes && !args.json && process.stdin.isTTY) {
296
+ const ans = await prompt(`Export up to ${bold(limit)} verified contacts to ${bold(dest)}? Uses quota/credits. [y/N] `);
297
+ if (!/^y(es)?$/i.test(ans)) return console.log(dim("aborted."));
298
+ }
299
+ const create = await request("POST", args.base, "/api/mcp/exports/create", { key, body: { limit, filters: args.filters } });
300
+ const job = need(create, "export");
301
+ const statusPath = job.status_api_path || (job.job_id ? `/api/mcp/exports/${job.job_id}` : null);
302
+ if (!statusPath) {
303
+ if (args.json) return console.log(JSON.stringify(job, null, 2));
304
+ return console.log("Export queued. " + JSON.stringify(job));
305
+ }
306
+ if (!args.json) process.stdout.write(dim("Export queued — verifying & building CSV"));
307
+ let downloadPath = job.download_api_path || null;
308
+ const started = Date.now();
309
+ // Poll until the job reports a terminal state.
310
+ /* eslint-disable no-constant-condition */
311
+ while (true) {
312
+ await new Promise((r) => setTimeout(r, 2500));
313
+ const st = await request("GET", args.base, statusPath, { key });
314
+ const s = need(st, "export status");
315
+ const status = (s.status || "").toLowerCase();
316
+ if (!args.json) process.stdout.write(".");
317
+ if (s.download_api_path) downloadPath = s.download_api_path;
318
+ if (["completed", "done", "ready", "succeeded"].includes(status) || s.downloadable) {
319
+ downloadPath = downloadPath || `/api/mcp/exports/${job.job_id}/download`;
320
+ break;
321
+ }
322
+ if (["failed", "error", "cancelled", "canceled"].includes(status)) die(`\nexport ${status}.`);
323
+ if (Date.now() - started > 1000 * 60 * 20) die("\nexport timed out after 20 minutes.");
324
+ }
325
+ if (!args.json) process.stdout.write("\n");
326
+ if (!downloadPath) {
327
+ if (args.json) return console.log(JSON.stringify(job, null, 2));
328
+ return console.log("Export ready but no download path returned. Check `argorant export-list`.");
329
+ }
330
+ await downloadTo(args.base, downloadPath, key, dest);
331
+ if (args.json) return console.log(JSON.stringify({ ok: true, file: dest, job_id: job.job_id }, null, 2));
332
+ const rows = (() => {
333
+ try {
334
+ return fs.readFileSync(dest, "utf8").split("\n").filter(Boolean).length - 1;
335
+ } catch {
336
+ return null;
337
+ }
338
+ })();
339
+ console.log(green("✓") + ` Saved ${rows != null ? bold(rows.toLocaleString()) + " rows → " : ""}${bold(dest)}`);
340
+ }
341
+
342
+ function help() {
343
+ const p = bold("argorant");
344
+ console.log(`
345
+ ${bold("Argorant")} — verified B2B contacts from your terminal ${dim("v" + VERSION)}
346
+
347
+ ${bold("USAGE")}
348
+ ${p} <command> "<query>" [filters]
349
+
350
+ ${bold("COMMANDS")}
351
+ ${cyan("login")} [key] Save an API key (or set ARGORANT_API_KEY)
352
+ ${cyan("whoami")} Account, scopes, and daily quota
353
+ ${cyan("count")} "<query>" Count matching contacts ${dim("(free)")}
354
+ ${cyan("search")} "<query>" -n 10 Preview matches, details redacted ${dim("(free)")}
355
+ ${cyan("reveal")} "<query>" -n 25 Reveal full contact details ${dim("(uses quota)")}
356
+ ${cyan("export")} "<query>" -n 1000 -o leads.csv Verified CSV export ${dim("(uses quota)")}
357
+
358
+ ${bold("FILTERS")}
359
+ --title <t> --seniority <s> --department <d>
360
+ --industry <i> --country <c> --state <s> --city <c>
361
+ --company <name> --domain <domain> --verify-status <v>
362
+ --has-phone --has-linkedin --has-email
363
+
364
+ ${bold("OPTIONS")}
365
+ -n, --limit <n> Max rows -o, --output <file> CSV path (export)
366
+ --json Raw JSON output -y, --yes Skip confirmations
367
+ --base <url> Override API base (or ARGORANT_API_BASE)
368
+
369
+ ${bold("EXAMPLES")}
370
+ ${p} count "fintech CFOs in germany"
371
+ ${p} search "heads of procurement" --country Germany -n 10
372
+ ${p} export --industry fintech --title CFO --country Germany -n 500 -o cfos.csv
373
+
374
+ Docs: ${cyan("https://argorant.com/docs/cli")}
375
+ `);
376
+ }
377
+
378
+ async function main() {
379
+ const argv = process.argv.slice(2);
380
+ const cmd = argv[0];
381
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") return help();
382
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") return console.log(VERSION);
383
+ const args = parseArgs(argv.slice(1));
384
+ // Allow a saved non-default base from login.
385
+ if (args.base === DEFAULT_BASE) {
386
+ const saved = loadConfig().base;
387
+ if (saved && !process.env.ARGORANT_API_BASE) args.base = saved;
388
+ }
389
+ const table = {
390
+ login: cmdLogin,
391
+ whoami: cmdWhoami,
392
+ count: cmdCount,
393
+ search: cmdSearch,
394
+ reveal: cmdReveal,
395
+ export: cmdExport,
396
+ };
397
+ const fn = table[cmd];
398
+ if (!fn) die(`unknown command: ${cmd}\nRun \`argorant help\` for usage.`);
399
+ try {
400
+ await fn(args);
401
+ } catch (e) {
402
+ die(e && e.message ? e.message : String(e));
403
+ }
404
+ }
405
+
406
+ main();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "argorant",
3
+ "version": "0.1.0",
4
+ "description": "Search, count, reveal, and export verified B2B contacts from the Argorant database — from your terminal, scripts, or coding agent.",
5
+ "bin": {
6
+ "argorant": "bin/argorant.js"
7
+ },
8
+ "type": "commonjs",
9
+ "engines": {
10
+ "node": ">=16"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "argorant",
18
+ "b2b",
19
+ "leads",
20
+ "prospecting",
21
+ "email-verification",
22
+ "sales",
23
+ "contacts",
24
+ "cli"
25
+ ],
26
+ "homepage": "https://argorant.com/docs/cli",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://argorant.com/docs/cli"
30
+ },
31
+ "license": "MIT"
32
+ }