kingminos 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +107 -2
- package/dist/kingminos.js +1823 -0
- package/package.json +38 -4
|
@@ -0,0 +1,1823 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/kingminos.ts
|
|
4
|
+
import { readFileSync, realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/sanitize.ts
|
|
8
|
+
function stripSafeToWrite(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map(stripSafeToWrite);
|
|
11
|
+
}
|
|
12
|
+
if (value !== null && typeof value === "object") {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [key, child] of Object.entries(value)) {
|
|
15
|
+
if (key === "safe_to_write") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
out[key] = stripSafeToWrite(child);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/client.ts
|
|
26
|
+
async function apiRequest(input) {
|
|
27
|
+
const headers = new Headers(input.headers);
|
|
28
|
+
headers.set("x-kingminos-client", "cli");
|
|
29
|
+
if (input.token) {
|
|
30
|
+
headers.set("Authorization", `Bearer ${input.token}`);
|
|
31
|
+
}
|
|
32
|
+
let body;
|
|
33
|
+
if (input.body !== void 0) {
|
|
34
|
+
headers.set("content-type", "application/json");
|
|
35
|
+
body = JSON.stringify(input.body);
|
|
36
|
+
}
|
|
37
|
+
const response = await input.fetch(input.url, {
|
|
38
|
+
method: input.method,
|
|
39
|
+
headers,
|
|
40
|
+
body
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
status: response.status,
|
|
44
|
+
contentType: response.headers.get("content-type") ?? "",
|
|
45
|
+
text: await response.text()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function formatBody(text, contentType) {
|
|
49
|
+
const trimmed = text.replace(/^\uFEFF/, "");
|
|
50
|
+
if (trimmed === "") {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
const jsonLike = contentType.includes("json") || looksLikeJson(trimmed);
|
|
54
|
+
if (jsonLike) {
|
|
55
|
+
try {
|
|
56
|
+
return `${JSON.stringify(stripSafeToWrite(JSON.parse(trimmed)), null, 2)}
|
|
57
|
+
`;
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return trimmed.endsWith("\n") ? trimmed : `${trimmed}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
function looksLikeJson(text) {
|
|
65
|
+
const start = text.trimStart()[0];
|
|
66
|
+
return start === "{" || start === "[";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/const.ts
|
|
70
|
+
var DEFAULT_BASE_URL = "https://api.kingminos.com";
|
|
71
|
+
var TOKEN_ENV = "KINGMINOS_API_KEY";
|
|
72
|
+
var BASE_URL_ENV = "KINGMINOS_BASE_URL";
|
|
73
|
+
var BYOK_PROVIDERS = [
|
|
74
|
+
"zoominfo",
|
|
75
|
+
"leadmagic",
|
|
76
|
+
"findymail",
|
|
77
|
+
"wiza",
|
|
78
|
+
"aiark",
|
|
79
|
+
"builtwith",
|
|
80
|
+
"prospeo",
|
|
81
|
+
"apollo",
|
|
82
|
+
"companyurlfinder",
|
|
83
|
+
"peopledatalabs",
|
|
84
|
+
"hginsights",
|
|
85
|
+
"brandfetch",
|
|
86
|
+
"enrichcrm",
|
|
87
|
+
"lemlist",
|
|
88
|
+
"snovio",
|
|
89
|
+
"cognism",
|
|
90
|
+
"lusha",
|
|
91
|
+
"starbridge",
|
|
92
|
+
"clay",
|
|
93
|
+
"databar",
|
|
94
|
+
"quickenrich",
|
|
95
|
+
"getleads",
|
|
96
|
+
"blitzapi"
|
|
97
|
+
];
|
|
98
|
+
function isByokProvider(value) {
|
|
99
|
+
return BYOK_PROVIDERS.includes(value);
|
|
100
|
+
}
|
|
101
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["help", "h", "dry-run", "skip-cache", "json"]);
|
|
102
|
+
var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["token", "base-url", "help", "h", "json"]);
|
|
103
|
+
var EXIT = {
|
|
104
|
+
ok: 0,
|
|
105
|
+
usage: 1,
|
|
106
|
+
unauthorized: 2,
|
|
107
|
+
payment: 3,
|
|
108
|
+
rateLimited: 4,
|
|
109
|
+
server: 5,
|
|
110
|
+
http: 6
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/usage.ts
|
|
114
|
+
var UsageError = class extends Error {
|
|
115
|
+
constructor(message) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = "UsageError";
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function assertNever(value, message) {
|
|
121
|
+
throw new Error(message ?? `unexpected value: ${String(value)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/parse.ts
|
|
125
|
+
var COMMAND_FLAGS = {
|
|
126
|
+
health: /* @__PURE__ */ new Set(),
|
|
127
|
+
openapi: /* @__PURE__ */ new Set(),
|
|
128
|
+
providers: /* @__PURE__ */ new Set(),
|
|
129
|
+
capabilities: /* @__PURE__ */ new Set(),
|
|
130
|
+
recipes: /* @__PURE__ */ new Set(),
|
|
131
|
+
batch: /* @__PURE__ */ new Set(["file", "priority", "explain", "schema", "dry-run", "path"]),
|
|
132
|
+
"company.resolve": /* @__PURE__ */ new Set([
|
|
133
|
+
"body",
|
|
134
|
+
"email",
|
|
135
|
+
"name",
|
|
136
|
+
"domain",
|
|
137
|
+
"website",
|
|
138
|
+
"dry-run",
|
|
139
|
+
"external-ref",
|
|
140
|
+
"routing",
|
|
141
|
+
"compliance",
|
|
142
|
+
"schema-version",
|
|
143
|
+
"idempotency-key",
|
|
144
|
+
"explain",
|
|
145
|
+
"schema",
|
|
146
|
+
"max-provider-credits",
|
|
147
|
+
"path",
|
|
148
|
+
"preset",
|
|
149
|
+
"priority"
|
|
150
|
+
]),
|
|
151
|
+
"company.domain": /* @__PURE__ */ new Set([
|
|
152
|
+
"body",
|
|
153
|
+
"email",
|
|
154
|
+
"name",
|
|
155
|
+
"company-name",
|
|
156
|
+
"domain",
|
|
157
|
+
"website",
|
|
158
|
+
"city",
|
|
159
|
+
"state",
|
|
160
|
+
"country",
|
|
161
|
+
"postal-code",
|
|
162
|
+
"phone",
|
|
163
|
+
"linkedin-url",
|
|
164
|
+
"family-policy",
|
|
165
|
+
"path",
|
|
166
|
+
"preset",
|
|
167
|
+
"skip-cache",
|
|
168
|
+
"dry-run",
|
|
169
|
+
"external-ref",
|
|
170
|
+
"routing",
|
|
171
|
+
"compliance",
|
|
172
|
+
"schema-version",
|
|
173
|
+
"idempotency-key",
|
|
174
|
+
"explain",
|
|
175
|
+
"schema",
|
|
176
|
+
"max-provider-credits",
|
|
177
|
+
"cache",
|
|
178
|
+
"cache-control",
|
|
179
|
+
"priority"
|
|
180
|
+
]),
|
|
181
|
+
"company.b2b_social": /* @__PURE__ */ new Set([
|
|
182
|
+
"body",
|
|
183
|
+
"email",
|
|
184
|
+
"name",
|
|
185
|
+
"domain",
|
|
186
|
+
"website",
|
|
187
|
+
"linkedin-url",
|
|
188
|
+
"hint",
|
|
189
|
+
"country",
|
|
190
|
+
"path",
|
|
191
|
+
"preset",
|
|
192
|
+
"dry-run",
|
|
193
|
+
"external-ref",
|
|
194
|
+
"routing",
|
|
195
|
+
"compliance",
|
|
196
|
+
"idempotency-key",
|
|
197
|
+
"explain",
|
|
198
|
+
"schema",
|
|
199
|
+
"max-provider-credits",
|
|
200
|
+
"priority"
|
|
201
|
+
]),
|
|
202
|
+
"company.icon": /* @__PURE__ */ new Set([
|
|
203
|
+
"body",
|
|
204
|
+
"email",
|
|
205
|
+
"name",
|
|
206
|
+
"domain",
|
|
207
|
+
"website",
|
|
208
|
+
"dry-run",
|
|
209
|
+
"external-ref",
|
|
210
|
+
"compliance",
|
|
211
|
+
"idempotency-key",
|
|
212
|
+
"explain",
|
|
213
|
+
"schema"
|
|
214
|
+
]),
|
|
215
|
+
"company.description": /* @__PURE__ */ new Set([
|
|
216
|
+
"body",
|
|
217
|
+
"email",
|
|
218
|
+
"name",
|
|
219
|
+
"domain",
|
|
220
|
+
"website",
|
|
221
|
+
"path",
|
|
222
|
+
"preset",
|
|
223
|
+
"max-chars",
|
|
224
|
+
"skip-cache",
|
|
225
|
+
"dry-run",
|
|
226
|
+
"external-ref",
|
|
227
|
+
"routing",
|
|
228
|
+
"compliance",
|
|
229
|
+
"idempotency-key",
|
|
230
|
+
"explain",
|
|
231
|
+
"schema",
|
|
232
|
+
"max-provider-credits",
|
|
233
|
+
"cache",
|
|
234
|
+
"cache-control",
|
|
235
|
+
"priority"
|
|
236
|
+
]),
|
|
237
|
+
"company.hierarchy": /* @__PURE__ */ new Set([
|
|
238
|
+
"body",
|
|
239
|
+
"email",
|
|
240
|
+
"name",
|
|
241
|
+
"domain",
|
|
242
|
+
"website",
|
|
243
|
+
"country",
|
|
244
|
+
"path",
|
|
245
|
+
"preset",
|
|
246
|
+
"dry-run",
|
|
247
|
+
"external-ref",
|
|
248
|
+
"routing",
|
|
249
|
+
"compliance",
|
|
250
|
+
"idempotency-key",
|
|
251
|
+
"explain",
|
|
252
|
+
"schema",
|
|
253
|
+
"max-provider-credits",
|
|
254
|
+
"priority"
|
|
255
|
+
]),
|
|
256
|
+
"person.verify_employment": /* @__PURE__ */ new Set([
|
|
257
|
+
"body",
|
|
258
|
+
"email",
|
|
259
|
+
"first-name",
|
|
260
|
+
"last-name",
|
|
261
|
+
"linkedin-url",
|
|
262
|
+
"expected-company",
|
|
263
|
+
"expected-company-domain",
|
|
264
|
+
"expected-company-name",
|
|
265
|
+
"expected-company-website",
|
|
266
|
+
"dry-run",
|
|
267
|
+
"external-ref",
|
|
268
|
+
"routing",
|
|
269
|
+
"compliance",
|
|
270
|
+
"idempotency-key",
|
|
271
|
+
"explain",
|
|
272
|
+
"schema",
|
|
273
|
+
"max-provider-credits",
|
|
274
|
+
"priority"
|
|
275
|
+
]),
|
|
276
|
+
"person.language": /* @__PURE__ */ new Set([
|
|
277
|
+
"body",
|
|
278
|
+
"email",
|
|
279
|
+
"first-name",
|
|
280
|
+
"last-name",
|
|
281
|
+
"linkedin-url",
|
|
282
|
+
"skip-cache",
|
|
283
|
+
"dry-run",
|
|
284
|
+
"external-ref",
|
|
285
|
+
"compliance",
|
|
286
|
+
"idempotency-key",
|
|
287
|
+
"explain",
|
|
288
|
+
"schema",
|
|
289
|
+
"priority"
|
|
290
|
+
]),
|
|
291
|
+
"person.phone": /* @__PURE__ */ new Set([
|
|
292
|
+
"body",
|
|
293
|
+
"email",
|
|
294
|
+
"first-name",
|
|
295
|
+
"last-name",
|
|
296
|
+
"linkedin-url",
|
|
297
|
+
"domain",
|
|
298
|
+
"phone-types",
|
|
299
|
+
"max-results",
|
|
300
|
+
"format",
|
|
301
|
+
"skip-cache",
|
|
302
|
+
"dry-run",
|
|
303
|
+
"external-ref",
|
|
304
|
+
"compliance",
|
|
305
|
+
"idempotency-key",
|
|
306
|
+
"explain",
|
|
307
|
+
"schema",
|
|
308
|
+
"cache",
|
|
309
|
+
"cache-control",
|
|
310
|
+
"priority"
|
|
311
|
+
]),
|
|
312
|
+
"person.title": /* @__PURE__ */ new Set([
|
|
313
|
+
"body",
|
|
314
|
+
"email",
|
|
315
|
+
"first-name",
|
|
316
|
+
"last-name",
|
|
317
|
+
"linkedin-url",
|
|
318
|
+
"company-domain",
|
|
319
|
+
"fields",
|
|
320
|
+
"path",
|
|
321
|
+
"routing",
|
|
322
|
+
"skip-cache",
|
|
323
|
+
"dry-run",
|
|
324
|
+
"external-ref",
|
|
325
|
+
"compliance",
|
|
326
|
+
"idempotency-key",
|
|
327
|
+
"explain",
|
|
328
|
+
"schema",
|
|
329
|
+
"cache",
|
|
330
|
+
"cache-control",
|
|
331
|
+
"priority"
|
|
332
|
+
]),
|
|
333
|
+
"person.name": /* @__PURE__ */ new Set([
|
|
334
|
+
"body",
|
|
335
|
+
"email",
|
|
336
|
+
"linkedin-url",
|
|
337
|
+
"path",
|
|
338
|
+
"routing",
|
|
339
|
+
"skip-cache",
|
|
340
|
+
"dry-run",
|
|
341
|
+
"external-ref",
|
|
342
|
+
"compliance",
|
|
343
|
+
"idempotency-key",
|
|
344
|
+
"explain",
|
|
345
|
+
"schema",
|
|
346
|
+
"cache",
|
|
347
|
+
"cache-control",
|
|
348
|
+
"priority"
|
|
349
|
+
]),
|
|
350
|
+
"person.b2b_social": /* @__PURE__ */ new Set([
|
|
351
|
+
"body",
|
|
352
|
+
"email",
|
|
353
|
+
"first-name",
|
|
354
|
+
"last-name",
|
|
355
|
+
"linkedin-url",
|
|
356
|
+
"path",
|
|
357
|
+
"routing",
|
|
358
|
+
"skip-cache",
|
|
359
|
+
"dry-run",
|
|
360
|
+
"external-ref",
|
|
361
|
+
"compliance",
|
|
362
|
+
"idempotency-key",
|
|
363
|
+
"explain",
|
|
364
|
+
"schema",
|
|
365
|
+
"cache",
|
|
366
|
+
"cache-control",
|
|
367
|
+
"priority"
|
|
368
|
+
]),
|
|
369
|
+
"runs.get": /* @__PURE__ */ new Set(["explain", "schema"]),
|
|
370
|
+
"credentials.put": /* @__PURE__ */ new Set(["body", "api-key", "client-id", "client-secret"]),
|
|
371
|
+
"credentials.delete": /* @__PURE__ */ new Set(),
|
|
372
|
+
"subjects.delete": /* @__PURE__ */ new Set(),
|
|
373
|
+
help: /* @__PURE__ */ new Set()
|
|
374
|
+
};
|
|
375
|
+
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
376
|
+
...GLOBAL_FLAGS,
|
|
377
|
+
...BOOLEAN_FLAGS,
|
|
378
|
+
...Object.values(COMMAND_FLAGS).flatMap((names) => [...names])
|
|
379
|
+
]);
|
|
380
|
+
function parseArgv(argv) {
|
|
381
|
+
const positionals = [];
|
|
382
|
+
const flags = {};
|
|
383
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
384
|
+
const arg = argv[i];
|
|
385
|
+
if (arg === void 0) {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
if (arg === "--") {
|
|
389
|
+
positionals.push(...argv.slice(i + 1));
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
if (arg.startsWith("--")) {
|
|
393
|
+
const eq = arg.indexOf("=");
|
|
394
|
+
const rawName = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
395
|
+
const name = rawName.trim();
|
|
396
|
+
if (!name) {
|
|
397
|
+
throw new UsageError("empty flag");
|
|
398
|
+
}
|
|
399
|
+
if (!KNOWN_FLAGS.has(name)) {
|
|
400
|
+
throw new UsageError(`unknown flag --${name}`);
|
|
401
|
+
}
|
|
402
|
+
if (BOOLEAN_FLAGS.has(name)) {
|
|
403
|
+
if (eq !== -1) {
|
|
404
|
+
throw new UsageError(`--${name} does not take a value`);
|
|
405
|
+
}
|
|
406
|
+
flags[name] = true;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const value = eq === -1 ? argv[i + 1] : arg.slice(eq + 1);
|
|
410
|
+
if (eq === -1) {
|
|
411
|
+
i += 1;
|
|
412
|
+
}
|
|
413
|
+
if (value === void 0 || eq === -1 && value.startsWith("--")) {
|
|
414
|
+
throw new UsageError(`--${name} requires a value`);
|
|
415
|
+
}
|
|
416
|
+
flags[name] = value;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (arg === "-h") {
|
|
420
|
+
flags.help = true;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (arg.startsWith("-")) {
|
|
424
|
+
throw new UsageError(`unknown flag ${arg}`);
|
|
425
|
+
}
|
|
426
|
+
positionals.push(arg);
|
|
427
|
+
}
|
|
428
|
+
return { positionals, flags };
|
|
429
|
+
}
|
|
430
|
+
function resolveCommand(positionals) {
|
|
431
|
+
if (positionals.length === 0) {
|
|
432
|
+
return { name: "help" };
|
|
433
|
+
}
|
|
434
|
+
const [head, mid, tail, ...rest] = positionals;
|
|
435
|
+
if (rest.length > 0) {
|
|
436
|
+
throw new UsageError(`unexpected arguments: ${rest.join(" ")}`);
|
|
437
|
+
}
|
|
438
|
+
switch (head) {
|
|
439
|
+
case "help":
|
|
440
|
+
return { name: "help", topic: mid };
|
|
441
|
+
case "health":
|
|
442
|
+
case "openapi":
|
|
443
|
+
case "providers":
|
|
444
|
+
case "capabilities":
|
|
445
|
+
case "recipes":
|
|
446
|
+
if (mid !== void 0) {
|
|
447
|
+
throw new UsageError(`${head} does not take arguments`);
|
|
448
|
+
}
|
|
449
|
+
return { name: head };
|
|
450
|
+
case "batch":
|
|
451
|
+
return resolveBatch(mid, tail);
|
|
452
|
+
case "company":
|
|
453
|
+
return resolveCompany(mid, tail);
|
|
454
|
+
case "person":
|
|
455
|
+
if (mid !== "verify-employment" && mid !== "language" && mid !== "phone" && mid !== "title" && mid !== "name" && mid !== "b2b-social") {
|
|
456
|
+
throw new UsageError(
|
|
457
|
+
"person commands: verify-employment | language | phone | title | name | b2b-social"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (tail !== void 0) {
|
|
461
|
+
throw new UsageError(`person ${mid} does not take positional arguments`);
|
|
462
|
+
}
|
|
463
|
+
if (mid === "language") {
|
|
464
|
+
return { name: "person.language" };
|
|
465
|
+
}
|
|
466
|
+
if (mid === "phone") {
|
|
467
|
+
return { name: "person.phone" };
|
|
468
|
+
}
|
|
469
|
+
if (mid === "title") {
|
|
470
|
+
return { name: "person.title" };
|
|
471
|
+
}
|
|
472
|
+
if (mid === "name") {
|
|
473
|
+
return { name: "person.name" };
|
|
474
|
+
}
|
|
475
|
+
if (mid === "b2b-social") {
|
|
476
|
+
return { name: "person.b2b_social" };
|
|
477
|
+
}
|
|
478
|
+
return { name: "person.verify_employment" };
|
|
479
|
+
case "runs":
|
|
480
|
+
if (mid !== "get" || !tail) {
|
|
481
|
+
throw new UsageError("runs commands: get <id>");
|
|
482
|
+
}
|
|
483
|
+
return { name: "runs.get", id: tail };
|
|
484
|
+
case "credentials":
|
|
485
|
+
if (mid !== "put" && mid !== "delete" || !tail) {
|
|
486
|
+
throw new UsageError("credentials commands: put <provider> | delete <provider>");
|
|
487
|
+
}
|
|
488
|
+
return mid === "put" ? { name: "credentials.put", provider: tail } : { name: "credentials.delete", provider: tail };
|
|
489
|
+
case "subjects":
|
|
490
|
+
if (mid !== "delete" || !tail) {
|
|
491
|
+
throw new UsageError("subjects commands: delete <subject_key>");
|
|
492
|
+
}
|
|
493
|
+
return { name: "subjects.delete", subjectKey: tail };
|
|
494
|
+
default:
|
|
495
|
+
throw new UsageError(`unknown command ${head}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function resolveBatch(mid, tail) {
|
|
499
|
+
if (mid === "company") {
|
|
500
|
+
const inner = resolveCompany(tail, void 0);
|
|
501
|
+
switch (inner.name) {
|
|
502
|
+
case "company.resolve":
|
|
503
|
+
case "company.domain":
|
|
504
|
+
case "company.hierarchy":
|
|
505
|
+
case "company.b2b_social":
|
|
506
|
+
case "company.icon":
|
|
507
|
+
case "company.description":
|
|
508
|
+
return { name: "batch", capability: inner.name };
|
|
509
|
+
default:
|
|
510
|
+
throw new UsageError(
|
|
511
|
+
"batch company commands: resolve | domain | hierarchy | b2b-social | icon | description"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (mid === "person") {
|
|
516
|
+
if (tail !== "verify-employment" && tail !== "language" && tail !== "phone" && tail !== "title" && tail !== "name" && tail !== "b2b-social") {
|
|
517
|
+
throw new UsageError(
|
|
518
|
+
"batch person commands: verify-employment | language | phone | title | name | b2b-social"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (tail === "language") {
|
|
522
|
+
return { name: "batch", capability: "person.language" };
|
|
523
|
+
}
|
|
524
|
+
if (tail === "phone") {
|
|
525
|
+
return { name: "batch", capability: "person.phone" };
|
|
526
|
+
}
|
|
527
|
+
if (tail === "title") {
|
|
528
|
+
return { name: "batch", capability: "person.title" };
|
|
529
|
+
}
|
|
530
|
+
if (tail === "name") {
|
|
531
|
+
return { name: "batch", capability: "person.name" };
|
|
532
|
+
}
|
|
533
|
+
if (tail === "b2b-social") {
|
|
534
|
+
return { name: "batch", capability: "person.b2b_social" };
|
|
535
|
+
}
|
|
536
|
+
return { name: "batch", capability: "person.verify_employment" };
|
|
537
|
+
}
|
|
538
|
+
throw new UsageError(
|
|
539
|
+
"batch commands: batch <company|person> <command> --file <jsonl> --priority speed|balance|accuracy|coverage"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
function resolveCompany(mid, tail) {
|
|
543
|
+
if (tail !== void 0) {
|
|
544
|
+
throw new UsageError(
|
|
545
|
+
"company <resolve|domain|hierarchy|b2b-social|icon|description> does not take positional arguments"
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
switch (mid) {
|
|
549
|
+
case "resolve":
|
|
550
|
+
return { name: "company.resolve" };
|
|
551
|
+
case "domain":
|
|
552
|
+
return { name: "company.domain" };
|
|
553
|
+
case "hierarchy":
|
|
554
|
+
return { name: "company.hierarchy" };
|
|
555
|
+
case "b2b-social":
|
|
556
|
+
return { name: "company.b2b_social" };
|
|
557
|
+
case "icon":
|
|
558
|
+
return { name: "company.icon" };
|
|
559
|
+
case "description":
|
|
560
|
+
return { name: "company.description" };
|
|
561
|
+
default:
|
|
562
|
+
throw new UsageError(
|
|
563
|
+
"company commands: resolve | domain | hierarchy | b2b-social | icon | description"
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function assertKnownFlags(commandName, flags) {
|
|
568
|
+
const allowed = COMMAND_FLAGS[commandName];
|
|
569
|
+
if (!allowed) {
|
|
570
|
+
throw new UsageError(`unknown command ${commandName}`);
|
|
571
|
+
}
|
|
572
|
+
for (const name of Object.keys(flags)) {
|
|
573
|
+
if (GLOBAL_FLAGS.has(name) || allowed.has(name)) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
throw new UsageError(`unknown flag --${name}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function flagString(flags, name) {
|
|
580
|
+
const value = flags[name];
|
|
581
|
+
if (value === void 0 || value === true) {
|
|
582
|
+
return void 0;
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
}
|
|
586
|
+
function flagBool(flags, name) {
|
|
587
|
+
return flags[name] === true ? true : void 0;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/priority.ts
|
|
591
|
+
var PRIORITY_HELP = "Before a bulk run, ask which path: speed, balance, accuracy, or coverage. Re-run with --priority speed|balance|accuracy|coverage. Max yield = coverage. company.resolve value is the one-step lookup, not a tier.";
|
|
592
|
+
function parsePriority(value) {
|
|
593
|
+
switch (value) {
|
|
594
|
+
case "speed":
|
|
595
|
+
case "balance":
|
|
596
|
+
case "accuracy":
|
|
597
|
+
case "coverage":
|
|
598
|
+
return value;
|
|
599
|
+
default:
|
|
600
|
+
throw new UsageError(PRIORITY_HELP);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function pathForPriority(priority) {
|
|
604
|
+
return priority;
|
|
605
|
+
}
|
|
606
|
+
function applyPriority(body, priority, _capability) {
|
|
607
|
+
const path = pathForPriority(priority);
|
|
608
|
+
const routing = body.routing && typeof body.routing === "object" && !Array.isArray(body.routing) ? body.routing : {};
|
|
609
|
+
return { ...body, routing: { ...routing, path } };
|
|
610
|
+
}
|
|
611
|
+
function formatDecisionNotes(body) {
|
|
612
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
613
|
+
return "";
|
|
614
|
+
}
|
|
615
|
+
const record = body;
|
|
616
|
+
const lines = [];
|
|
617
|
+
const confidence = record.answer?.confidence;
|
|
618
|
+
if (confidence === "high" || confidence === "medium" || confidence === "low") {
|
|
619
|
+
lines.push(`Confidence: ${confidence}`);
|
|
620
|
+
} else if (confidence === null) {
|
|
621
|
+
lines.push("Confidence: none");
|
|
622
|
+
}
|
|
623
|
+
const spent = record.usage?.credits?.spent;
|
|
624
|
+
if (typeof spent === "number") {
|
|
625
|
+
lines.push(`Credits spent: ${spent}`);
|
|
626
|
+
}
|
|
627
|
+
return lines.length > 0 ? `
|
|
628
|
+
${lines.join("\n")}
|
|
629
|
+
` : "";
|
|
630
|
+
}
|
|
631
|
+
function formatNextStepsHints(body) {
|
|
632
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
633
|
+
return "";
|
|
634
|
+
}
|
|
635
|
+
const steps = body.next_steps;
|
|
636
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
637
|
+
return "";
|
|
638
|
+
}
|
|
639
|
+
const lines = ["", "Next:"];
|
|
640
|
+
for (const raw of steps) {
|
|
641
|
+
if (!raw || typeof raw !== "object") {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const step = raw;
|
|
645
|
+
const credits = typeof step.est_credits === "number" ? ` (~${step.est_credits} credits)` : "";
|
|
646
|
+
const target = step.capability ? ` ${step.capability}` : "";
|
|
647
|
+
lines.push(`- ${step.action ?? "next"}${target}${credits}: ${step.reason ?? ""}`.trim());
|
|
648
|
+
if (step.suggested && typeof step.suggested === "object") {
|
|
649
|
+
lines.push(` suggested: ${JSON.stringify(step.suggested)}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return `${lines.join("\n")}
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/commands.ts
|
|
657
|
+
function buildRequest(command, flags, loader) {
|
|
658
|
+
switch (command.name) {
|
|
659
|
+
case "help":
|
|
660
|
+
throw new UsageError("help is not an HTTP call");
|
|
661
|
+
case "batch":
|
|
662
|
+
throw new UsageError("batch is not a single HTTP call");
|
|
663
|
+
case "health":
|
|
664
|
+
return { method: "GET", path: "/health", headers: {}, auth: false };
|
|
665
|
+
case "openapi":
|
|
666
|
+
return { method: "GET", path: "/openapi.yaml", headers: {}, auth: false };
|
|
667
|
+
case "providers":
|
|
668
|
+
return { method: "GET", path: "/v1/providers", headers: {}, auth: true };
|
|
669
|
+
case "capabilities":
|
|
670
|
+
return { method: "GET", path: "/v1/capabilities", headers: {}, auth: true };
|
|
671
|
+
case "recipes":
|
|
672
|
+
return { method: "GET", path: "/v1/recipes", headers: {}, auth: true };
|
|
673
|
+
case "company.resolve":
|
|
674
|
+
return capabilityPost("/v1/company/resolve", resolveBody(flags, loader), flags);
|
|
675
|
+
case "company.domain":
|
|
676
|
+
return capabilityPost("/v1/company/domain", domainBody(flags, loader), flags, {
|
|
677
|
+
cache: true
|
|
678
|
+
});
|
|
679
|
+
case "company.hierarchy":
|
|
680
|
+
return capabilityPost("/v1/company/hierarchy", hierarchyBody(flags, loader), flags);
|
|
681
|
+
case "company.b2b_social":
|
|
682
|
+
return capabilityPost("/v1/company/b2b-social", b2bSocialBody(flags, loader), flags);
|
|
683
|
+
case "company.icon":
|
|
684
|
+
return capabilityPost("/v1/company/icon", iconBody(flags, loader), flags);
|
|
685
|
+
case "company.description":
|
|
686
|
+
return capabilityPost("/v1/company/description", descriptionBody(flags, loader), flags, {
|
|
687
|
+
cache: true
|
|
688
|
+
});
|
|
689
|
+
case "person.verify_employment":
|
|
690
|
+
return capabilityPost("/v1/person/verify-employment", employmentBody(flags, loader), flags);
|
|
691
|
+
case "person.language":
|
|
692
|
+
return capabilityPost("/v1/person/language", languageBody(flags, loader), flags);
|
|
693
|
+
case "person.phone":
|
|
694
|
+
return capabilityPost("/v1/person/phone", phoneBody(flags, loader), flags, {
|
|
695
|
+
cache: true
|
|
696
|
+
});
|
|
697
|
+
case "person.title":
|
|
698
|
+
return capabilityPost("/v1/person/title", titleBody(flags, loader), flags, {
|
|
699
|
+
cache: true
|
|
700
|
+
});
|
|
701
|
+
case "person.name":
|
|
702
|
+
return capabilityPost("/v1/person/name", nameBody(flags, loader), flags, {
|
|
703
|
+
cache: true
|
|
704
|
+
});
|
|
705
|
+
case "person.b2b_social":
|
|
706
|
+
return capabilityPost("/v1/person/b2b-social", personB2bSocialBody(flags, loader), flags, {
|
|
707
|
+
cache: true
|
|
708
|
+
});
|
|
709
|
+
case "runs.get":
|
|
710
|
+
return {
|
|
711
|
+
method: "GET",
|
|
712
|
+
path: `/v1/runs/${encodeURIComponent(command.id)}`,
|
|
713
|
+
headers: explainSchemaHeaders(flags),
|
|
714
|
+
auth: true
|
|
715
|
+
};
|
|
716
|
+
case "credentials.put":
|
|
717
|
+
return {
|
|
718
|
+
method: "PUT",
|
|
719
|
+
path: `/v1/credentials/${encodeURIComponent(parseProvider(command.provider))}`,
|
|
720
|
+
headers: {},
|
|
721
|
+
body: credentialsPutBody(command.provider, flags, loader),
|
|
722
|
+
auth: true
|
|
723
|
+
};
|
|
724
|
+
case "credentials.delete":
|
|
725
|
+
return {
|
|
726
|
+
method: "DELETE",
|
|
727
|
+
path: `/v1/credentials/${encodeURIComponent(parseProvider(command.provider))}`,
|
|
728
|
+
headers: {},
|
|
729
|
+
auth: true
|
|
730
|
+
};
|
|
731
|
+
case "subjects.delete":
|
|
732
|
+
return {
|
|
733
|
+
method: "DELETE",
|
|
734
|
+
path: `/v1/subjects/${encodeURIComponent(command.subjectKey)}`,
|
|
735
|
+
headers: {},
|
|
736
|
+
auth: true
|
|
737
|
+
};
|
|
738
|
+
default:
|
|
739
|
+
return assertNever(command);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function capabilityPost(path, body, flags, options) {
|
|
743
|
+
return {
|
|
744
|
+
method: "POST",
|
|
745
|
+
path,
|
|
746
|
+
headers: {
|
|
747
|
+
...explainSchemaHeaders(flags),
|
|
748
|
+
...optionalHeader("Idempotency-Key", flagString(flags, "idempotency-key")),
|
|
749
|
+
...optionalHeader("X-Router-Max-Provider-Credits", flagString(flags, "max-provider-credits")),
|
|
750
|
+
...options?.cache ? {
|
|
751
|
+
...optionalHeader("X-Router-Cache", flagString(flags, "cache")),
|
|
752
|
+
...optionalHeader("Cache-Control", flagString(flags, "cache-control"))
|
|
753
|
+
} : {}
|
|
754
|
+
},
|
|
755
|
+
body,
|
|
756
|
+
auth: true
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function explainSchemaHeaders(flags) {
|
|
760
|
+
return {
|
|
761
|
+
...optionalHeader("X-Router-Explain", flagString(flags, "explain")),
|
|
762
|
+
...optionalHeader("X-Router-Schema", flagString(flags, "schema"))
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function optionalHeader(name, value) {
|
|
766
|
+
return value === void 0 ? {} : { [name]: value };
|
|
767
|
+
}
|
|
768
|
+
function resolveBody(flags, loader) {
|
|
769
|
+
const body = baseBody(flags, loader);
|
|
770
|
+
assignString(body, "email", flags);
|
|
771
|
+
assignString(body, "name", flags);
|
|
772
|
+
assignString(body, "domain", flags);
|
|
773
|
+
assignString(body, "website", flags);
|
|
774
|
+
assignCommon(body, flags);
|
|
775
|
+
assignJson(body, "routing", flags);
|
|
776
|
+
assignJson(body, "compliance", flags);
|
|
777
|
+
assignString(body, "schema_version", flags, "schema-version");
|
|
778
|
+
assignString(body, "path", flags);
|
|
779
|
+
assignString(body, "preset", flags);
|
|
780
|
+
return withPriority(body, flags, "company.resolve");
|
|
781
|
+
}
|
|
782
|
+
function domainBody(flags, loader) {
|
|
783
|
+
const body = baseBody(flags, loader);
|
|
784
|
+
assignString(body, "email", flags);
|
|
785
|
+
assignString(body, "name", flags);
|
|
786
|
+
assignString(body, "company_name", flags, "company-name");
|
|
787
|
+
assignString(body, "domain", flags);
|
|
788
|
+
assignString(body, "website", flags);
|
|
789
|
+
assignString(body, "city", flags);
|
|
790
|
+
assignString(body, "state", flags);
|
|
791
|
+
assignString(body, "country", flags);
|
|
792
|
+
assignString(body, "postal_code", flags, "postal-code");
|
|
793
|
+
assignString(body, "phone", flags);
|
|
794
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
795
|
+
assignString(body, "family_policy", flags, "family-policy");
|
|
796
|
+
assignString(body, "path", flags);
|
|
797
|
+
assignString(body, "preset", flags);
|
|
798
|
+
if (flagBool(flags, "skip-cache")) {
|
|
799
|
+
body.skip_cache = true;
|
|
800
|
+
}
|
|
801
|
+
assignCommon(body, flags);
|
|
802
|
+
assignJson(body, "routing", flags);
|
|
803
|
+
assignJson(body, "compliance", flags);
|
|
804
|
+
assignString(body, "schema_version", flags, "schema-version");
|
|
805
|
+
return withPriority(body, flags, "company.domain");
|
|
806
|
+
}
|
|
807
|
+
function b2bSocialBody(flags, loader) {
|
|
808
|
+
const body = baseBody(flags, loader);
|
|
809
|
+
assignString(body, "email", flags);
|
|
810
|
+
assignString(body, "name", flags);
|
|
811
|
+
assignString(body, "domain", flags);
|
|
812
|
+
assignString(body, "website", flags);
|
|
813
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
814
|
+
assignString(body, "hint", flags);
|
|
815
|
+
assignString(body, "country", flags);
|
|
816
|
+
assignString(body, "path", flags);
|
|
817
|
+
assignString(body, "preset", flags);
|
|
818
|
+
assignCommon(body, flags);
|
|
819
|
+
assignJson(body, "routing", flags);
|
|
820
|
+
assignJson(body, "compliance", flags);
|
|
821
|
+
return withPriority(body, flags, "company.b2b_social");
|
|
822
|
+
}
|
|
823
|
+
function iconBody(flags, loader) {
|
|
824
|
+
const body = baseBody(flags, loader);
|
|
825
|
+
assignString(body, "email", flags);
|
|
826
|
+
assignString(body, "name", flags);
|
|
827
|
+
assignString(body, "domain", flags);
|
|
828
|
+
assignString(body, "website", flags);
|
|
829
|
+
assignCommon(body, flags);
|
|
830
|
+
assignJson(body, "compliance", flags);
|
|
831
|
+
return body;
|
|
832
|
+
}
|
|
833
|
+
function descriptionBody(flags, loader) {
|
|
834
|
+
const body = baseBody(flags, loader);
|
|
835
|
+
assignString(body, "email", flags);
|
|
836
|
+
assignString(body, "name", flags);
|
|
837
|
+
assignString(body, "domain", flags);
|
|
838
|
+
assignString(body, "website", flags);
|
|
839
|
+
assignString(body, "path", flags);
|
|
840
|
+
assignString(body, "preset", flags);
|
|
841
|
+
assignInteger(body, "max_chars", flags, "max-chars", 80, 500);
|
|
842
|
+
if (flagBool(flags, "skip-cache")) {
|
|
843
|
+
body.skip_cache = true;
|
|
844
|
+
}
|
|
845
|
+
assignCommon(body, flags);
|
|
846
|
+
assignJson(body, "routing", flags);
|
|
847
|
+
assignJson(body, "compliance", flags);
|
|
848
|
+
return withPriority(body, flags, "company.description");
|
|
849
|
+
}
|
|
850
|
+
function hierarchyBody(flags, loader) {
|
|
851
|
+
const body = baseBody(flags, loader);
|
|
852
|
+
assignString(body, "email", flags);
|
|
853
|
+
assignString(body, "name", flags);
|
|
854
|
+
assignString(body, "domain", flags);
|
|
855
|
+
assignString(body, "website", flags);
|
|
856
|
+
assignString(body, "country", flags);
|
|
857
|
+
assignString(body, "path", flags);
|
|
858
|
+
assignString(body, "preset", flags);
|
|
859
|
+
assignCommon(body, flags);
|
|
860
|
+
assignJson(body, "routing", flags);
|
|
861
|
+
assignJson(body, "compliance", flags);
|
|
862
|
+
return withPriority(body, flags, "company.hierarchy");
|
|
863
|
+
}
|
|
864
|
+
function employmentBody(flags, loader) {
|
|
865
|
+
const body = baseBody(flags, loader);
|
|
866
|
+
assignString(body, "email", flags);
|
|
867
|
+
assignString(body, "first_name", flags, "first-name");
|
|
868
|
+
assignString(body, "last_name", flags, "last-name");
|
|
869
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
870
|
+
const expected = expectedCompany(flags);
|
|
871
|
+
if (expected) {
|
|
872
|
+
body.expected_company = expected;
|
|
873
|
+
}
|
|
874
|
+
assignCommon(body, flags);
|
|
875
|
+
assignJson(body, "routing", flags);
|
|
876
|
+
assignJson(body, "compliance", flags);
|
|
877
|
+
return withPriority(body, flags, "person.verify_employment");
|
|
878
|
+
}
|
|
879
|
+
function languageBody(flags, loader) {
|
|
880
|
+
const body = baseBody(flags, loader);
|
|
881
|
+
assignString(body, "email", flags);
|
|
882
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
883
|
+
assignString(body, "first_name", flags, "first-name");
|
|
884
|
+
assignString(body, "last_name", flags, "last-name");
|
|
885
|
+
if (flagBool(flags, "skip-cache")) {
|
|
886
|
+
body.skip_cache = true;
|
|
887
|
+
}
|
|
888
|
+
assignCommon(body, flags);
|
|
889
|
+
assignJson(body, "compliance", flags);
|
|
890
|
+
return withPriority(body, flags, "person.language");
|
|
891
|
+
}
|
|
892
|
+
function phoneBody(flags, loader) {
|
|
893
|
+
const body = baseBody(flags, loader);
|
|
894
|
+
assignString(body, "email", flags);
|
|
895
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
896
|
+
assignString(body, "first_name", flags, "first-name");
|
|
897
|
+
assignString(body, "last_name", flags, "last-name");
|
|
898
|
+
assignString(body, "domain", flags);
|
|
899
|
+
const phoneTypes = flagString(flags, "phone-types");
|
|
900
|
+
if (phoneTypes !== void 0) {
|
|
901
|
+
const types = phoneTypes.split(",").map((type) => type.trim()).filter(Boolean);
|
|
902
|
+
if (types.length > 0) {
|
|
903
|
+
body.phone_types = types;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const maxResults = Number(flagString(flags, "max-results"));
|
|
907
|
+
if (Number.isInteger(maxResults) && maxResults >= 1) {
|
|
908
|
+
body.max_results = maxResults;
|
|
909
|
+
}
|
|
910
|
+
assignString(body, "format", flags);
|
|
911
|
+
if (flagBool(flags, "skip-cache")) {
|
|
912
|
+
body.skip_cache = true;
|
|
913
|
+
}
|
|
914
|
+
assignCommon(body, flags);
|
|
915
|
+
assignJson(body, "compliance", flags);
|
|
916
|
+
return withPriority(body, flags, "person.phone");
|
|
917
|
+
}
|
|
918
|
+
function titleBody(flags, loader) {
|
|
919
|
+
const body = baseBody(flags, loader);
|
|
920
|
+
assignString(body, "email", flags);
|
|
921
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
922
|
+
assignString(body, "first_name", flags, "first-name");
|
|
923
|
+
assignString(body, "last_name", flags, "last-name");
|
|
924
|
+
assignString(body, "company_domain", flags, "company-domain");
|
|
925
|
+
const fields = flagString(flags, "fields");
|
|
926
|
+
if (fields !== void 0) {
|
|
927
|
+
const list = fields.split(",").map((field) => field.trim()).filter(Boolean);
|
|
928
|
+
if (list.length > 0) {
|
|
929
|
+
body.fields = list;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (flagBool(flags, "skip-cache")) {
|
|
933
|
+
body.skip_cache = true;
|
|
934
|
+
}
|
|
935
|
+
assignCommon(body, flags);
|
|
936
|
+
assignRouting(body, flags);
|
|
937
|
+
assignJson(body, "compliance", flags);
|
|
938
|
+
return withPriority(body, flags, "person.title");
|
|
939
|
+
}
|
|
940
|
+
function nameBody(flags, loader) {
|
|
941
|
+
const body = baseBody(flags, loader);
|
|
942
|
+
assignString(body, "email", flags);
|
|
943
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
944
|
+
if (flagBool(flags, "skip-cache")) {
|
|
945
|
+
body.skip_cache = true;
|
|
946
|
+
}
|
|
947
|
+
assignCommon(body, flags);
|
|
948
|
+
assignRouting(body, flags);
|
|
949
|
+
assignJson(body, "compliance", flags);
|
|
950
|
+
return withPriority(body, flags, "person.name");
|
|
951
|
+
}
|
|
952
|
+
function personB2bSocialBody(flags, loader) {
|
|
953
|
+
const body = baseBody(flags, loader);
|
|
954
|
+
assignString(body, "email", flags);
|
|
955
|
+
assignString(body, "linkedin_url", flags, "linkedin-url");
|
|
956
|
+
assignString(body, "first_name", flags, "first-name");
|
|
957
|
+
assignString(body, "last_name", flags, "last-name");
|
|
958
|
+
if (flagBool(flags, "skip-cache")) {
|
|
959
|
+
body.skip_cache = true;
|
|
960
|
+
}
|
|
961
|
+
assignCommon(body, flags);
|
|
962
|
+
assignRouting(body, flags);
|
|
963
|
+
assignJson(body, "compliance", flags);
|
|
964
|
+
return withPriority(body, flags, "person.b2b_social");
|
|
965
|
+
}
|
|
966
|
+
function assignRouting(body, flags) {
|
|
967
|
+
assignJson(body, "routing", flags);
|
|
968
|
+
const path = flagString(flags, "path");
|
|
969
|
+
if (path !== void 0) {
|
|
970
|
+
const routing = body.routing && typeof body.routing === "object" && !Array.isArray(body.routing) ? body.routing : {};
|
|
971
|
+
body.routing = { ...routing, path };
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function expectedCompany(flags) {
|
|
975
|
+
const fromJson = parseJsonFlag(flags, "expected-company");
|
|
976
|
+
const company = fromJson ? { ...fromJson } : {};
|
|
977
|
+
const domain = flagString(flags, "expected-company-domain");
|
|
978
|
+
const name = flagString(flags, "expected-company-name");
|
|
979
|
+
const website = flagString(flags, "expected-company-website");
|
|
980
|
+
if (domain !== void 0) {
|
|
981
|
+
company.domain = domain;
|
|
982
|
+
}
|
|
983
|
+
if (name !== void 0) {
|
|
984
|
+
company.name = name;
|
|
985
|
+
}
|
|
986
|
+
if (website !== void 0) {
|
|
987
|
+
company.website = website;
|
|
988
|
+
}
|
|
989
|
+
return Object.keys(company).length > 0 ? company : void 0;
|
|
990
|
+
}
|
|
991
|
+
function withPriority(body, flags, capability) {
|
|
992
|
+
const raw = flagString(flags, "priority");
|
|
993
|
+
if (raw === void 0) {
|
|
994
|
+
return body;
|
|
995
|
+
}
|
|
996
|
+
return applyPriority(body, parsePriority(raw), capability);
|
|
997
|
+
}
|
|
998
|
+
function assignCommon(body, flags) {
|
|
999
|
+
if (flagBool(flags, "dry-run")) {
|
|
1000
|
+
body.dry_run = true;
|
|
1001
|
+
}
|
|
1002
|
+
assignString(body, "external_ref", flags, "external-ref");
|
|
1003
|
+
}
|
|
1004
|
+
function assignString(body, field, flags, flagName = field.replaceAll("_", "-")) {
|
|
1005
|
+
const value = flagString(flags, flagName);
|
|
1006
|
+
if (value !== void 0) {
|
|
1007
|
+
body[field] = value;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function assignInteger(body, field, flags, flagName, min, max) {
|
|
1011
|
+
const raw = flagString(flags, flagName);
|
|
1012
|
+
if (raw === void 0) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const value = Number(raw);
|
|
1016
|
+
if (!Number.isInteger(value)) {
|
|
1017
|
+
throw new UsageError(`--${flagName} must be an integer`);
|
|
1018
|
+
}
|
|
1019
|
+
if (value < min || value > max) {
|
|
1020
|
+
throw new UsageError(`--${flagName} must be between ${min} and ${max}`);
|
|
1021
|
+
}
|
|
1022
|
+
body[field] = value;
|
|
1023
|
+
}
|
|
1024
|
+
function assignJson(body, field, flags) {
|
|
1025
|
+
const parsed = parseJsonFlag(flags, field);
|
|
1026
|
+
if (parsed) {
|
|
1027
|
+
body[field] = parsed;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function parseJsonFlag(flags, name) {
|
|
1031
|
+
const raw = flagString(flags, name);
|
|
1032
|
+
if (raw === void 0) {
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
return parseObjectJson(raw, `--${name}`);
|
|
1036
|
+
}
|
|
1037
|
+
function baseBody(flags, loader) {
|
|
1038
|
+
const raw = flagString(flags, "body");
|
|
1039
|
+
if (raw === void 0) {
|
|
1040
|
+
return {};
|
|
1041
|
+
}
|
|
1042
|
+
return { ...loadBodyObject(raw, loader) };
|
|
1043
|
+
}
|
|
1044
|
+
function loadBodyObject(raw, loader) {
|
|
1045
|
+
if (raw === "-") {
|
|
1046
|
+
return parseObjectJson(loader.stdin, "--body -");
|
|
1047
|
+
}
|
|
1048
|
+
if (raw.startsWith("@")) {
|
|
1049
|
+
const path = raw.slice(1);
|
|
1050
|
+
if (!path) {
|
|
1051
|
+
throw new UsageError("--body @file requires a path");
|
|
1052
|
+
}
|
|
1053
|
+
return parseObjectJson(loader.readFile(path), `--body @${path}`);
|
|
1054
|
+
}
|
|
1055
|
+
return parseObjectJson(raw, "--body");
|
|
1056
|
+
}
|
|
1057
|
+
function parseObjectJson(raw, label) {
|
|
1058
|
+
let parsed;
|
|
1059
|
+
try {
|
|
1060
|
+
parsed = JSON.parse(raw);
|
|
1061
|
+
} catch {
|
|
1062
|
+
throw new UsageError(`${label} is not valid JSON`);
|
|
1063
|
+
}
|
|
1064
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1065
|
+
throw new UsageError(`${label} must be a JSON object`);
|
|
1066
|
+
}
|
|
1067
|
+
return parsed;
|
|
1068
|
+
}
|
|
1069
|
+
function parseProvider(provider) {
|
|
1070
|
+
if (provider === "websearch") {
|
|
1071
|
+
throw new UsageError("websearch is house-only (byok_not_supported)");
|
|
1072
|
+
}
|
|
1073
|
+
if (!isByokProvider(provider)) {
|
|
1074
|
+
throw new UsageError(`unknown_provider: ${provider}`);
|
|
1075
|
+
}
|
|
1076
|
+
return provider;
|
|
1077
|
+
}
|
|
1078
|
+
function credentialsPutBody(provider, flags, loader) {
|
|
1079
|
+
const rawBody = flagString(flags, "body");
|
|
1080
|
+
const fromFile = rawBody ? loadBodyObject(rawBody, loader) : void 0;
|
|
1081
|
+
if (provider === "zoominfo") {
|
|
1082
|
+
const clientId = flagString(flags, "client-id") ?? stringField(fromFile, "clientId");
|
|
1083
|
+
const clientSecret = flagString(flags, "client-secret") ?? stringField(fromFile, "clientSecret");
|
|
1084
|
+
if (!clientId || !clientSecret) {
|
|
1085
|
+
throw new UsageError(
|
|
1086
|
+
"credentials put zoominfo requires --client-id and --client-secret (or --body { clientId, clientSecret })"
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return { clientId, clientSecret };
|
|
1090
|
+
}
|
|
1091
|
+
if (fromFile && flagString(flags, "api-key") === void 0 && (stringField(fromFile, "apiKey") || stringField(fromFile, "api_key"))) {
|
|
1092
|
+
return fromFile;
|
|
1093
|
+
}
|
|
1094
|
+
const apiKey = flagString(flags, "api-key") ?? stringField(fromFile, "apiKey") ?? stringField(fromFile, "api_key");
|
|
1095
|
+
if (!apiKey) {
|
|
1096
|
+
throw new UsageError(`credentials put ${provider} requires --api-key (or --body { apiKey })`);
|
|
1097
|
+
}
|
|
1098
|
+
return { apiKey };
|
|
1099
|
+
}
|
|
1100
|
+
function stringField(body, key) {
|
|
1101
|
+
const value = body?.[key];
|
|
1102
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/help.ts
|
|
1106
|
+
var ROOT_HELP = `King Minos CLI \u2014 KingMinos by OutboundSync
|
|
1107
|
+
|
|
1108
|
+
Usage:
|
|
1109
|
+
kingminos [--token <key>] [--base-url <url>] [--json] <command>
|
|
1110
|
+
|
|
1111
|
+
Auth:
|
|
1112
|
+
Pass a tenant Bearer with --token or ${TOKEN_ENV}.
|
|
1113
|
+
Tenant keys use the osr_ prefix. App-minted user tokens use km_.
|
|
1114
|
+
Mint a key at https://app.kingminos.com after sign-in.
|
|
1115
|
+
The CLI never stores or logs a key.
|
|
1116
|
+
|
|
1117
|
+
API:
|
|
1118
|
+
Default ${DEFAULT_BASE_URL}
|
|
1119
|
+
Override with --base-url or ${BASE_URL_ENV}
|
|
1120
|
+
|
|
1121
|
+
Global flags:
|
|
1122
|
+
--token <key> Bearer token (overrides ${TOKEN_ENV})
|
|
1123
|
+
--base-url <url> API host
|
|
1124
|
+
--json Print the response body only (no Next: hints)
|
|
1125
|
+
--help, -h This help
|
|
1126
|
+
|
|
1127
|
+
Commands (live OpenAPI only):
|
|
1128
|
+
health GET /health
|
|
1129
|
+
openapi GET /openapi.yaml
|
|
1130
|
+
providers GET /v1/providers
|
|
1131
|
+
capabilities GET /v1/capabilities
|
|
1132
|
+
recipes GET /v1/recipes
|
|
1133
|
+
company resolve POST /v1/company/resolve
|
|
1134
|
+
company domain POST /v1/company/domain
|
|
1135
|
+
company hierarchy POST /v1/company/hierarchy
|
|
1136
|
+
company b2b-social POST /v1/company/b2b-social
|
|
1137
|
+
company icon POST /v1/company/icon
|
|
1138
|
+
company description POST /v1/company/description
|
|
1139
|
+
person verify-employment POST /v1/person/verify-employment
|
|
1140
|
+
person language POST /v1/person/language
|
|
1141
|
+
person phone POST /v1/person/phone
|
|
1142
|
+
person title POST /v1/person/title
|
|
1143
|
+
person name POST /v1/person/name
|
|
1144
|
+
person b2b-social POST /v1/person/b2b-social
|
|
1145
|
+
runs get <id> GET /v1/runs/{id}
|
|
1146
|
+
credentials put <provider> PUT /v1/credentials/{provider}
|
|
1147
|
+
credentials delete <provider> DELETE /v1/credentials/{provider}
|
|
1148
|
+
subjects delete <subject_key> DELETE /v1/subjects/{subject_key}
|
|
1149
|
+
batch <company|person> <cmd> POST each JSONL/JSON-array row (requires --file --priority)
|
|
1150
|
+
|
|
1151
|
+
Tiers:
|
|
1152
|
+
--priority speed|balance|accuracy|coverage maps to routing.path.
|
|
1153
|
+
Ask which path before a bulk run. Max yield = coverage.
|
|
1154
|
+
company.resolve value is the one-step lookup, not a tier.
|
|
1155
|
+
|
|
1156
|
+
Credits:
|
|
1157
|
+
Billing is for delivered answers. Print usage.credits.spent from the response.
|
|
1158
|
+
The CLI does not hardcode prices.
|
|
1159
|
+
|
|
1160
|
+
Your Keys providers: ${BYOK_PROVIDERS.join(", ")}
|
|
1161
|
+
Websearch is house-only (no credentials command).
|
|
1162
|
+
LeadMagic, Wiza, Findymail, and AI Ark are house-key vendors. ZoomInfo, BuiltWith,
|
|
1163
|
+
Prospeo, Apollo, Company URL Finder, People Data Labs, HG Insights, Brandfetch,
|
|
1164
|
+
and Enrich-CRM stay BYOK.
|
|
1165
|
+
|
|
1166
|
+
Exit codes: 0 ok \xB7 1 usage \xB7 2 unauthorized (401) \xB7 3 out of credits (402)
|
|
1167
|
+
4 rate limited (429) \xB7 5 server (5xx) \xB7 6 other HTTP error
|
|
1168
|
+
|
|
1169
|
+
See kingminos <command> --help for request fields on that call.
|
|
1170
|
+
`;
|
|
1171
|
+
function commandHelp(command) {
|
|
1172
|
+
switch (command) {
|
|
1173
|
+
case "health":
|
|
1174
|
+
return `kingminos health
|
|
1175
|
+
|
|
1176
|
+
GET /health (no auth)
|
|
1177
|
+
`;
|
|
1178
|
+
case "openapi":
|
|
1179
|
+
return `kingminos openapi
|
|
1180
|
+
|
|
1181
|
+
GET /openapi.yaml (no auth)
|
|
1182
|
+
Prints the live OpenAPI document.
|
|
1183
|
+
`;
|
|
1184
|
+
case "providers":
|
|
1185
|
+
return `kingminos providers
|
|
1186
|
+
|
|
1187
|
+
GET /v1/providers
|
|
1188
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1189
|
+
`;
|
|
1190
|
+
case "capabilities":
|
|
1191
|
+
return `kingminos capabilities
|
|
1192
|
+
|
|
1193
|
+
GET /v1/capabilities
|
|
1194
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1195
|
+
`;
|
|
1196
|
+
case "recipes":
|
|
1197
|
+
return `kingminos recipes
|
|
1198
|
+
|
|
1199
|
+
GET /v1/recipes
|
|
1200
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1201
|
+
Machine-readable chaining workflows. Ask speed / balance / accuracy / coverage before a bulk run. Max yield = coverage.
|
|
1202
|
+
`;
|
|
1203
|
+
case "batch":
|
|
1204
|
+
return `kingminos batch <company|person> <command> --file <path|-> --priority speed|balance|accuracy|coverage
|
|
1205
|
+
|
|
1206
|
+
Client-side loop over JSONL (or a JSON array) of request bodies. Not a bulk API.
|
|
1207
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1208
|
+
Ask speed / balance / accuracy / coverage before a list run; --priority is required. Max yield = coverage.
|
|
1209
|
+
`;
|
|
1210
|
+
case "company.resolve":
|
|
1211
|
+
return `kingminos company resolve
|
|
1212
|
+
|
|
1213
|
+
POST /v1/company/resolve
|
|
1214
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1215
|
+
|
|
1216
|
+
Body fields: --email --name --domain --website --dry-run --external-ref
|
|
1217
|
+
--path --preset --priority speed|balance|accuracy|coverage
|
|
1218
|
+
--routing <json> --compliance <json> --schema-version 2
|
|
1219
|
+
--body <json|@file|->
|
|
1220
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1221
|
+
--schema 2 --max-provider-credits <n>
|
|
1222
|
+
`;
|
|
1223
|
+
case "company.domain":
|
|
1224
|
+
return `kingminos company domain
|
|
1225
|
+
|
|
1226
|
+
POST /v1/company/domain
|
|
1227
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1228
|
+
|
|
1229
|
+
Body fields: --email --name --company-name --domain --website
|
|
1230
|
+
--city --state --country --postal-code --phone --linkedin-url
|
|
1231
|
+
--family-policy collapse_brand|separate_regional
|
|
1232
|
+
--path --preset --priority speed|balance|accuracy|coverage
|
|
1233
|
+
--skip-cache --dry-run --external-ref
|
|
1234
|
+
--routing <json> --compliance <json> --schema-version 2
|
|
1235
|
+
--body <json|@file|->
|
|
1236
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1237
|
+
--max-provider-credits <n> --cache bypass
|
|
1238
|
+
--cache-control no-cache|no-store
|
|
1239
|
+
`;
|
|
1240
|
+
case "company.b2b_social":
|
|
1241
|
+
return `kingminos company b2b-social
|
|
1242
|
+
|
|
1243
|
+
POST /v1/company/b2b-social
|
|
1244
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1245
|
+
|
|
1246
|
+
Decide the company's B2B social page URL (result.linkedin_url).
|
|
1247
|
+
Pass --domain (required). --name is optional ranking only.
|
|
1248
|
+
--path speed is the bulk-list path (homepage-first, then ranked search).
|
|
1249
|
+
Delivered answers are billed; misses are not. See usage.credits.spent.
|
|
1250
|
+
Do not use company resolve for a B2B social page.
|
|
1251
|
+
|
|
1252
|
+
Body fields: --email --name --domain --website --linkedin-url
|
|
1253
|
+
--hint --country
|
|
1254
|
+
--path --preset --priority speed|balance|accuracy|coverage
|
|
1255
|
+
--dry-run --external-ref
|
|
1256
|
+
--routing <json> --compliance <json>
|
|
1257
|
+
--body <json|@file|->
|
|
1258
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1259
|
+
--schema 2 --max-provider-credits <n>
|
|
1260
|
+
`;
|
|
1261
|
+
case "company.icon":
|
|
1262
|
+
return `kingminos company icon
|
|
1263
|
+
|
|
1264
|
+
POST /v1/company/icon
|
|
1265
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1266
|
+
|
|
1267
|
+
Body fields: --email --name --domain --website
|
|
1268
|
+
--dry-run --external-ref
|
|
1269
|
+
--compliance <json>
|
|
1270
|
+
--body <json|@file|->
|
|
1271
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1272
|
+
--schema 2
|
|
1273
|
+
`;
|
|
1274
|
+
case "company.description":
|
|
1275
|
+
return `kingminos company description
|
|
1276
|
+
|
|
1277
|
+
POST /v1/company/description
|
|
1278
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1279
|
+
|
|
1280
|
+
Body fields: --email --name --domain --website
|
|
1281
|
+
--path --preset --priority speed|balance|accuracy|coverage
|
|
1282
|
+
--max-chars <80-500> --skip-cache --dry-run --external-ref
|
|
1283
|
+
--routing <json> --compliance <json>
|
|
1284
|
+
--body <json|@file|->
|
|
1285
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1286
|
+
--schema 2 --max-provider-credits <n>
|
|
1287
|
+
--cache bypass --cache-control no-cache|no-store
|
|
1288
|
+
`;
|
|
1289
|
+
case "company.hierarchy":
|
|
1290
|
+
return `kingminos company hierarchy
|
|
1291
|
+
|
|
1292
|
+
POST /v1/company/hierarchy
|
|
1293
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1294
|
+
|
|
1295
|
+
Body fields: --email --name --domain --website --country
|
|
1296
|
+
--path --preset --priority speed|balance|accuracy|coverage
|
|
1297
|
+
--dry-run --external-ref
|
|
1298
|
+
--routing <json> --compliance <json>
|
|
1299
|
+
--body <json|@file|->
|
|
1300
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1301
|
+
--schema 2 --max-provider-credits <n>
|
|
1302
|
+
`;
|
|
1303
|
+
case "person.verify_employment":
|
|
1304
|
+
return `kingminos person verify-employment
|
|
1305
|
+
|
|
1306
|
+
POST /v1/person/verify-employment
|
|
1307
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1308
|
+
|
|
1309
|
+
Body fields: --email --first-name --last-name --linkedin-url
|
|
1310
|
+
--expected-company <json>
|
|
1311
|
+
--expected-company-domain --expected-company-name
|
|
1312
|
+
--expected-company-website --priority speed|balance|accuracy|coverage
|
|
1313
|
+
--dry-run --external-ref --routing <json> --compliance <json>
|
|
1314
|
+
--body <json|@file|->
|
|
1315
|
+
Headers: --idempotency-key --explain default|full|minimal
|
|
1316
|
+
--schema 2 --max-provider-credits <n>
|
|
1317
|
+
`;
|
|
1318
|
+
case "person.language":
|
|
1319
|
+
return `kingminos person language
|
|
1320
|
+
|
|
1321
|
+
POST /v1/person/language
|
|
1322
|
+
Can we engage this person in English, and if not, in what language?
|
|
1323
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1324
|
+
|
|
1325
|
+
Body fields: --email | --linkedin-url (one required)
|
|
1326
|
+
--first-name --last-name (match-only: rejects a mismatched profile)
|
|
1327
|
+
--priority speed|balance|accuracy --skip-cache
|
|
1328
|
+
--dry-run --external-ref --compliance <json>
|
|
1329
|
+
--body <json|@file|->
|
|
1330
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1331
|
+
`;
|
|
1332
|
+
case "person.phone":
|
|
1333
|
+
return `kingminos person phone
|
|
1334
|
+
|
|
1335
|
+
POST /v1/person/phone
|
|
1336
|
+
One dialable phone number for a person, plus the ranked rest.
|
|
1337
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1338
|
+
|
|
1339
|
+
Body fields: --email | --linkedin-url (one required)
|
|
1340
|
+
--first-name --last-name (match-only: rejects a mismatched profile)
|
|
1341
|
+
--domain
|
|
1342
|
+
--phone-types mobile,landline,other,unknown (omit = any)
|
|
1343
|
+
--max-results <1-10> (default 3)
|
|
1344
|
+
--format e164|international|national|digits|digits_national
|
|
1345
|
+
(fills \`number\`; default e164; every phone also has \`formats\`)
|
|
1346
|
+
--priority speed|balance|accuracy|coverage
|
|
1347
|
+
--skip-cache --dry-run --external-ref --compliance <json>
|
|
1348
|
+
--body <json|@file|->
|
|
1349
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1350
|
+
--cache bypass --cache-control no-cache|no-store
|
|
1351
|
+
`;
|
|
1352
|
+
case "person.title":
|
|
1353
|
+
return `kingminos person title
|
|
1354
|
+
|
|
1355
|
+
POST /v1/person/title
|
|
1356
|
+
A person's current job title, seniority and function.
|
|
1357
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1358
|
+
|
|
1359
|
+
Body fields: --email | --linkedin-url (one required)
|
|
1360
|
+
--first-name --last-name (match-only: rejects a mismatched profile)
|
|
1361
|
+
--company-domain (match-only current employer)
|
|
1362
|
+
--fields title,seniority,function (omit = all three)
|
|
1363
|
+
--path speed|balance|accuracy|coverage
|
|
1364
|
+
--priority speed|balance|accuracy|coverage
|
|
1365
|
+
--routing <json> (only / ignore)
|
|
1366
|
+
--skip-cache --dry-run --external-ref --compliance <json>
|
|
1367
|
+
--body <json|@file|->
|
|
1368
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1369
|
+
--cache bypass --cache-control no-cache|no-store
|
|
1370
|
+
Delivered answers are billed; misses are not. See usage.credits.spent.
|
|
1371
|
+
`;
|
|
1372
|
+
case "person.name":
|
|
1373
|
+
return `kingminos person name
|
|
1374
|
+
|
|
1375
|
+
POST /v1/person/name
|
|
1376
|
+
A person's proper-cased first, last and full name.
|
|
1377
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1378
|
+
|
|
1379
|
+
Body fields: --email | --linkedin-url (one required)
|
|
1380
|
+
--path speed|balance|accuracy|coverage
|
|
1381
|
+
--priority speed|balance|accuracy|coverage
|
|
1382
|
+
--routing <json> (only / ignore)
|
|
1383
|
+
--skip-cache --dry-run --external-ref --compliance <json>
|
|
1384
|
+
--body <json|@file|->
|
|
1385
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1386
|
+
--cache bypass --cache-control no-cache|no-store
|
|
1387
|
+
Delivered answers are billed; misses are not. See usage.credits.spent.
|
|
1388
|
+
`;
|
|
1389
|
+
case "person.b2b_social":
|
|
1390
|
+
return `kingminos person b2b-social
|
|
1391
|
+
|
|
1392
|
+
POST /v1/person/b2b-social
|
|
1393
|
+
A person's B2B social profile URL (result.linkedin_url) from their email.
|
|
1394
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1395
|
+
|
|
1396
|
+
Body fields: --email (required)
|
|
1397
|
+
--linkedin-url --first-name --last-name (match-only: rejects a mismatched profile)
|
|
1398
|
+
--path speed|balance|accuracy (no coverage)
|
|
1399
|
+
--priority speed|balance|accuracy
|
|
1400
|
+
--routing <json> (only / ignore)
|
|
1401
|
+
--skip-cache --dry-run --external-ref --compliance <json>
|
|
1402
|
+
--body <json|@file|->
|
|
1403
|
+
Headers: --idempotency-key --explain default|full|minimal --schema 2
|
|
1404
|
+
--cache bypass --cache-control no-cache|no-store
|
|
1405
|
+
Delivered answers are billed; misses are not. See usage.credits.spent.
|
|
1406
|
+
`;
|
|
1407
|
+
case "runs.get":
|
|
1408
|
+
return `kingminos runs get <id>
|
|
1409
|
+
|
|
1410
|
+
GET /v1/runs/{id}
|
|
1411
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1412
|
+
|
|
1413
|
+
Headers: --explain default|full|minimal --schema 2
|
|
1414
|
+
`;
|
|
1415
|
+
case "credentials.put":
|
|
1416
|
+
return `kingminos credentials put <provider>
|
|
1417
|
+
|
|
1418
|
+
PUT /v1/credentials/{provider}
|
|
1419
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1420
|
+
Providers: ${BYOK_PROVIDERS.join(", ")}
|
|
1421
|
+
Websearch is house-only (byok_not_supported).
|
|
1422
|
+
|
|
1423
|
+
zoominfo: --client-id --client-secret \u2192 { clientId, clientSecret }
|
|
1424
|
+
others: --api-key \u2192 { apiKey }
|
|
1425
|
+
or --body <json|@file|-> (api_key alias accepted by the API)
|
|
1426
|
+
|
|
1427
|
+
Responses never include the raw key. This CLI does not store or log it.
|
|
1428
|
+
`;
|
|
1429
|
+
case "credentials.delete":
|
|
1430
|
+
return `kingminos credentials delete <provider>
|
|
1431
|
+
|
|
1432
|
+
DELETE /v1/credentials/{provider}
|
|
1433
|
+
Requires tenant Bearer (--token or ${TOKEN_ENV}).
|
|
1434
|
+
Providers: ${BYOK_PROVIDERS.join(", ")}
|
|
1435
|
+
`;
|
|
1436
|
+
case "subjects.delete":
|
|
1437
|
+
return `kingminos subjects delete <subject_key>
|
|
1438
|
+
|
|
1439
|
+
DELETE /v1/subjects/{subject_key}
|
|
1440
|
+
Requires tenant Bearer with erase scope (--token or ${TOKEN_ENV}).
|
|
1441
|
+
Company: registrable domain. Person: email:<addr> or li:<slug>.
|
|
1442
|
+
`;
|
|
1443
|
+
default:
|
|
1444
|
+
return ROOT_HELP;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/http-error.ts
|
|
1449
|
+
function classifyHttpError(status, body) {
|
|
1450
|
+
const extra = errorSuffix(body);
|
|
1451
|
+
if (status === 401) {
|
|
1452
|
+
return {
|
|
1453
|
+
exitCode: EXIT.unauthorized,
|
|
1454
|
+
message: `Unauthorized (401)${authDetail(body)}. Set KINGMINOS_API_KEY or pass --token.${extra}`
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
if (status === 402) {
|
|
1458
|
+
return {
|
|
1459
|
+
exitCode: EXIT.payment,
|
|
1460
|
+
message: `Out of credits (402). Add credits at https://app.kingminos.com.${extra}`
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
if (status === 429) {
|
|
1464
|
+
const budget = isBudgetError(body);
|
|
1465
|
+
return {
|
|
1466
|
+
exitCode: EXIT.rateLimited,
|
|
1467
|
+
message: budget ? `Out of credits or daily key budget (429). Retry after Retry-After, or add credits.${extra}` : `Rate limited (429). Retry after the Retry-After header.${extra}`
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
if (status >= 500) {
|
|
1471
|
+
return {
|
|
1472
|
+
exitCode: EXIT.server,
|
|
1473
|
+
message: `KingMinos server error (${status}). Retry the same Idempotency-Key if you sent one.${extra}`
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
exitCode: EXIT.http,
|
|
1478
|
+
message: `HTTP ${status}${extra}`
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function authDetail(body) {
|
|
1482
|
+
const detail = stringField2(body, "detail");
|
|
1483
|
+
switch (detail) {
|
|
1484
|
+
case "missing":
|
|
1485
|
+
return ": missing Bearer token";
|
|
1486
|
+
case "malformed":
|
|
1487
|
+
return ": malformed Bearer token (send `Authorization: Bearer <key>`, not a bare key)";
|
|
1488
|
+
case "mismatch":
|
|
1489
|
+
return ": API key did not match";
|
|
1490
|
+
case void 0:
|
|
1491
|
+
return ": missing, malformed, or mismatched API key";
|
|
1492
|
+
default:
|
|
1493
|
+
return `: ${detail}`;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function isBudgetError(body) {
|
|
1497
|
+
const error = stringField2(body, "error") ?? "";
|
|
1498
|
+
return error === "key_budget_exhausted" || error === "tenant_budget_exhausted" || error.includes("budget") || error.includes("credit");
|
|
1499
|
+
}
|
|
1500
|
+
function errorSuffix(body) {
|
|
1501
|
+
const error = stringField2(body, "error");
|
|
1502
|
+
const hint = stringField2(body, "hint");
|
|
1503
|
+
const parts = [error, hint].filter((part) => Boolean(part));
|
|
1504
|
+
return parts.length > 0 ? ` ${parts.join(" \u2014 ")}` : "";
|
|
1505
|
+
}
|
|
1506
|
+
function stringField2(body, key) {
|
|
1507
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1508
|
+
return void 0;
|
|
1509
|
+
}
|
|
1510
|
+
const value = body[key];
|
|
1511
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/kingminos.ts
|
|
1515
|
+
async function runCli(io) {
|
|
1516
|
+
try {
|
|
1517
|
+
return await runCliUnsafe(io);
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
const secrets = collectSecrets(io);
|
|
1520
|
+
if (error instanceof UsageError) {
|
|
1521
|
+
io.stderr(`${redact(error.message, secrets)}
|
|
1522
|
+
`);
|
|
1523
|
+
return EXIT.usage;
|
|
1524
|
+
}
|
|
1525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1526
|
+
io.stderr(`${redact(message, secrets)}
|
|
1527
|
+
`);
|
|
1528
|
+
return EXIT.usage;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async function runCliUnsafe(io) {
|
|
1532
|
+
const parsed = parseArgv(io.argv);
|
|
1533
|
+
if (parsed.flags.help === true || parsed.flags.h === true) {
|
|
1534
|
+
if (parsed.positionals.length === 0) {
|
|
1535
|
+
io.stdout(ROOT_HELP);
|
|
1536
|
+
return 0;
|
|
1537
|
+
}
|
|
1538
|
+
const command2 = resolveCommand(parsed.positionals);
|
|
1539
|
+
io.stdout(command2.name === "help" ? ROOT_HELP : commandHelp(command2.name));
|
|
1540
|
+
return 0;
|
|
1541
|
+
}
|
|
1542
|
+
const command = resolveCommand(parsed.positionals);
|
|
1543
|
+
if (command.name === "help") {
|
|
1544
|
+
io.stdout(command.topic ? commandHelp(helpTopic(command.topic)) : ROOT_HELP);
|
|
1545
|
+
return 0;
|
|
1546
|
+
}
|
|
1547
|
+
assertKnownFlags(command.name, parsed.flags);
|
|
1548
|
+
if (command.name === "batch") {
|
|
1549
|
+
return runBatch(io, command, parsed.flags);
|
|
1550
|
+
}
|
|
1551
|
+
const request = buildRequest(command, parsed.flags, {
|
|
1552
|
+
readFile: io.readFile ?? ((path) => readFileSync(path, "utf8")),
|
|
1553
|
+
stdin: io.stdin ?? ""
|
|
1554
|
+
});
|
|
1555
|
+
const token = request.auth ? requireToken(parsed.flags, io.env) : void 0;
|
|
1556
|
+
const baseUrl = resolveBaseUrl(parsed.flags, io.env);
|
|
1557
|
+
const response = await apiRequest({
|
|
1558
|
+
fetch: io.fetch,
|
|
1559
|
+
method: request.method,
|
|
1560
|
+
url: `${baseUrl}${request.path}`,
|
|
1561
|
+
token,
|
|
1562
|
+
headers: request.headers,
|
|
1563
|
+
body: request.body
|
|
1564
|
+
});
|
|
1565
|
+
const formatted = formatBody(response.text, response.contentType);
|
|
1566
|
+
const secrets = collectSecrets(io, parsed.flags);
|
|
1567
|
+
const asJson = flagBool(parsed.flags, "json") === true;
|
|
1568
|
+
if (response.status >= 400) {
|
|
1569
|
+
return writeHttpError(io, response.status, response.text, formatted, secrets, asJson);
|
|
1570
|
+
}
|
|
1571
|
+
io.stdout(redact(formatted, secrets));
|
|
1572
|
+
if (!asJson) {
|
|
1573
|
+
const parsedBody = parseJson(response.text);
|
|
1574
|
+
io.stdout(redact(formatDecisionNotes(parsedBody), secrets));
|
|
1575
|
+
io.stdout(redact(formatNextStepsHints(parsedBody), secrets));
|
|
1576
|
+
}
|
|
1577
|
+
return EXIT.ok;
|
|
1578
|
+
}
|
|
1579
|
+
function writeHttpError(io, status, rawText, formatted, secrets, asJson) {
|
|
1580
|
+
const classified = classifyHttpError(status, parseJson(rawText));
|
|
1581
|
+
if (asJson) {
|
|
1582
|
+
io.stderr(redact(formatted, secrets));
|
|
1583
|
+
} else {
|
|
1584
|
+
io.stderr(redact(`${classified.message}
|
|
1585
|
+
${formatted}`, secrets));
|
|
1586
|
+
}
|
|
1587
|
+
return classified.exitCode;
|
|
1588
|
+
}
|
|
1589
|
+
function parseJson(text) {
|
|
1590
|
+
try {
|
|
1591
|
+
return JSON.parse(text);
|
|
1592
|
+
} catch {
|
|
1593
|
+
return null;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async function runBatch(io, command, flags) {
|
|
1597
|
+
const rawPriority = flagString(flags, "priority");
|
|
1598
|
+
if (!rawPriority) {
|
|
1599
|
+
throw new UsageError(
|
|
1600
|
+
"Before a bulk run, ask which path: speed, balance, accuracy, or coverage. Re-run with --priority speed|balance|accuracy|coverage. Max yield = coverage."
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const priority = parsePriority(rawPriority);
|
|
1604
|
+
const file = flagString(flags, "file");
|
|
1605
|
+
if (!file) {
|
|
1606
|
+
throw new UsageError(
|
|
1607
|
+
"batch requires --file <path|-> (JSONL or a JSON array of request bodies)"
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const loader = {
|
|
1611
|
+
readFile: io.readFile ?? ((path) => readFileSync(path, "utf8")),
|
|
1612
|
+
stdin: io.stdin ?? ""
|
|
1613
|
+
};
|
|
1614
|
+
const raw = file === "-" ? loader.stdin : loader.readFile(file);
|
|
1615
|
+
const rows = parseBatchRows(raw);
|
|
1616
|
+
if (rows.length === 0) {
|
|
1617
|
+
throw new UsageError("batch file is empty");
|
|
1618
|
+
}
|
|
1619
|
+
const token = requireToken(flags, io.env);
|
|
1620
|
+
const baseUrl = resolveBaseUrl(flags, io.env);
|
|
1621
|
+
const secrets = collectSecrets(io, flags);
|
|
1622
|
+
const asJson = flagBool(flags, "json") === true;
|
|
1623
|
+
let worst = EXIT.ok;
|
|
1624
|
+
for (const [index, row] of rows.entries()) {
|
|
1625
|
+
const body = applyPriority(row, priority, command.capability);
|
|
1626
|
+
const request = buildRequest({ name: command.capability }, flags, {
|
|
1627
|
+
...loader,
|
|
1628
|
+
stdin: JSON.stringify(body)
|
|
1629
|
+
});
|
|
1630
|
+
request.body = body;
|
|
1631
|
+
const response = await apiRequest({
|
|
1632
|
+
fetch: io.fetch,
|
|
1633
|
+
method: request.method,
|
|
1634
|
+
url: `${baseUrl}${request.path}`,
|
|
1635
|
+
token,
|
|
1636
|
+
headers: request.headers,
|
|
1637
|
+
body: request.body
|
|
1638
|
+
});
|
|
1639
|
+
const formatted = formatBody(response.text, response.contentType);
|
|
1640
|
+
if (response.status >= 400) {
|
|
1641
|
+
const prefix = asJson ? "" : `row ${index + 1} `;
|
|
1642
|
+
const code = writeHttpError(
|
|
1643
|
+
io,
|
|
1644
|
+
response.status,
|
|
1645
|
+
response.text,
|
|
1646
|
+
`${prefix}${formatted}`,
|
|
1647
|
+
secrets,
|
|
1648
|
+
asJson
|
|
1649
|
+
);
|
|
1650
|
+
worst = Math.max(worst, code);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (asJson) {
|
|
1654
|
+
io.stdout(redact(formatted, secrets));
|
|
1655
|
+
} else {
|
|
1656
|
+
io.stdout(redact(`# row ${index + 1}
|
|
1657
|
+
${formatted}`, secrets));
|
|
1658
|
+
const parsedBody = parseJson(response.text);
|
|
1659
|
+
io.stdout(redact(formatDecisionNotes(parsedBody), secrets));
|
|
1660
|
+
io.stdout(redact(formatNextStepsHints(parsedBody), secrets));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return worst;
|
|
1664
|
+
}
|
|
1665
|
+
function parseBatchRows(raw) {
|
|
1666
|
+
const trimmed = raw.trim();
|
|
1667
|
+
if (!trimmed) {
|
|
1668
|
+
return [];
|
|
1669
|
+
}
|
|
1670
|
+
if (trimmed.startsWith("[")) {
|
|
1671
|
+
const parsed = JSON.parse(trimmed);
|
|
1672
|
+
if (!Array.isArray(parsed)) {
|
|
1673
|
+
throw new UsageError("--file JSON array is invalid");
|
|
1674
|
+
}
|
|
1675
|
+
return parsed.map((row, index) => {
|
|
1676
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
1677
|
+
throw new UsageError(`--file row ${index + 1} must be a JSON object`);
|
|
1678
|
+
}
|
|
1679
|
+
return row;
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
return trimmed.split(/\r?\n/).flatMap((line, index) => {
|
|
1683
|
+
if (!line.trim()) {
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
let parsed;
|
|
1687
|
+
try {
|
|
1688
|
+
parsed = JSON.parse(line);
|
|
1689
|
+
} catch {
|
|
1690
|
+
throw new UsageError(`--file line ${index + 1} is not valid JSON`);
|
|
1691
|
+
}
|
|
1692
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1693
|
+
throw new UsageError(`--file line ${index + 1} must be a JSON object`);
|
|
1694
|
+
}
|
|
1695
|
+
return [parsed];
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
function helpTopic(topic) {
|
|
1699
|
+
switch (topic) {
|
|
1700
|
+
case "resolve":
|
|
1701
|
+
return "company.resolve";
|
|
1702
|
+
case "domain":
|
|
1703
|
+
return "company.domain";
|
|
1704
|
+
case "hierarchy":
|
|
1705
|
+
return "company.hierarchy";
|
|
1706
|
+
case "b2b-social":
|
|
1707
|
+
return "company.b2b_social";
|
|
1708
|
+
case "icon":
|
|
1709
|
+
return "company.icon";
|
|
1710
|
+
case "description":
|
|
1711
|
+
return "company.description";
|
|
1712
|
+
case "verify-employment":
|
|
1713
|
+
return "person.verify_employment";
|
|
1714
|
+
case "language":
|
|
1715
|
+
return "person.language";
|
|
1716
|
+
case "phone":
|
|
1717
|
+
return "person.phone";
|
|
1718
|
+
case "title":
|
|
1719
|
+
return "person.title";
|
|
1720
|
+
case "name":
|
|
1721
|
+
return "person.name";
|
|
1722
|
+
case "recipes":
|
|
1723
|
+
return "recipes";
|
|
1724
|
+
case "batch":
|
|
1725
|
+
return "batch";
|
|
1726
|
+
default:
|
|
1727
|
+
return topic;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function resolveBaseUrl(flags, env) {
|
|
1731
|
+
const fromFlag = typeof flags["base-url"] === "string" ? flags["base-url"] : void 0;
|
|
1732
|
+
const raw = fromFlag?.trim() || env[BASE_URL_ENV]?.trim() || DEFAULT_BASE_URL;
|
|
1733
|
+
return raw.replace(/\/+$/, "");
|
|
1734
|
+
}
|
|
1735
|
+
function optionalToken(flags, env) {
|
|
1736
|
+
const fromFlag = typeof flags.token === "string" ? flags.token.trim() : "";
|
|
1737
|
+
const fromEnv = env[TOKEN_ENV]?.trim() ?? "";
|
|
1738
|
+
return fromFlag || fromEnv || void 0;
|
|
1739
|
+
}
|
|
1740
|
+
function requireToken(flags, env) {
|
|
1741
|
+
const token = optionalToken(flags, env);
|
|
1742
|
+
if (!token) {
|
|
1743
|
+
throw new UsageError(`missing tenant Bearer: set ${TOKEN_ENV} or pass --token`);
|
|
1744
|
+
}
|
|
1745
|
+
return token;
|
|
1746
|
+
}
|
|
1747
|
+
function collectSecrets(io, flags) {
|
|
1748
|
+
const secrets = [];
|
|
1749
|
+
const push = (value) => {
|
|
1750
|
+
const trimmed = value?.trim();
|
|
1751
|
+
if (trimmed) {
|
|
1752
|
+
secrets.push(trimmed);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
push(io.env[TOKEN_ENV]);
|
|
1756
|
+
if (typeof flags?.token === "string") {
|
|
1757
|
+
push(flags.token);
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof flags?.["api-key"] === "string") {
|
|
1760
|
+
push(flags["api-key"]);
|
|
1761
|
+
}
|
|
1762
|
+
if (typeof flags?.["client-secret"] === "string") {
|
|
1763
|
+
push(flags["client-secret"]);
|
|
1764
|
+
}
|
|
1765
|
+
return secrets;
|
|
1766
|
+
}
|
|
1767
|
+
function redact(text, secrets) {
|
|
1768
|
+
return secrets.reduce((out, secret) => out.split(secret).join("[redacted]"), text);
|
|
1769
|
+
}
|
|
1770
|
+
function launchedAsCli() {
|
|
1771
|
+
const self = fileURLToPath(import.meta.url);
|
|
1772
|
+
const argv1 = process.argv[1];
|
|
1773
|
+
if (!argv1) {
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
return realpathSync(argv1) === realpathSync(self);
|
|
1778
|
+
} catch {
|
|
1779
|
+
return argv1.endsWith("kingminos.ts") || argv1.endsWith("kingminos.js") || /(^|\/)kingminos$/.test(argv1);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async function readStdin() {
|
|
1783
|
+
if (process.stdin.isTTY) {
|
|
1784
|
+
return "";
|
|
1785
|
+
}
|
|
1786
|
+
const chunks = [];
|
|
1787
|
+
for await (const chunk of process.stdin) {
|
|
1788
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1789
|
+
}
|
|
1790
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1791
|
+
}
|
|
1792
|
+
if (launchedAsCli()) {
|
|
1793
|
+
const readBodyStdin = process.argv.includes("--body") && process.argv.includes("-");
|
|
1794
|
+
const stdin = readBodyStdin ? await readStdin() : "";
|
|
1795
|
+
const code = await runCli({
|
|
1796
|
+
argv: process.argv.slice(2),
|
|
1797
|
+
env: process.env,
|
|
1798
|
+
stdout: (text) => {
|
|
1799
|
+
process.stdout.write(text);
|
|
1800
|
+
},
|
|
1801
|
+
stderr: (text) => {
|
|
1802
|
+
process.stderr.write(text);
|
|
1803
|
+
},
|
|
1804
|
+
fetch,
|
|
1805
|
+
stdin
|
|
1806
|
+
});
|
|
1807
|
+
process.exitCode = code;
|
|
1808
|
+
}
|
|
1809
|
+
export {
|
|
1810
|
+
BASE_URL_ENV,
|
|
1811
|
+
DEFAULT_BASE_URL,
|
|
1812
|
+
EXIT,
|
|
1813
|
+
ROOT_HELP,
|
|
1814
|
+
TOKEN_ENV,
|
|
1815
|
+
assertKnownFlags,
|
|
1816
|
+
buildRequest,
|
|
1817
|
+
classifyHttpError,
|
|
1818
|
+
formatDecisionNotes,
|
|
1819
|
+
parseArgv,
|
|
1820
|
+
resolveCommand,
|
|
1821
|
+
runCli,
|
|
1822
|
+
stripSafeToWrite
|
|
1823
|
+
};
|