@signaliz/cli 1.0.61 → 1.0.63

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 +237 -35
  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,
@@ -238,14 +297,38 @@ function customerEmailResult(value, kind) {
238
297
  result_returned_from: resultReturnedFrom,
239
298
  fresh_enrichment_used: freshEnrichmentUsed,
240
299
  live_provider_called: liveProviderCalled,
300
+ billing_replayed: typeof raw.billing_replayed === "boolean" ? raw.billing_replayed : result.billingReplayed === true,
301
+ ...typeof raw.idempotency_replayed === "boolean" || typeof result.idempotencyReplayed === "boolean" ? {
302
+ idempotency_replayed: typeof raw.idempotency_replayed === "boolean" ? raw.idempotency_replayed : result.idempotencyReplayed === true
303
+ } : {},
241
304
  billing_metadata: normalizedBillingMetadata,
242
305
  ...Object.keys(historicalBillingMetadata2).length > 0 ? { historical_billing_metadata: historicalBillingMetadata2 } : {},
243
306
  ...estimatedExternalCostUsd2 !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd2 } : {},
244
- ...result.runId ?? raw.run_id ? { run_id: result.runId ?? raw.run_id } : {},
307
+ ...directRunId ? { run_id: directRunId } : {},
308
+ ...raw.recovery_mode ? {
309
+ recovery_mode: raw.recovery_mode,
310
+ recovery_status: raw.recovery_status ?? "completed"
311
+ } : {},
245
312
  ...raw.cache_publication_status ? {
246
313
  cache_publication_status: raw.cache_publication_status,
247
314
  cache_publication_retry_eligible: raw.cache_publication_retry_eligible === true,
248
315
  ...raw.suggested_action ? { suggested_action: raw.suggested_action } : {}
316
+ } : {},
317
+ ...terminalErrorCode ? {
318
+ email: null,
319
+ success: false,
320
+ found: false,
321
+ is_valid: false,
322
+ verified_for_sending: false,
323
+ is_deliverable: false,
324
+ error_code: terminalErrorCode,
325
+ retry_eligible: retryEligible,
326
+ ...terminalError ? { error: terminalError } : {},
327
+ ...retryEligible && retryStrategy ? { retry_strategy: retryStrategy } : {},
328
+ ...suggestedAction ? { suggested_action: suggestedAction } : {},
329
+ ...result.doNotAutoResubmit === true || raw.do_not_auto_resubmit === true ? { do_not_auto_resubmit: true } : {},
330
+ ...verificationRunId2 ? { verification_run_id: verificationRunId2 } : {},
331
+ ...jobId ? { job_id: jobId } : {}
249
332
  } : {}
250
333
  };
251
334
  }
@@ -307,6 +390,9 @@ function customerEmailResult(value, kind) {
307
390
  fresh_enrichment_used: typeof raw.fresh_enrichment_used === "boolean" ? raw.fresh_enrichment_used : result.freshEnrichmentUsed === true,
308
391
  live_provider_called: typeof raw.live_provider_called === "boolean" ? raw.live_provider_called : result.liveProviderCalled === true,
309
392
  billing_replayed: typeof raw.billing_replayed === "boolean" ? raw.billing_replayed : result.billingReplayed === true,
393
+ ...typeof raw.idempotency_replayed === "boolean" || typeof result.idempotencyReplayed === "boolean" ? {
394
+ idempotency_replayed: typeof raw.idempotency_replayed === "boolean" ? raw.idempotency_replayed : result.idempotencyReplayed === true
395
+ } : {},
310
396
  billing_metadata: billingMetadata,
311
397
  ...Object.keys(historicalBillingMetadata).length > 0 ? { historical_billing_metadata: historicalBillingMetadata } : {},
312
398
  ...estimatedExternalCostUsd !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd } : {},
@@ -375,7 +461,9 @@ function rejectDurableNoWaitPollingFlags(label) {
375
461
  if (!hasFlag("no-wait")) return;
376
462
  const conflicts = ["max-wait-ms", "poll-interval-ms", "concurrency"].filter(hasFlag);
377
463
  if (conflicts.length > 0) {
378
- fail(`${label} --no-wait cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
464
+ throw new CliInputError(
465
+ `${label} --no-wait cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
466
+ );
379
467
  }
380
468
  }
381
469
  function recoverableJobOutput(value, command) {
@@ -454,10 +542,11 @@ function resolveBaseUrl() {
454
542
  const config = loadConfig();
455
543
  return (flag("base-url") || process.env.SIGNALIZ_BASE_URL || process.env.SIGNALIZ_API_URL || config.baseUrl || "https://api.signaliz.com/functions/v1").replace(/\/$/, "");
456
544
  }
457
- function createClient() {
545
+ function createClient(maxRetries) {
458
546
  return new import_sdk.Signaliz({
459
547
  apiKey: resolveApiKey(),
460
- baseUrl: resolveBaseUrl()
548
+ baseUrl: resolveBaseUrl(),
549
+ ...maxRetries !== void 0 ? { maxRetries } : {}
461
550
  });
462
551
  }
463
552
  function numberValue(value) {
@@ -470,6 +559,18 @@ function nullableNumberValue(value) {
470
559
  function stringValue(value) {
471
560
  return typeof value === "string" ? value.trim() : "";
472
561
  }
562
+ function hasFindEmailLinkedInIdentity(value) {
563
+ const linkedinUrl = stringValue(value);
564
+ if (!linkedinUrl) return false;
565
+ 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;
566
+ try {
567
+ const url = new URL(linkedinUrl);
568
+ const hostname = url.hostname.toLowerCase();
569
+ return url.protocol === "https:" && !url.username && !url.password && !url.port && (hostname === "linkedin.com" || hostname.endsWith(".linkedin.com")) && /^\/in\/[^/]+\/?$/i.test(url.pathname);
570
+ } catch {
571
+ return false;
572
+ }
573
+ }
473
574
  function signalsEverythingErrorType(status) {
474
575
  if (status === 400 || status === 422) return "validation";
475
576
  if (status === 401) return "auth_expired";
@@ -716,7 +817,9 @@ async function findEmail() {
716
817
  "skip-cache"
717
818
  ].filter(hasFlag);
718
819
  if (conflicts.length > 0) {
719
- fail(`Find Email --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
820
+ throw new CliInputError(
821
+ `Find Email --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
822
+ );
720
823
  }
721
824
  const result2 = await createClient().resumeFindEmailBatch(jobId, {
722
825
  pageSize: numberFlag("page-size"),
@@ -746,6 +849,15 @@ async function findEmail() {
746
849
  idempotencyKey
747
850
  };
748
851
  });
852
+ const invalidIndexes = requests.flatMap((request2, index) => {
853
+ if (stringValue(request2.runId)) return [];
854
+ if (hasFindEmailLinkedInIdentity(request2.linkedinUrl)) return [];
855
+ const hasName = stringValue(request2.fullName) || stringValue(request2.firstName) && stringValue(request2.lastName);
856
+ return !stringValue(request2.companyDomain) || !hasName ? [index] : [];
857
+ });
858
+ if (invalidIndexes.length === requests.length) {
859
+ 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.");
860
+ }
749
861
  if (hasFlag("dry-run")) {
750
862
  const result3 = await createClient().findEmails(requests, { ...batchOptions(), dryRun: true });
751
863
  dryRunOutput(result3);
@@ -761,8 +873,9 @@ async function findEmail() {
761
873
  const firstName = flag("first-name");
762
874
  const lastName = flag("last-name");
763
875
  const linkedinUrl = flag("linkedin-url");
764
- if (!runId && (!companyDomain || !fullName && !(firstName && lastName))) {
765
- fail('Usage: signaliz find-email --company-domain example.com --full-name "Jane Doe", or signaliz find-email --run-id run_...');
876
+ const hasNameAndDomain = Boolean(companyDomain && (fullName || firstName && lastName));
877
+ if (!runId && !hasFindEmailLinkedInIdentity(linkedinUrl) && !hasNameAndDomain) {
878
+ 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.");
766
879
  }
767
880
  const request = runId ? { runId } : {
768
881
  companyDomain,
@@ -775,11 +888,11 @@ async function findEmail() {
775
888
  idempotencyKey: flag("idempotency-key")
776
889
  };
777
890
  if (hasFlag("dry-run")) {
778
- const result2 = await createClient().findEmail({ ...request, dryRun: true });
891
+ const result2 = await createClient(runId ? 0 : void 0).findEmail({ ...request, dryRun: true });
779
892
  dryRunOutput(result2);
780
893
  return;
781
894
  }
782
- const result = await createClient().findEmail(request);
895
+ const result = await createClient(runId ? 0 : void 0).findEmail(request);
783
896
  const publicResult = customerEmailResult(result, "find");
784
897
  if (outputFailedCoreProduct(publicResult, "No verified email found")) return;
785
898
  output(publicResult, () => {
@@ -819,7 +932,7 @@ async function verifyEmail(args) {
819
932
  ...positionalEmail ? ["email argument"] : [],
820
933
  ...conflicts.map((name) => `--${name}`)
821
934
  ];
822
- fail(`Verify Email --job-id cannot be combined with ${conflictList.join(", ")}.`);
935
+ throw new CliInputError(`Verify Email --job-id cannot be combined with ${conflictList.join(", ")}.`);
823
936
  }
824
937
  const result2 = await createClient().resumeVerifyEmailBatch(jobId, {
825
938
  pageSize: numberFlag("page-size"),
@@ -860,7 +973,9 @@ async function verifyEmail(args) {
860
973
  }
861
974
  const email = args[0] && !args[0].startsWith("--") ? args[0] : flag("email");
862
975
  const verificationRunId = flag("verification-run-id");
863
- if (!email && !verificationRunId) fail("Usage: signaliz verify-email [jane@example.com] [--verification-run-id run_...]");
976
+ if (!email && !verificationRunId) {
977
+ throw new CliInputError("Usage: signaliz verify-email [jane@example.com] [--verification-run-id run_...]");
978
+ }
864
979
  const options = {
865
980
  skipCache: hasFlag("skip-cache"),
866
981
  verificationRunId,
@@ -870,11 +985,11 @@ async function verifyEmail(args) {
870
985
  idempotencyKey: flag("idempotency-key")
871
986
  };
872
987
  if (hasFlag("dry-run")) {
873
- const result2 = await createClient().verifyEmail(email, { ...options, dryRun: true });
988
+ const result2 = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, { ...options, dryRun: true });
874
989
  dryRunOutput(result2);
875
990
  return;
876
991
  }
877
- const result = await createClient().verifyEmail(email, options);
992
+ const result = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, options);
878
993
  const publicResult = customerEmailResult(result, "verify");
879
994
  if (outputFailedCoreProduct(publicResult, "Email verification failed")) return;
880
995
  output(publicResult, () => {
@@ -932,7 +1047,9 @@ async function companySignals() {
932
1047
  "concurrency"
933
1048
  ].filter(hasFlag);
934
1049
  if (conflicts.length > 0) {
935
- fail(`Company Signals --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
1050
+ throw new CliInputError(
1051
+ `Company Signals --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
1052
+ );
936
1053
  }
937
1054
  const result2 = await createClient().resumeCompanySignalBatch(jobId, {
938
1055
  pageSize: numberFlag("page-size"),
@@ -974,9 +1091,46 @@ async function companySignals() {
974
1091
  for (const request2 of requests) {
975
1092
  requireIntegerInRange(request2.targetSignalCount, "targetSignalCount", 1, 15);
976
1093
  requireIntegerInRange(request2.lookbackDays, "lookbackDays", 1, 365);
1094
+ requireIntegerInRange(request2.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1095
+ }
1096
+ if (durableBatch && requests.some((request2) => request2.includeCandidateEvidence === true)) {
1097
+ throw new CliInputError(
1098
+ "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."
1099
+ );
1100
+ }
1101
+ if (durableBatch) {
1102
+ const controlKey = (request2) => JSON.stringify({
1103
+ researchPrompt: request2.researchPrompt,
1104
+ signalTypes: request2.signalTypes,
1105
+ targetSignalCount: request2.targetSignalCount,
1106
+ lookbackDays: request2.lookbackDays,
1107
+ online: request2.online,
1108
+ enableDeepSearch: request2.enableDeepSearch,
1109
+ searchMode: request2.searchMode ?? "balanced",
1110
+ includeSummary: request2.includeSummary,
1111
+ enableOutreachIntelligence: request2.enableOutreachIntelligence,
1112
+ enablePredictiveIntelligence: request2.enablePredictiveIntelligence,
1113
+ skipCache: request2.skipCache,
1114
+ includeCandidateEvidence: request2.includeCandidateEvidence
1115
+ });
1116
+ const firstControlKey = controlKey(requests[0]);
1117
+ const incompatible = requests.some(
1118
+ (request2) => Boolean(stringValue(request2.signalRunId)) || Boolean(stringValue(request2.idempotencyKey)) || !stringValue(request2.companyDomain) && !stringValue(request2.companyName) || controlKey(request2) !== firstControlKey
1119
+ );
1120
+ if (incompatible) {
1121
+ throw new CliInputError(
1122
+ "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."
1123
+ );
1124
+ }
1125
+ } else if (hasFlag("no-wait") && !hasFlag("dry-run")) {
1126
+ throw new CliInputError(
1127
+ "Company Signals --no-wait requires more than 25 homogeneous new-enrichment rows; recovery reads and mixed controls must use batches of at most 25."
1128
+ );
977
1129
  }
978
1130
  if (hasFlag("dry-run")) {
979
- if (hasFlag("no-wait")) fail("Company Signals --no-wait cannot be combined with --dry-run.");
1131
+ if (hasFlag("no-wait")) {
1132
+ throw new CliInputError("Company Signals --no-wait cannot be combined with --dry-run.");
1133
+ }
980
1134
  const result3 = await createClient().enrichCompanies(requests, { ...batchOptions(), dryRun: true });
981
1135
  dryRunOutput(result3);
982
1136
  return;
@@ -993,12 +1147,18 @@ async function companySignals() {
993
1147
  batchOutput(customerSignalBatch(result2));
994
1148
  return;
995
1149
  }
996
- if (hasFlag("no-wait")) fail("Company Signals --no-wait requires --input with more than 25 homogeneous rows.");
997
- if (hasFlag("poll-interval-ms")) fail("Company Signals --poll-interval-ms requires --input or --job-id.");
1150
+ if (hasFlag("no-wait")) {
1151
+ throw new CliInputError("Company Signals --no-wait requires --input with more than 25 homogeneous rows.");
1152
+ }
1153
+ if (hasFlag("poll-interval-ms")) {
1154
+ throw new CliInputError("Company Signals --poll-interval-ms requires --input or --job-id.");
1155
+ }
998
1156
  const companyDomain = flag("domain") || flag("company-domain");
999
1157
  const companyName = flag("company-name");
1000
1158
  const signalRunId = flag("signal-run-id");
1001
- if (!companyDomain && !companyName && !signalRunId) fail("Usage: signaliz company-signals --domain example.com");
1159
+ if (!companyDomain && !companyName && !signalRunId) {
1160
+ throw new CliInputError("Usage: signaliz company-signals --domain example.com");
1161
+ }
1002
1162
  const request = {
1003
1163
  signalRunId,
1004
1164
  companyDomain,
@@ -1018,6 +1178,9 @@ async function companySignals() {
1018
1178
  maxWaitMs: numberFlag("max-wait-ms"),
1019
1179
  idempotencyKey: flag("idempotency-key")
1020
1180
  };
1181
+ requireIntegerInRange(request.targetSignalCount, "targetSignalCount", 1, 15);
1182
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1183
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1021
1184
  if (hasFlag("dry-run")) {
1022
1185
  const result2 = await createClient().enrichCompanySignals({ ...request, dryRun: true });
1023
1186
  dryRunOutput(result2);
@@ -1181,7 +1344,9 @@ async function signalToCopy() {
1181
1344
  "concurrency"
1182
1345
  ].filter(hasFlag);
1183
1346
  if (conflicts.length > 0) {
1184
- fail(`Signal to Copy --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
1347
+ throw new CliInputError(
1348
+ `Signal to Copy --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`
1349
+ );
1185
1350
  }
1186
1351
  const result2 = await createClient().resumeSignalCopyBatch(jobId, {
1187
1352
  pageSize: numberFlag("page-size"),
@@ -1213,8 +1378,27 @@ async function signalToCopy() {
1213
1378
  idempotencyKey: row.idempotencyKey || row.idempotency_key
1214
1379
  };
1215
1380
  });
1381
+ for (const request2 of requests) {
1382
+ requireIntegerInRange(request2.lookbackDays, "lookbackDays", 1, 365);
1383
+ requireIntegerInRange(request2.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1384
+ requireSignalToCopyIdentity(request2);
1385
+ }
1386
+ if (durableBatch) {
1387
+ const invalidIndex = requests.findIndex(
1388
+ (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)
1389
+ );
1390
+ if (invalidIndex >= 0) {
1391
+ throw new CliInputError(
1392
+ `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.`
1393
+ );
1394
+ }
1395
+ } else if (hasFlag("no-wait") && !hasFlag("dry-run")) {
1396
+ throw new CliInputError("Signal to Copy --no-wait requires a durable batch of more than 25 rows.");
1397
+ }
1216
1398
  if (hasFlag("dry-run")) {
1217
- if (hasFlag("no-wait")) fail("Signal to Copy --no-wait cannot be combined with --dry-run.");
1399
+ if (hasFlag("no-wait")) {
1400
+ throw new CliInputError("Signal to Copy --no-wait cannot be combined with --dry-run.");
1401
+ }
1218
1402
  const result3 = await createClient().createSignalCopyBatch(requests, { ...batchOptions(), dryRun: true });
1219
1403
  dryRunOutput(result3);
1220
1404
  return;
@@ -1231,8 +1415,12 @@ async function signalToCopy() {
1231
1415
  batchOutput(customerSignalBatch(result2));
1232
1416
  return;
1233
1417
  }
1234
- if (hasFlag("no-wait")) fail("Signal to Copy --no-wait requires --input with more than 25 available-data-only rows.");
1235
- if (hasFlag("poll-interval-ms")) fail("Signal to Copy --poll-interval-ms requires --input or --job-id.");
1418
+ if (hasFlag("no-wait")) {
1419
+ throw new CliInputError("Signal to Copy --no-wait requires --input with more than 25 available-data-only rows.");
1420
+ }
1421
+ if (hasFlag("poll-interval-ms")) {
1422
+ throw new CliInputError("Signal to Copy --poll-interval-ms requires --input or --job-id.");
1423
+ }
1236
1424
  const companyDomain = flag("company-domain") || flag("domain");
1237
1425
  const personName = flag("person-name");
1238
1426
  const title = flag("title");
@@ -1251,6 +1439,9 @@ async function signalToCopy() {
1251
1439
  maxWaitMs: numberFlag("max-wait-ms"),
1252
1440
  idempotencyKey: flag("idempotency-key")
1253
1441
  };
1442
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1443
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1444
+ requireSignalToCopyIdentity(request);
1254
1445
  if (hasFlag("dry-run")) {
1255
1446
  const result2 = await createClient().signalToCopy({ ...request, dryRun: true });
1256
1447
  dryRunOutput(result2);
@@ -1354,19 +1545,30 @@ main().catch((error) => {
1354
1545
  const message = error instanceof Error ? error.message : String(error);
1355
1546
  if (hasFlag("json") || hasFlag("jsonl")) {
1356
1547
  const signalizError = error instanceof import_sdk.SignalizError ? error : null;
1548
+ const cliInputError = error instanceof CliInputError;
1549
+ 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"));
1357
1550
  process.stdout.write(`${stringifyMachineJson({
1358
1551
  success: false,
1359
1552
  error: "product_request_failed",
1360
- error_code: signalizError?.code || (error instanceof RangeError || error instanceof TypeError ? "INPUT_001" : "CLI_ERROR"),
1553
+ error_code: signalizError?.code || (cliInputError ? "INPUT_001" : "CLI_ERROR"),
1361
1554
  message,
1362
1555
  retry_eligible: signalizError?.isRetryable ?? false,
1363
1556
  ...signalizError?.retryAfter !== void 0 ? {
1364
1557
  retry_after: signalizError.retryAfter,
1365
1558
  retry_after_seconds: signalizError.retryAfter
1366
1559
  } : {},
1367
- ...signalizError?.details?.retry_strategy === "new_idempotency_key" ? { retry_strategy: "new_idempotency_key" } : {},
1560
+ ...["new_idempotency_key", "same_run_recovery"].includes(String(signalizError?.details?.retry_strategy || "")) ? { retry_strategy: signalizError?.details?.retry_strategy } : {},
1368
1561
  ...signalizError?.details?.credits_used === 0 ? { credits_used: 0 } : {},
1369
1562
  ...signalizError?.details?.credits_charged === 0 ? { credits_charged: 0 } : {},
1563
+ ...signalizError?.details?.live_provider_called === false || recoveryReadZeroSpend ? { live_provider_called: false } : {},
1564
+ ...signalizError?.details?.estimated_external_cost_usd === 0 || recoveryReadZeroSpend ? { estimated_external_cost_usd: 0 } : {},
1565
+ ...signalizError?.details?.do_not_auto_resubmit === true ? { do_not_auto_resubmit: true } : {},
1566
+ ...cliInputError ? {
1567
+ credits_used: 0,
1568
+ credits_charged: 0,
1569
+ live_provider_called: false,
1570
+ estimated_external_cost_usd: 0
1571
+ } : {},
1370
1572
  ...typeof signalizError?.details?.run_id === "string" ? { run_id: signalizError.details.run_id } : {},
1371
1573
  ...typeof signalizError?.details?.verification_run_id === "string" ? { verification_run_id: signalizError.details.verification_run_id } : {},
1372
1574
  ...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.61",
3
+ "version": "1.0.63",
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.67"
33
33
  },
34
34
  "devDependencies": {
35
35
  "tsup": "^8.0.0",