@signaliz/cli 1.0.61 → 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 +230 -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,
@@ -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";
@@ -716,7 +810,9 @@ async function findEmail() {
716
810
  "skip-cache"
717
811
  ].filter(hasFlag);
718
812
  if (conflicts.length > 0) {
719
- 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
+ );
720
816
  }
721
817
  const result2 = await createClient().resumeFindEmailBatch(jobId, {
722
818
  pageSize: numberFlag("page-size"),
@@ -746,6 +842,15 @@ async function findEmail() {
746
842
  idempotencyKey
747
843
  };
748
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
+ }
749
854
  if (hasFlag("dry-run")) {
750
855
  const result3 = await createClient().findEmails(requests, { ...batchOptions(), dryRun: true });
751
856
  dryRunOutput(result3);
@@ -761,8 +866,9 @@ async function findEmail() {
761
866
  const firstName = flag("first-name");
762
867
  const lastName = flag("last-name");
763
868
  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_...');
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.");
766
872
  }
767
873
  const request = runId ? { runId } : {
768
874
  companyDomain,
@@ -775,11 +881,11 @@ async function findEmail() {
775
881
  idempotencyKey: flag("idempotency-key")
776
882
  };
777
883
  if (hasFlag("dry-run")) {
778
- const result2 = await createClient().findEmail({ ...request, dryRun: true });
884
+ const result2 = await createClient(runId ? 0 : void 0).findEmail({ ...request, dryRun: true });
779
885
  dryRunOutput(result2);
780
886
  return;
781
887
  }
782
- const result = await createClient().findEmail(request);
888
+ const result = await createClient(runId ? 0 : void 0).findEmail(request);
783
889
  const publicResult = customerEmailResult(result, "find");
784
890
  if (outputFailedCoreProduct(publicResult, "No verified email found")) return;
785
891
  output(publicResult, () => {
@@ -819,7 +925,7 @@ async function verifyEmail(args) {
819
925
  ...positionalEmail ? ["email argument"] : [],
820
926
  ...conflicts.map((name) => `--${name}`)
821
927
  ];
822
- 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(", ")}.`);
823
929
  }
824
930
  const result2 = await createClient().resumeVerifyEmailBatch(jobId, {
825
931
  pageSize: numberFlag("page-size"),
@@ -860,7 +966,9 @@ async function verifyEmail(args) {
860
966
  }
861
967
  const email = args[0] && !args[0].startsWith("--") ? args[0] : flag("email");
862
968
  const verificationRunId = flag("verification-run-id");
863
- 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
+ }
864
972
  const options = {
865
973
  skipCache: hasFlag("skip-cache"),
866
974
  verificationRunId,
@@ -870,11 +978,11 @@ async function verifyEmail(args) {
870
978
  idempotencyKey: flag("idempotency-key")
871
979
  };
872
980
  if (hasFlag("dry-run")) {
873
- const result2 = await createClient().verifyEmail(email, { ...options, dryRun: true });
981
+ const result2 = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, { ...options, dryRun: true });
874
982
  dryRunOutput(result2);
875
983
  return;
876
984
  }
877
- const result = await createClient().verifyEmail(email, options);
985
+ const result = await createClient(verificationRunId ? 0 : void 0).verifyEmail(email, options);
878
986
  const publicResult = customerEmailResult(result, "verify");
879
987
  if (outputFailedCoreProduct(publicResult, "Email verification failed")) return;
880
988
  output(publicResult, () => {
@@ -932,7 +1040,9 @@ async function companySignals() {
932
1040
  "concurrency"
933
1041
  ].filter(hasFlag);
934
1042
  if (conflicts.length > 0) {
935
- 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
+ );
936
1046
  }
937
1047
  const result2 = await createClient().resumeCompanySignalBatch(jobId, {
938
1048
  pageSize: numberFlag("page-size"),
@@ -974,9 +1084,46 @@ async function companySignals() {
974
1084
  for (const request2 of requests) {
975
1085
  requireIntegerInRange(request2.targetSignalCount, "targetSignalCount", 1, 15);
976
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
+ );
977
1122
  }
978
1123
  if (hasFlag("dry-run")) {
979
- 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
+ }
980
1127
  const result3 = await createClient().enrichCompanies(requests, { ...batchOptions(), dryRun: true });
981
1128
  dryRunOutput(result3);
982
1129
  return;
@@ -993,12 +1140,18 @@ async function companySignals() {
993
1140
  batchOutput(customerSignalBatch(result2));
994
1141
  return;
995
1142
  }
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.");
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
+ }
998
1149
  const companyDomain = flag("domain") || flag("company-domain");
999
1150
  const companyName = flag("company-name");
1000
1151
  const signalRunId = flag("signal-run-id");
1001
- 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
+ }
1002
1155
  const request = {
1003
1156
  signalRunId,
1004
1157
  companyDomain,
@@ -1018,6 +1171,9 @@ async function companySignals() {
1018
1171
  maxWaitMs: numberFlag("max-wait-ms"),
1019
1172
  idempotencyKey: flag("idempotency-key")
1020
1173
  };
1174
+ requireIntegerInRange(request.targetSignalCount, "targetSignalCount", 1, 15);
1175
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1176
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1021
1177
  if (hasFlag("dry-run")) {
1022
1178
  const result2 = await createClient().enrichCompanySignals({ ...request, dryRun: true });
1023
1179
  dryRunOutput(result2);
@@ -1181,7 +1337,9 @@ async function signalToCopy() {
1181
1337
  "concurrency"
1182
1338
  ].filter(hasFlag);
1183
1339
  if (conflicts.length > 0) {
1184
- 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
+ );
1185
1343
  }
1186
1344
  const result2 = await createClient().resumeSignalCopyBatch(jobId, {
1187
1345
  pageSize: numberFlag("page-size"),
@@ -1213,8 +1371,27 @@ async function signalToCopy() {
1213
1371
  idempotencyKey: row.idempotencyKey || row.idempotency_key
1214
1372
  };
1215
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
+ }
1216
1391
  if (hasFlag("dry-run")) {
1217
- 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
+ }
1218
1395
  const result3 = await createClient().createSignalCopyBatch(requests, { ...batchOptions(), dryRun: true });
1219
1396
  dryRunOutput(result3);
1220
1397
  return;
@@ -1231,8 +1408,12 @@ async function signalToCopy() {
1231
1408
  batchOutput(customerSignalBatch(result2));
1232
1409
  return;
1233
1410
  }
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.");
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
+ }
1236
1417
  const companyDomain = flag("company-domain") || flag("domain");
1237
1418
  const personName = flag("person-name");
1238
1419
  const title = flag("title");
@@ -1251,6 +1432,9 @@ async function signalToCopy() {
1251
1432
  maxWaitMs: numberFlag("max-wait-ms"),
1252
1433
  idempotencyKey: flag("idempotency-key")
1253
1434
  };
1435
+ requireIntegerInRange(request.lookbackDays, "lookbackDays", 1, 365);
1436
+ requireIntegerInRange(request.maxWaitMs, "maxWaitMs", 1e4, 12e4);
1437
+ requireSignalToCopyIdentity(request);
1254
1438
  if (hasFlag("dry-run")) {
1255
1439
  const result2 = await createClient().signalToCopy({ ...request, dryRun: true });
1256
1440
  dryRunOutput(result2);
@@ -1354,19 +1538,30 @@ main().catch((error) => {
1354
1538
  const message = error instanceof Error ? error.message : String(error);
1355
1539
  if (hasFlag("json") || hasFlag("jsonl")) {
1356
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"));
1357
1543
  process.stdout.write(`${stringifyMachineJson({
1358
1544
  success: false,
1359
1545
  error: "product_request_failed",
1360
- error_code: signalizError?.code || (error instanceof RangeError || error instanceof TypeError ? "INPUT_001" : "CLI_ERROR"),
1546
+ error_code: signalizError?.code || (cliInputError ? "INPUT_001" : "CLI_ERROR"),
1361
1547
  message,
1362
1548
  retry_eligible: signalizError?.isRetryable ?? false,
1363
1549
  ...signalizError?.retryAfter !== void 0 ? {
1364
1550
  retry_after: signalizError.retryAfter,
1365
1551
  retry_after_seconds: signalizError.retryAfter
1366
1552
  } : {},
1367
- ...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 } : {},
1368
1554
  ...signalizError?.details?.credits_used === 0 ? { credits_used: 0 } : {},
1369
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
+ } : {},
1370
1565
  ...typeof signalizError?.details?.run_id === "string" ? { run_id: signalizError.details.run_id } : {},
1371
1566
  ...typeof signalizError?.details?.verification_run_id === "string" ? { verification_run_id: signalizError.details.verification_run_id } : {},
1372
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.61",
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",