@signaliz/cli 1.0.60 → 1.0.62

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 +2 -0
  2. package/dist/bin.js +248 -39
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -14,6 +14,8 @@ signaliz auth login
14
14
  # https://api.signaliz.com/functions/v1/api/v1/signals
15
15
 
16
16
  signaliz find-email --company-domain example.com --full-name "Jane Doe"
17
+ # An HTTPS LinkedIn person profile URL (/in/<slug>) is sufficient without a name or company domain
18
+ signaliz find-email --linkedin-url "https://www.linkedin.com/in/jane-doe"
17
19
  # Force a fresh-provider quality check instead of reusing cached email or verification data
18
20
  signaliz find-email --company-domain example.com --full-name "Jane Doe" --skip-cache
19
21
  signaliz verify-email jane@example.com
package/dist/bin.js CHANGED
@@ -7,11 +7,18 @@ var import_node_os = require("os");
7
7
  var import_node_path = require("path");
8
8
  var import_node_readline = require("readline");
9
9
  var import_sdk = require("@signaliz/sdk");
10
+ var CliInputError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "CliInputError";
14
+ }
15
+ };
10
16
  var CONFIG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".signaliz");
11
17
  var CONFIG_FILE = (0, import_node_path.join)(CONFIG_DIR, "config.json");
12
18
  var HELP = `Signaliz CLI
13
19
 
14
20
  Five product commands:
21
+ signaliz find-email --linkedin-url https://www.linkedin.com/in/jane-doe [--skip-cache]
15
22
  signaliz find-email --company-domain example.com --full-name "Jane Doe" [--skip-cache]
16
23
  signaliz find-email --run-id run_... Resume an existing Find Email run without duplicate spend
17
24
  signaliz find-email --job-id job_... Resume an existing Find Email batch without redispatch
@@ -92,13 +99,25 @@ function numberFlag(name) {
92
99
  const value = flag(name);
93
100
  if (value === void 0) return void 0;
94
101
  const parsed = Number(value);
95
- if (!Number.isFinite(parsed)) fail(`--${name} must be a number`);
102
+ if (!Number.isFinite(parsed)) throw new CliInputError(`--${name} must be a number`);
96
103
  return parsed;
97
104
  }
98
105
  function requireIntegerInRange(value, name, minimum, maximum) {
99
106
  if (value === void 0) return;
100
107
  if (!Number.isInteger(value) || value < minimum || value > maximum) {
101
- throw new RangeError(`${name} must be an integer between ${minimum} and ${maximum}`);
108
+ throw new CliInputError(`${name} must be an integer between ${minimum} and ${maximum}`);
109
+ }
110
+ }
111
+ function requireSignalToCopyIdentity(request) {
112
+ if (stringValue(request.signalRunId)) return;
113
+ const missing = [
114
+ ["companyDomain", request.companyDomain],
115
+ ["personName", request.personName],
116
+ ["title", request.title],
117
+ ["campaignOffer", request.campaignOffer]
118
+ ].filter(([, value]) => !stringValue(value)).map(([name]) => name);
119
+ if (missing.length > 0) {
120
+ throw new CliInputError(`Signal to Copy requires signalRunId or: ${missing.join(", ")}`);
102
121
  }
103
122
  }
104
123
  function csvFlag(name) {
@@ -108,7 +127,7 @@ function csvFlag(name) {
108
127
  function companySignalSearchMode(value) {
109
128
  if (value === void 0 || value === null || value === "") return void 0;
110
129
  if (value === "fast" || value === "balanced" || value === "coverage") return value;
111
- fail("--search-mode must be fast, balanced, or coverage");
130
+ throw new CliInputError("--search-mode must be fast, balanced, or coverage");
112
131
  }
113
132
  function readBatchInput() {
114
133
  const path = flag("input");
@@ -118,23 +137,28 @@ function readBatchInput() {
118
137
  try {
119
138
  raw = (0, import_node_fs.readFileSync)(path === "-" ? 0 : path, "utf8").trim();
120
139
  } catch (error) {
121
- fail(`Unable to read --input ${source}: ${error instanceof Error ? error.message : String(error)}`);
140
+ throw new CliInputError(`Unable to read --input ${source}: ${error instanceof Error ? error.message : String(error)}`);
122
141
  }
123
- if (!raw) fail(`--input ${source} must contain at least one record`);
142
+ if (!raw) throw new CliInputError(`--input ${source} must contain at least one record`);
124
143
  let rows;
125
144
  try {
126
145
  rows = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).filter(Boolean).map((line, index) => {
127
146
  try {
128
147
  return JSON.parse(line);
129
148
  } catch {
130
- fail(`Invalid JSON on line ${index + 1} of ${source}`);
149
+ throw new CliInputError(`Invalid JSON on line ${index + 1} of ${source}`);
131
150
  }
132
151
  });
133
152
  } catch (error) {
134
- fail(`Invalid JSON in --input ${source}: ${error instanceof Error ? error.message : String(error)}`);
153
+ if (error instanceof CliInputError) throw error;
154
+ throw new CliInputError(`Invalid JSON in --input ${source}: ${error instanceof Error ? error.message : String(error)}`);
155
+ }
156
+ if (!Array.isArray(rows) || rows.length === 0) {
157
+ throw new CliInputError("--input must be a non-empty JSON array or JSONL file");
158
+ }
159
+ if (rows.length > 5e3) {
160
+ throw new CliInputError(`--input supports at most 5,000 records; received ${rows.length}`);
135
161
  }
136
- if (!Array.isArray(rows) || rows.length === 0) fail("--input must be a non-empty JSON array or JSONL file");
137
- if (rows.length > 5e3) fail(`--input supports at most 5,000 records; received ${rows.length}`);
138
162
  return rows;
139
163
  }
140
164
  function record(value) {
@@ -164,6 +188,11 @@ function customerEmailResult(value, kind) {
164
188
  if (String(result.status || "").toLowerCase() === "processing") {
165
189
  const verificationRunId2 = result.verificationRunId ?? raw.verification_run_id;
166
190
  const runId2 = result.runId ?? raw.run_id ?? verificationRunId2;
191
+ const creditsUsed2 = safeNonNegativeNumber(result.creditsUsed ?? raw.credits_used);
192
+ const creditsCharged2 = safeNonNegativeNumber(result.creditsCharged ?? raw.credits_charged);
193
+ const estimatedExternalCostUsd2 = safeNonNegativeNumber(
194
+ result.estimatedExternalCostUsd ?? raw.estimated_external_cost_usd
195
+ );
167
196
  return {
168
197
  success: true,
169
198
  status: "processing",
@@ -171,16 +200,28 @@ function customerEmailResult(value, kind) {
171
200
  run_id: runId2,
172
201
  ...verificationRunId2 ? { verification_run_id: verificationRunId2 } : {},
173
202
  next_poll_after_seconds: result.nextPollAfterSeconds ?? raw.next_poll_after_seconds,
174
- suggested_action: result.suggestedAction ?? raw.suggested_action ?? (verificationRunId2 ? "retry_with_verification_run_id" : "retry_with_run_id")
203
+ suggested_action: result.suggestedAction ?? raw.suggested_action ?? (verificationRunId2 ? "retry_with_verification_run_id" : "retry_with_run_id"),
204
+ retry_eligible: raw.retry_eligible ?? true,
205
+ retry_strategy: raw.retry_strategy ?? "same_run_recovery",
206
+ do_not_auto_resubmit: raw.do_not_auto_resubmit ?? true,
207
+ recovery_mode: raw.recovery_mode ?? "same_run",
208
+ recovery_status: raw.recovery_status ?? "pending",
209
+ ...creditsUsed2 !== null ? { credits_used: creditsUsed2 } : {},
210
+ ...creditsCharged2 !== null ? { credits_charged: creditsCharged2 } : {},
211
+ ...typeof raw.live_provider_called === "boolean" ? { live_provider_called: raw.live_provider_called } : {},
212
+ ...estimatedExternalCostUsd2 !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd2 } : {},
213
+ ...Object.keys(customerEmailBillingMetadata(raw.billing_metadata)).length > 0 ? { billing_metadata: customerEmailBillingMetadata(raw.billing_metadata) } : {}
175
214
  };
176
215
  }
177
216
  const creditsUsed = Number(result.creditsUsed ?? raw.credits_used ?? 0);
217
+ const creditsCharged = Number(result.creditsCharged ?? raw.credits_charged ?? creditsUsed);
178
218
  const base = {
179
219
  email: typeof result.email === "string" && result.email.trim() ? result.email.trim() : null,
180
220
  status: "completed",
181
221
  success: result.success === true,
182
222
  is_valid: typeof result.isValid === "boolean" ? result.isValid : typeof raw.is_valid === "boolean" ? raw.is_valid : result.isVerified ?? false,
183
- credits_used: Number.isFinite(creditsUsed) && creditsUsed >= 0 ? creditsUsed : 0
223
+ credits_used: Number.isFinite(creditsUsed) && creditsUsed >= 0 ? creditsUsed : 0,
224
+ credits_charged: Number.isFinite(creditsCharged) && creditsCharged >= 0 ? creditsCharged : 0
184
225
  };
185
226
  if (kind === "find") {
186
227
  const verifiedForSending2 = typeof result.isVerifiedForSending === "boolean" ? result.isVerifiedForSending : raw.verified_for_sending === true;
@@ -211,11 +252,29 @@ function customerEmailResult(value, kind) {
211
252
  cache_hit: cacheHit2,
212
253
  fresh_enrichment_used: freshEnrichmentUsed,
213
254
  credits_used: base.credits_used,
255
+ credits_charged: base.credits_charged,
214
256
  live_provider_called: liveProviderCalled
215
257
  };
216
258
  const estimatedExternalCostUsd2 = safeNonNegativeNumber(
217
259
  result.estimatedExternalCostUsd ?? raw.estimated_external_cost_usd
218
260
  );
261
+ const rawErrorCode = String(result.errorCode ?? raw.error_code ?? "").trim().toUpperCase();
262
+ const terminalErrorCode = ["EMAIL_NOT_FOUND", "PROVIDER_001", "PROVIDER_002"].includes(rawErrorCode) ? rawErrorCode : null;
263
+ const directRunId = stringValue(result.runId ?? raw.run_id);
264
+ const verificationRunId2 = stringValue(raw.verification_run_id);
265
+ const jobId = stringValue(raw.job_id);
266
+ const rawRetryStrategy = result.retryStrategy ?? raw.retry_strategy;
267
+ const retryStrategy = ["new_idempotency_key", "same_run_recovery"].includes(String(rawRetryStrategy)) ? String(rawRetryStrategy) : null;
268
+ const recoveryStatus = String(raw.recovery_status ?? "").trim().toLowerCase();
269
+ const nestedRunActive = Boolean(verificationRunId2 || jobId) && !["completed", "failed"].includes(recoveryStatus);
270
+ const manualReviewRequested = (result.suggestedAction ?? raw.suggested_action) === "manual_review";
271
+ const sameRunRetryProven = retryStrategy === "same_run_recovery" && !manualReviewRequested && nestedRunActive;
272
+ const newRequestRetryProven = retryStrategy === "new_idempotency_key" && raw.credits_used === 0 && raw.credits_charged === 0;
273
+ const retryEligible = terminalErrorCode === "PROVIDER_002" && !manualReviewRequested && (result.retryEligible ?? raw.retry_eligible) === true && (sameRunRetryProven || newRequestRetryProven);
274
+ const suggestedAction = retryEligible && retryStrategy === "same_run_recovery" ? verificationRunId2 ? "retry_with_verification_run_id" : jobId ? "check_job_status" : null : terminalErrorCode === "PROVIDER_002" && !retryEligible ? "manual_review" : null;
275
+ const terminalError = terminalErrorCode ? String(
276
+ result.error ?? raw.error ?? raw.message ?? (terminalErrorCode === "EMAIL_NOT_FOUND" ? "No verified email found" : "")
277
+ ).trim().slice(0, 300) || null : null;
219
278
  return {
220
279
  ...base,
221
280
  is_valid: verifiedForSending2,
@@ -241,11 +300,31 @@ function customerEmailResult(value, kind) {
241
300
  billing_metadata: normalizedBillingMetadata,
242
301
  ...Object.keys(historicalBillingMetadata2).length > 0 ? { historical_billing_metadata: historicalBillingMetadata2 } : {},
243
302
  ...estimatedExternalCostUsd2 !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd2 } : {},
244
- ...result.runId ?? raw.run_id ? { run_id: result.runId ?? raw.run_id } : {},
303
+ ...directRunId ? { run_id: directRunId } : {},
304
+ ...raw.recovery_mode ? {
305
+ recovery_mode: raw.recovery_mode,
306
+ recovery_status: raw.recovery_status ?? "completed"
307
+ } : {},
245
308
  ...raw.cache_publication_status ? {
246
309
  cache_publication_status: raw.cache_publication_status,
247
310
  cache_publication_retry_eligible: raw.cache_publication_retry_eligible === true,
248
311
  ...raw.suggested_action ? { suggested_action: raw.suggested_action } : {}
312
+ } : {},
313
+ ...terminalErrorCode ? {
314
+ email: null,
315
+ success: false,
316
+ found: false,
317
+ is_valid: false,
318
+ verified_for_sending: false,
319
+ is_deliverable: false,
320
+ error_code: terminalErrorCode,
321
+ retry_eligible: retryEligible,
322
+ ...terminalError ? { error: terminalError } : {},
323
+ ...retryEligible && retryStrategy ? { retry_strategy: retryStrategy } : {},
324
+ ...suggestedAction ? { suggested_action: suggestedAction } : {},
325
+ ...result.doNotAutoResubmit === true || raw.do_not_auto_resubmit === true ? { do_not_auto_resubmit: true } : {},
326
+ ...verificationRunId2 ? { verification_run_id: verificationRunId2 } : {},
327
+ ...jobId ? { job_id: jobId } : {}
249
328
  } : {}
250
329
  };
251
330
  }
@@ -375,7 +454,9 @@ function rejectDurableNoWaitPollingFlags(label) {
375
454
  if (!hasFlag("no-wait")) return;
376
455
  const conflicts = ["max-wait-ms", "poll-interval-ms", "concurrency"].filter(hasFlag);
377
456
  if (conflicts.length > 0) {
378
- fail(`${label} --no-wait cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
457
+ throw new CliInputError(
458
+ `${label} --no-wait cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
459
+ );
379
460
  }
380
461
  }
381
462
  function recoverableJobOutput(value, command) {
@@ -454,10 +535,11 @@ function resolveBaseUrl() {
454
535
  const config = loadConfig();
455
536
  return (flag("base-url") || process.env.SIGNALIZ_BASE_URL || process.env.SIGNALIZ_API_URL || config.baseUrl || "https://api.signaliz.com/functions/v1").replace(/\/$/, "");
456
537
  }
457
- function createClient() {
538
+ function createClient(maxRetries) {
458
539
  return new import_sdk.Signaliz({
459
540
  apiKey: resolveApiKey(),
460
- baseUrl: resolveBaseUrl()
541
+ baseUrl: resolveBaseUrl(),
542
+ ...maxRetries !== void 0 ? { maxRetries } : {}
461
543
  });
462
544
  }
463
545
  function numberValue(value) {
@@ -470,6 +552,18 @@ function nullableNumberValue(value) {
470
552
  function stringValue(value) {
471
553
  return typeof value === "string" ? value.trim() : "";
472
554
  }
555
+ function hasFindEmailLinkedInIdentity(value) {
556
+ const linkedinUrl = stringValue(value);
557
+ if (!linkedinUrl) return false;
558
+ if (!/^[hH][tT][tT][pP][sS]:\/\/(?:[A-Za-z0-9-]+\.)*[lL][iI][nN][kK][eE][dD][iI][nN]\.[cC][oO][mM]\/[iI][nN]\/[^/?#]+\/?(?:[?#].*)?$/.test(linkedinUrl)) return false;
559
+ try {
560
+ const url = new URL(linkedinUrl);
561
+ const hostname = url.hostname.toLowerCase();
562
+ return url.protocol === "https:" && !url.username && !url.password && !url.port && (hostname === "linkedin.com" || hostname.endsWith(".linkedin.com")) && /^\/in\/[^/]+\/?$/i.test(url.pathname);
563
+ } catch {
564
+ return false;
565
+ }
566
+ }
473
567
  function signalsEverythingErrorType(status) {
474
568
  if (status === 400 || status === 422) return "validation";
475
569
  if (status === 401) return "auth_expired";
@@ -497,9 +591,9 @@ function normalizeSignalsEverythingResult(request, value) {
497
591
  source: stringValue(item.source)
498
592
  };
499
593
  }).filter((item) => Boolean(item.url)) : [];
500
- const eventDate = stringValue(signal.event_date || signal.date) || evidence[0]?.publishedAt || "";
501
- const sourceUrl = stringValue(signal.source_url || signal.url) || evidence[0]?.url || null;
502
- if (!eventDate || !sourceUrl || !evidence.some((item) => Boolean(item.publishedAt))) {
594
+ const eventDate = stringValue(signal.event_date || signal.date);
595
+ const sourceUrl = stringValue(signal.source_url || signal.url);
596
+ if (!eventDate || !sourceUrl || !evidence.some((item) => item.url === sourceUrl && item.publishedAt === eventDate)) {
503
597
  throw new Error("Signals Everything response violated the dated-evidence and source-URL contract.");
504
598
  }
505
599
  return {
@@ -522,6 +616,7 @@ function normalizeSignalsEverythingResult(request, value) {
522
616
  const status = statuses.has(statusValue) ? statusValue : signals2.length > 0 ? "completed" : "failed";
523
617
  const guardrail = record(value.cost_guardrail);
524
618
  const maxCost = numberValue(guardrail.max_cost_per_qualified_signal_usd);
619
+ const costSource = stringValue(guardrail.cost_source);
525
620
  const identityCostSource = stringValue(guardrail.identity_cost_source);
526
621
  const warnings = Array.isArray(value.warnings) ? value.warnings.filter((warning) => typeof warning === "string") : void 0;
527
622
  return {
@@ -539,6 +634,13 @@ function normalizeSignalsEverythingResult(request, value) {
539
634
  ...maxCost === void 0 ? {} : {
540
635
  costGuardrail: {
541
636
  maxCostPerQualifiedSignalUsd: maxCost,
637
+ projectedSourceCostUsd: nullableNumberValue(guardrail.projected_source_cost_usd),
638
+ maxSourceCostUsd: nullableNumberValue(guardrail.max_source_cost_usd),
639
+ projectedCostPerRequestedCompanySignalUsd: nullableNumberValue(
640
+ guardrail.projected_cost_per_requested_company_signal_usd
641
+ ),
642
+ sourceCostUsd: nullableNumberValue(guardrail.source_cost_usd),
643
+ costSource: ["observed", "catalog_preflight", "catalog_bounded", "mixed", "unavailable"].includes(costSource) ? costSource : void 0,
542
644
  costPerQualifiedSignalUsd: nullableNumberValue(guardrail.cost_per_qualified_signal_usd),
543
645
  identityResolutionCostUsd: nullableNumberValue(guardrail.identity_resolution_cost_usd),
544
646
  identityCostSource: [
@@ -547,7 +649,13 @@ function normalizeSignalsEverythingResult(request, value) {
547
649
  "not_used",
548
650
  "unavailable"
549
651
  ].includes(identityCostSource) ? identityCostSource : void 0,
550
- identityProviderRequests: numberValue(guardrail.identity_provider_requests)
652
+ identityProviderRequests: numberValue(guardrail.identity_provider_requests),
653
+ qualifiedCompanySignalCount: numberValue(guardrail.qualified_company_signal_count),
654
+ returnedCompanySignalCount: numberValue(guardrail.returned_company_signal_count),
655
+ availableCompanySignalCount: numberValue(guardrail.available_company_signal_count),
656
+ acquisitionCandidateLimit: numberValue(guardrail.acquisition_candidate_limit),
657
+ minimumQualifiedCompanySignals: numberValue(guardrail.minimum_qualified_company_signals),
658
+ withinLimit: guardrail.within_limit === true
551
659
  }
552
660
  },
553
661
  ...warnings?.length ? { warnings } : {},
@@ -702,7 +810,9 @@ async function findEmail() {
702
810
  "skip-cache"
703
811
  ].filter(hasFlag);
704
812
  if (conflicts.length > 0) {
705
- fail(`Find Email --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
813
+ throw new CliInputError(
814
+ `Find Email --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
815
+ );
706
816
  }
707
817
  const result2 = await createClient().resumeFindEmailBatch(jobId, {
708
818
  pageSize: numberFlag("page-size"),
@@ -732,6 +842,15 @@ async function findEmail() {
732
842
  idempotencyKey
733
843
  };
734
844
  });
845
+ const invalidIndexes = requests.flatMap((request2, index) => {
846
+ if (stringValue(request2.runId)) return [];
847
+ if (hasFindEmailLinkedInIdentity(request2.linkedinUrl)) return [];
848
+ const hasName = stringValue(request2.fullName) || stringValue(request2.firstName) && stringValue(request2.lastName);
849
+ return !stringValue(request2.companyDomain) || !hasName ? [index] : [];
850
+ });
851
+ if (invalidIndexes.length === requests.length) {
852
+ throw new CliInputError("Every Find Email row must provide an HTTPS LinkedIn person profile URL (/in/<slug>), or company_domain with full_name or first_name and last_name.");
853
+ }
735
854
  if (hasFlag("dry-run")) {
736
855
  const result3 = await createClient().findEmails(requests, { ...batchOptions(), dryRun: true });
737
856
  dryRunOutput(result3);
@@ -747,8 +866,9 @@ async function findEmail() {
747
866
  const firstName = flag("first-name");
748
867
  const lastName = flag("last-name");
749
868
  const linkedinUrl = flag("linkedin-url");
750
- if (!runId && (!companyDomain || !fullName && !(firstName && lastName))) {
751
- fail('Usage: signaliz find-email --company-domain example.com --full-name "Jane Doe", or signaliz find-email --run-id run_...');
869
+ const hasNameAndDomain = Boolean(companyDomain && (fullName || firstName && lastName));
870
+ if (!runId && !hasFindEmailLinkedInIdentity(linkedinUrl) && !hasNameAndDomain) {
871
+ throw new CliInputError("Find Email must provide an HTTPS --linkedin-url person profile (/in/<slug>), or --company-domain with --full-name or --first-name and --last-name, or --run-id.");
752
872
  }
753
873
  const request = runId ? { runId } : {
754
874
  companyDomain,
@@ -761,11 +881,11 @@ async function findEmail() {
761
881
  idempotencyKey: flag("idempotency-key")
762
882
  };
763
883
  if (hasFlag("dry-run")) {
764
- const result2 = await createClient().findEmail({ ...request, dryRun: true });
884
+ const result2 = await createClient(runId ? 0 : void 0).findEmail({ ...request, dryRun: true });
765
885
  dryRunOutput(result2);
766
886
  return;
767
887
  }
768
- const result = await createClient().findEmail(request);
888
+ const result = await createClient(runId ? 0 : void 0).findEmail(request);
769
889
  const publicResult = customerEmailResult(result, "find");
770
890
  if (outputFailedCoreProduct(publicResult, "No verified email found")) return;
771
891
  output(publicResult, () => {
@@ -805,7 +925,7 @@ async function verifyEmail(args) {
805
925
  ...positionalEmail ? ["email argument"] : [],
806
926
  ...conflicts.map((name) => `--${name}`)
807
927
  ];
808
- fail(`Verify Email --job-id cannot be combined with ${conflictList.join(", ")}.`);
928
+ throw new CliInputError(`Verify Email --job-id cannot be combined with ${conflictList.join(", ")}.`);
809
929
  }
810
930
  const result2 = await createClient().resumeVerifyEmailBatch(jobId, {
811
931
  pageSize: numberFlag("page-size"),
@@ -846,7 +966,9 @@ async function verifyEmail(args) {
846
966
  }
847
967
  const email = args[0] && !args[0].startsWith("--") ? args[0] : flag("email");
848
968
  const verificationRunId = flag("verification-run-id");
849
- if (!email && !verificationRunId) fail("Usage: signaliz verify-email [jane@example.com] [--verification-run-id run_...]");
969
+ if (!email && !verificationRunId) {
970
+ throw new CliInputError("Usage: signaliz verify-email [jane@example.com] [--verification-run-id run_...]");
971
+ }
850
972
  const options = {
851
973
  skipCache: hasFlag("skip-cache"),
852
974
  verificationRunId,
@@ -856,11 +978,11 @@ async function verifyEmail(args) {
856
978
  idempotencyKey: flag("idempotency-key")
857
979
  };
858
980
  if (hasFlag("dry-run")) {
859
- const result2 = await createClient().verifyEmail(email, { ...options, dryRun: true });
981
+ const result2 = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, { ...options, dryRun: true });
860
982
  dryRunOutput(result2);
861
983
  return;
862
984
  }
863
- const result = await createClient().verifyEmail(email, options);
985
+ const result = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, options);
864
986
  const publicResult = customerEmailResult(result, "verify");
865
987
  if (outputFailedCoreProduct(publicResult, "Email verification failed")) return;
866
988
  output(publicResult, () => {
@@ -918,7 +1040,9 @@ async function companySignals() {
918
1040
  "concurrency"
919
1041
  ].filter(hasFlag);
920
1042
  if (conflicts.length > 0) {
921
- fail(`Company Signals --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
1043
+ throw new CliInputError(
1044
+ `Company Signals --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
1045
+ );
922
1046
  }
923
1047
  const result2 = await createClient().resumeCompanySignalBatch(jobId, {
924
1048
  pageSize: numberFlag("page-size"),
@@ -960,9 +1084,46 @@ async function companySignals() {
960
1084
  for (const request2 of requests) {
961
1085
  requireIntegerInRange(request2.targetSignalCount, "targetSignalCount", 1, 15);
962
1086
  requireIntegerInRange(request2.lookbackDays, "lookbackDays", 1, 365);
1087
+ requireIntegerInRange(request2.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1088
+ }
1089
+ if (durableBatch && requests.some((request2) => request2.includeCandidateEvidence === true)) {
1090
+ throw new CliInputError(
1091
+ "includeCandidateEvidence is not supported in Company Signals batches larger than 25 because rejected evidence diagnostics are intentionally excluded from durable scale storage; split diagnostic requests into batches of at most 25."
1092
+ );
1093
+ }
1094
+ if (durableBatch) {
1095
+ const controlKey = (request2) => JSON.stringify({
1096
+ researchPrompt: request2.researchPrompt,
1097
+ signalTypes: request2.signalTypes,
1098
+ targetSignalCount: request2.targetSignalCount,
1099
+ lookbackDays: request2.lookbackDays,
1100
+ online: request2.online,
1101
+ enableDeepSearch: request2.enableDeepSearch,
1102
+ searchMode: request2.searchMode ?? "balanced",
1103
+ includeSummary: request2.includeSummary,
1104
+ enableOutreachIntelligence: request2.enableOutreachIntelligence,
1105
+ enablePredictiveIntelligence: request2.enablePredictiveIntelligence,
1106
+ skipCache: request2.skipCache,
1107
+ includeCandidateEvidence: request2.includeCandidateEvidence
1108
+ });
1109
+ const firstControlKey = controlKey(requests[0]);
1110
+ const incompatible = requests.some(
1111
+ (request2) => Boolean(stringValue(request2.signalRunId)) || Boolean(stringValue(request2.idempotencyKey)) || !stringValue(request2.companyDomain) && !stringValue(request2.companyName) || controlKey(request2) !== firstControlKey
1112
+ );
1113
+ if (incompatible) {
1114
+ throw new CliInputError(
1115
+ "Company Signals batches larger than 25 require homogeneous new-enrichment rows with company identity only and uniform controls; split recovery or mixed-control rows into batches of at most 25."
1116
+ );
1117
+ }
1118
+ } else if (hasFlag("no-wait") && !hasFlag("dry-run")) {
1119
+ throw new CliInputError(
1120
+ "Company Signals --no-wait requires more than 25 homogeneous new-enrichment rows; recovery reads and mixed controls must use batches of at most 25."
1121
+ );
963
1122
  }
964
1123
  if (hasFlag("dry-run")) {
965
- if (hasFlag("no-wait")) fail("Company Signals --no-wait cannot be combined with --dry-run.");
1124
+ if (hasFlag("no-wait")) {
1125
+ throw new CliInputError("Company Signals --no-wait cannot be combined with --dry-run.");
1126
+ }
966
1127
  const result3 = await createClient().enrichCompanies(requests, { ...batchOptions(), dryRun: true });
967
1128
  dryRunOutput(result3);
968
1129
  return;
@@ -979,12 +1140,18 @@ async function companySignals() {
979
1140
  batchOutput(customerSignalBatch(result2));
980
1141
  return;
981
1142
  }
982
- if (hasFlag("no-wait")) fail("Company Signals --no-wait requires --input with more than 25 homogeneous rows.");
983
- if (hasFlag("poll-interval-ms")) fail("Company Signals --poll-interval-ms requires --input or --job-id.");
1143
+ if (hasFlag("no-wait")) {
1144
+ throw new CliInputError("Company Signals --no-wait requires --input with more than 25 homogeneous rows.");
1145
+ }
1146
+ if (hasFlag("poll-interval-ms")) {
1147
+ throw new CliInputError("Company Signals --poll-interval-ms requires --input or --job-id.");
1148
+ }
984
1149
  const companyDomain = flag("domain") || flag("company-domain");
985
1150
  const companyName = flag("company-name");
986
1151
  const signalRunId = flag("signal-run-id");
987
- if (!companyDomain && !companyName && !signalRunId) fail("Usage: signaliz company-signals --domain example.com");
1152
+ if (!companyDomain && !companyName && !signalRunId) {
1153
+ throw new CliInputError("Usage: signaliz company-signals --domain example.com");
1154
+ }
988
1155
  const request = {
989
1156
  signalRunId,
990
1157
  companyDomain,
@@ -1004,6 +1171,9 @@ async function companySignals() {
1004
1171
  maxWaitMs: numberFlag("max-wait-ms"),
1005
1172
  idempotencyKey: flag("idempotency-key")
1006
1173
  };
1174
+ requireIntegerInRange(request.targetSignalCount, "targetSignalCount", 1, 15);
1175
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1176
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1007
1177
  if (hasFlag("dry-run")) {
1008
1178
  const result2 = await createClient().enrichCompanySignals({ ...request, dryRun: true });
1009
1179
  dryRunOutput(result2);
@@ -1167,7 +1337,9 @@ async function signalToCopy() {
1167
1337
  "concurrency"
1168
1338
  ].filter(hasFlag);
1169
1339
  if (conflicts.length > 0) {
1170
- fail(`Signal to Copy --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
1340
+ throw new CliInputError(
1341
+ `Signal to Copy --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
1342
+ );
1171
1343
  }
1172
1344
  const result2 = await createClient().resumeSignalCopyBatch(jobId, {
1173
1345
  pageSize: numberFlag("page-size"),
@@ -1199,8 +1371,27 @@ async function signalToCopy() {
1199
1371
  idempotencyKey: row.idempotencyKey || row.idempotency_key
1200
1372
  };
1201
1373
  });
1374
+ for (const request2 of requests) {
1375
+ requireIntegerInRange(request2.lookbackDays, "lookbackDays", 1, 365);
1376
+ requireIntegerInRange(request2.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1377
+ requireSignalToCopyIdentity(request2);
1378
+ }
1379
+ if (durableBatch) {
1380
+ const invalidIndex = requests.findIndex(
1381
+ (request2) => request2.skipCache === true || request2.enableDeepSearch === true || Boolean(stringValue(request2.idempotencyKey)) || request2.maxWaitMs !== void 0 || !stringValue(request2.companyDomain) || !stringValue(request2.personName) || !stringValue(request2.title) || !stringValue(request2.campaignOffer)
1382
+ );
1383
+ if (invalidIndex >= 0) {
1384
+ throw new CliInputError(
1385
+ `Signal to Copy batches larger than 25 are available-data-only; row ${invalidIndex} requests fresh/deep/synchronous controls or lacks required recipient fields. Split fresh requests into batches of at most 25.`
1386
+ );
1387
+ }
1388
+ } else if (hasFlag("no-wait") && !hasFlag("dry-run")) {
1389
+ throw new CliInputError("Signal to Copy --no-wait requires a durable batch of more than 25 rows.");
1390
+ }
1202
1391
  if (hasFlag("dry-run")) {
1203
- if (hasFlag("no-wait")) fail("Signal to Copy --no-wait cannot be combined with --dry-run.");
1392
+ if (hasFlag("no-wait")) {
1393
+ throw new CliInputError("Signal to Copy --no-wait cannot be combined with --dry-run.");
1394
+ }
1204
1395
  const result3 = await createClient().createSignalCopyBatch(requests, { ...batchOptions(), dryRun: true });
1205
1396
  dryRunOutput(result3);
1206
1397
  return;
@@ -1217,8 +1408,12 @@ async function signalToCopy() {
1217
1408
  batchOutput(customerSignalBatch(result2));
1218
1409
  return;
1219
1410
  }
1220
- if (hasFlag("no-wait")) fail("Signal to Copy --no-wait requires --input with more than 25 available-data-only rows.");
1221
- if (hasFlag("poll-interval-ms")) fail("Signal to Copy --poll-interval-ms requires --input or --job-id.");
1411
+ if (hasFlag("no-wait")) {
1412
+ throw new CliInputError("Signal to Copy --no-wait requires --input with more than 25 available-data-only rows.");
1413
+ }
1414
+ if (hasFlag("poll-interval-ms")) {
1415
+ throw new CliInputError("Signal to Copy --poll-interval-ms requires --input or --job-id.");
1416
+ }
1222
1417
  const companyDomain = flag("company-domain") || flag("domain");
1223
1418
  const personName = flag("person-name");
1224
1419
  const title = flag("title");
@@ -1237,6 +1432,9 @@ async function signalToCopy() {
1237
1432
  maxWaitMs: numberFlag("max-wait-ms"),
1238
1433
  idempotencyKey: flag("idempotency-key")
1239
1434
  };
1435
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1436
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1437
+ requireSignalToCopyIdentity(request);
1240
1438
  if (hasFlag("dry-run")) {
1241
1439
  const result2 = await createClient().signalToCopy({ ...request, dryRun: true });
1242
1440
  dryRunOutput(result2);
@@ -1340,19 +1538,30 @@ main().catch((error) => {
1340
1538
  const message = error instanceof Error ? error.message : String(error);
1341
1539
  if (hasFlag("json") || hasFlag("jsonl")) {
1342
1540
  const signalizError = error instanceof import_sdk.SignalizError ? error : null;
1541
+ const cliInputError = error instanceof CliInputError;
1542
+ const recoveryReadZeroSpend = signalizError?.details?.retry_strategy === "same_run_recovery" && signalizError.details.credits_used === 0 && signalizError.details.credits_charged === 0 && (process.argv[2] === "find-email" && hasFlag("run-id") || process.argv[2] === "verify-email" && hasFlag("verification-run-id"));
1343
1543
  process.stdout.write(`${stringifyMachineJson({
1344
1544
  success: false,
1345
1545
  error: "product_request_failed",
1346
- error_code: signalizError?.code || (error instanceof RangeError || error instanceof TypeError ? "INPUT_001" : "CLI_ERROR"),
1546
+ error_code: signalizError?.code || (cliInputError ? "INPUT_001" : "CLI_ERROR"),
1347
1547
  message,
1348
1548
  retry_eligible: signalizError?.isRetryable ?? false,
1349
1549
  ...signalizError?.retryAfter !== void 0 ? {
1350
1550
  retry_after: signalizError.retryAfter,
1351
1551
  retry_after_seconds: signalizError.retryAfter
1352
1552
  } : {},
1353
- ...signalizError?.details?.retry_strategy === "new_idempotency_key" ? { retry_strategy: "new_idempotency_key" } : {},
1553
+ ...["new_idempotency_key", "same_run_recovery"].includes(String(signalizError?.details?.retry_strategy || "")) ? { retry_strategy: signalizError?.details?.retry_strategy } : {},
1354
1554
  ...signalizError?.details?.credits_used === 0 ? { credits_used: 0 } : {},
1355
1555
  ...signalizError?.details?.credits_charged === 0 ? { credits_charged: 0 } : {},
1556
+ ...signalizError?.details?.live_provider_called === false || recoveryReadZeroSpend ? { live_provider_called: false } : {},
1557
+ ...signalizError?.details?.estimated_external_cost_usd === 0 || recoveryReadZeroSpend ? { estimated_external_cost_usd: 0 } : {},
1558
+ ...signalizError?.details?.do_not_auto_resubmit === true ? { do_not_auto_resubmit: true } : {},
1559
+ ...cliInputError ? {
1560
+ credits_used: 0,
1561
+ credits_charged: 0,
1562
+ live_provider_called: false,
1563
+ estimated_external_cost_usd: 0
1564
+ } : {},
1356
1565
  ...typeof signalizError?.details?.run_id === "string" ? { run_id: signalizError.details.run_id } : {},
1357
1566
  ...typeof signalizError?.details?.verification_run_id === "string" ? { verification_run_id: signalizError.details.verification_run_id } : {},
1358
1567
  ...typeof signalizError?.details?.job_id === "string" ? { job_id: signalizError.details.job_id } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaliz/cli",
3
- "version": "1.0.60",
3
+ "version": "1.0.62",
4
4
  "description": "Signaliz CLI for Signals Everything, Find Email, Verify Email, Company Signal Enrichment, and Signal to Copy AI.",
5
5
  "bin": {
6
6
  "signaliz": "dist/bin.js"
@@ -29,7 +29,7 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "dependencies": {
32
- "@signaliz/sdk": "^1.0.63"
32
+ "@signaliz/sdk": "^1.0.66"
33
33
  },
34
34
  "devDependencies": {
35
35
  "tsup": "^8.0.0",