@signaliz/cli 1.0.57 → 1.0.58

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 +29 -14
  2. package/dist/bin.js +393 -27
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -55,8 +55,20 @@ signaliz find-email --input contacts.jsonl --concurrency 20 --json
55
55
  signaliz verify-email --input emails.json --concurrency 20 --json
56
56
  signaliz company-signals --input companies.jsonl --concurrency 5 --json
57
57
  signaliz signal-to-copy --input copy-requests.jsonl --concurrency 5 --json
58
+
59
+ # Yield an agent turn immediately for a durable >25-row batch.
60
+ # The response includes both job_id and the actual idempotency_key.
61
+ signaliz company-signals --input companies.jsonl --no-wait --json
62
+ signaliz company-signals --job-id job_... --idempotency-key key_... --json
63
+ signaliz signal-to-copy --input copy-requests.jsonl --no-wait --json
64
+ signaliz signal-to-copy --job-id job_... --idempotency-key key_... --json
58
65
  ```
59
66
 
67
+ Retrieve completed Company/Copy result pages within seven days. If retention
68
+ expires, the CLI returns non-retryable `BATCH_RESULTS_EXPIRED`; do not
69
+ automatically rerun the submission because provider work may already have been
70
+ performed.
71
+
60
72
  The CLI compacts exact duplicate successes by default: the first row contains
61
73
  the full result and repeats contain `duplicateOf` pointing to its zero-based
62
74
  input index. Use `--expand-duplicates` only when every repeated row must contain
@@ -97,22 +109,25 @@ they reduce the ceiling.
97
109
 
98
110
  Batch input is capped at 5,000 records, concurrency is bounded to `1-50`, input
99
111
  order is preserved, and a failed item does not abort successful items.
100
- Exact duplicate tasks are single-flighted across the full input before
101
- transport. The four row-oriented core products use one recoverable REST job for inputs larger
102
- than 25; the CLI waits for completion and retrieves every result page before it
103
- emits complete results or terminal per-row errors. Find Email, Verify Email, and
104
- Signal to Copy use 500-row pages, while Company Signal Enrichment uses 25-row
105
- pages. Durable Signal to Copy jobs use available company-signal data only. If a
106
- Company Signal row contains `signal_run_id`, the CLI preserves the recovery-read
107
- contract with direct batches of at most 25. If a
108
- copy row explicitly requests fresh or deep research, the CLI preserves that
109
- behavior with synchronous chunks of at most 25. Signal to Copy also shares
110
- company research across recipients.
112
+ Synchronous exact duplicates are single-flighted. Durable Company/Copy
113
+ submissions preserve every row and index so idempotency stays stable across
114
+ REST, MCP, SDK, and CLI recovery. The four row-oriented core products use one
115
+ recoverable REST job for inputs larger than 25; the CLI waits by default or
116
+ returns `job_id`, `idempotency_key`, and an exact resume command with
117
+ `--no-wait`. Find Email, Verify Email, and Company Signal Enrichment use 25-row
118
+ pages; Signal to Copy uses 100-row pages. Durable Signal to Copy jobs use
119
+ persisted, eligible company-signal data only and never start live providers or
120
+ deep research. Large Copy inputs with fresh, deep, skip-cache, per-row
121
+ idempotency, or synchronous-only controls fail locally before HTTP; split those
122
+ requests into batches of at most 25. Company Signal `signal_run_id` recovery
123
+ rows likewise stay in direct batches of at most 25. Large Company Signal jobs
124
+ persist bounded result pages and exclude rejected candidate-evidence
125
+ diagnostics; use `--include-candidate-evidence` only with batches of at most 25.
111
126
 
112
127
  Every command uses the authenticated workspace. Find Email and Verify Email
113
128
  honor that workspace's configured cache window; eligible hits cost 0 credits,
114
- while fresh discovery or verification is charged on every plan. Team, Agency,
115
- and Pay-As-You-Go include fresh Company Signals and Signal to Copy. Those plans
129
+ while fresh discovery or verification is charged on every plan. Builder, Team,
130
+ Agency, and Pay-As-You-Go include fresh Company Signals and Signal to Copy. Those plans
116
131
  still check eligible workspace signal data first even when Company Signals is
117
132
  set to No cache. `--lookback-days` remains a hard evidence-age bound for both
118
133
  cached and fresh Company Signals and Signal to Copy results.
@@ -120,7 +135,7 @@ cached and fresh Company Signals and Signal to Copy results.
120
135
  Signals Everything is a query-first company discovery command rather than a
121
136
  row batch. It returns only dated, source-linked signals that contain both a
122
137
  company name and company domain. The default request is 100 companies and the
123
- maximum is 1,000. It is included on Team and Agency plans, is resumable with
138
+ maximum is 1,000. It is included on Builder, Team, and Agency plans, is resumable with
124
139
  `signal_search_run_id`, and reports a strict source-cost guardrail: results are
125
140
  only returned when source cost is below $0.01 per qualified company. A request
126
141
  is a ceiling, not a promise: it can return fewer results when public evidence
package/dist/bin.js CHANGED
@@ -14,13 +14,19 @@ var HELP = `Signaliz CLI
14
14
  Five product commands:
15
15
  signaliz find-email --company-domain example.com --full-name "Jane Doe" [--skip-cache]
16
16
  signaliz find-email --run-id run_... Resume an existing Find Email run without duplicate spend
17
+ signaliz find-email --job-id job_... Resume an existing Find Email batch without redispatch
17
18
  signaliz verify-email [jane@example.com] [--verification-run-id run_...] [--no-wait] [--max-wait-ms 600000]
19
+ signaliz verify-email --job-id job_... Resume an existing Verify Email batch without redispatch
18
20
  signaliz company-signals --domain example.com [--research-prompt "..."] [--search-mode fast|balanced|coverage] [--include-candidate-evidence] [--no-summary] [--max-wait-ms 120000]
21
+ signaliz company-signals --input companies.json --no-wait Submit a durable >25-row batch and return its job
22
+ signaliz company-signals --job-id job_... Resume an existing Company Signals batch without redispatch
19
23
  signaliz company-signals --signal-run-id run_... Read a completed run without duplicate spend
20
24
  signaliz signal-to-copy --company-domain example.com --person-name "Jane Doe" \\
21
25
  --title "VP Sales" --campaign-offer "pipeline intelligence" [--signal-run-id run_...] \\
22
26
  [--lookback-days 90] [--skip-cache] [--deep-search] [--max-wait-ms 120000]
23
27
  signaliz signal-to-copy --signal-run-id copy_... Reuse a completed signal run
28
+ signaliz signal-to-copy --input recipients.json --no-wait Submit an available-data-only >25-row batch
29
+ signaliz signal-to-copy --job-id job_... Resume an existing Signal to Copy batch without redispatch
24
30
  signaliz signals "companies that just got funded" [--icp-context "..."] [--limit 100] [--sources web,news] [--wait]
25
31
  signaliz signals --signal-search-run-id run_... Resume an existing company-attributed signal search
26
32
 
@@ -44,10 +50,11 @@ Common flags:
44
50
  --concurrency <1-50> Simultaneous API requests for batch input (default: 10)
45
51
  --idempotency-key <key> Reuse after an ambiguous response to recover the same request or job
46
52
  --run-id <run_...> Resume one existing Find Email run without dispatching new work
53
+ --job-id <job_...> Resume an existing core-product batch without dispatching new work
47
54
  --icp-context <text> Optional ICP, product, or pain context that returned signals must support
48
55
  --dry-run Return a no-spend plan without provider calls, jobs, or writes
49
56
 
50
- Company Signals and Signal to Copy always wait for complete results or terminal errors. Signals Everything is asynchronous by default; use --wait to poll until it completes.
57
+ Company Signals and Signal to Copy wait by default. For a durable batch larger than 25 rows, use --no-wait to return job_id + idempotency_key immediately, then resume with --job-id. Large Company Signal batches exclude candidate-evidence diagnostics; split --include-candidate-evidence requests into batches of at most 25. Large Signal to Copy batches are available-data-only. Signals Everything is asynchronous by default; use --wait to poll until it completes.
51
58
  `;
52
59
  function loadConfig() {
53
60
  if (!(0, import_node_fs.existsSync)(CONFIG_FILE)) return {};
@@ -127,37 +134,191 @@ function readBatchInput() {
127
134
  function record(value) {
128
135
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
129
136
  }
130
- function customerEmailResult(value) {
137
+ function safeNonNegativeNumber(value) {
138
+ if (value === null || value === void 0 || value === "") return null;
139
+ const numeric = Number(value);
140
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
141
+ }
142
+ function customerEmailBillingMetadata(value) {
143
+ const source = record(value);
144
+ const safe = {};
145
+ if (typeof source.result_returned_from === "string") safe.result_returned_from = source.result_returned_from;
146
+ for (const field of ["cache_hit", "fresh_enrichment_used", "live_provider_called", "openrouter_called", "byo_openrouter_used"]) {
147
+ if (typeof source[field] === "boolean") safe[field] = source[field];
148
+ }
149
+ for (const field of ["credits_used", "credits_charged", "estimated_external_cost_usd"]) {
150
+ const numeric = safeNonNegativeNumber(source[field]);
151
+ if (numeric !== null) safe[field] = numeric;
152
+ }
153
+ return safe;
154
+ }
155
+ function customerEmailResult(value, kind) {
131
156
  const result = record(value);
132
157
  const raw = record(result.raw);
133
158
  if (String(result.status || "").toLowerCase() === "processing") {
134
- const verificationRunId = result.verificationRunId ?? raw.verification_run_id;
135
- const runId = result.runId ?? raw.run_id ?? verificationRunId;
159
+ const verificationRunId2 = result.verificationRunId ?? raw.verification_run_id;
160
+ const runId2 = result.runId ?? raw.run_id ?? verificationRunId2;
136
161
  return {
137
162
  success: true,
138
163
  status: "processing",
139
164
  email: null,
140
- run_id: runId,
141
- ...verificationRunId ? { verification_run_id: verificationRunId } : {},
165
+ run_id: runId2,
166
+ ...verificationRunId2 ? { verification_run_id: verificationRunId2 } : {},
142
167
  next_poll_after_seconds: result.nextPollAfterSeconds ?? raw.next_poll_after_seconds,
143
- suggested_action: result.suggestedAction ?? raw.suggested_action ?? (verificationRunId ? "retry_with_verification_run_id" : "retry_with_run_id")
168
+ suggested_action: result.suggestedAction ?? raw.suggested_action ?? (verificationRunId2 ? "retry_with_verification_run_id" : "retry_with_run_id")
144
169
  };
145
170
  }
146
171
  const creditsUsed = Number(result.creditsUsed ?? raw.credits_used ?? 0);
147
- return {
172
+ const base = {
148
173
  email: typeof result.email === "string" && result.email.trim() ? result.email.trim() : null,
149
174
  status: "completed",
150
175
  success: result.success === true,
151
- is_valid: typeof raw.is_valid === "boolean" ? raw.is_valid : result.isValid ?? result.isVerified ?? false,
176
+ is_valid: typeof result.isValid === "boolean" ? result.isValid : typeof raw.is_valid === "boolean" ? raw.is_valid : result.isVerified ?? false,
152
177
  credits_used: Number.isFinite(creditsUsed) && creditsUsed >= 0 ? creditsUsed : 0
153
178
  };
179
+ if (kind === "find") {
180
+ const verifiedForSending2 = typeof result.isVerifiedForSending === "boolean" ? result.isVerifiedForSending : raw.verified_for_sending === true;
181
+ const isCatchAll2 = typeof result.isCatchAll === "boolean" ? result.isCatchAll : raw.email_is_catch_all === true || raw.is_catch_all === true;
182
+ const verificationStatus2 = String(
183
+ result.verificationStatus ?? raw.verification_status ?? raw.deliverability_status ?? (verifiedForSending2 ? "valid" : "unknown")
184
+ ).toLowerCase();
185
+ const deliverabilityStatus2 = String(
186
+ result.deliverabilityStatus ?? raw.deliverability_status ?? verificationStatus2
187
+ ).toLowerCase();
188
+ const verificationVerdict2 = String(
189
+ result.verificationVerdict ?? raw.verification_verdict ?? verificationStatus2
190
+ ).toLowerCase();
191
+ const billingMetadata2 = customerEmailBillingMetadata(
192
+ Object.keys(record(result.billingMetadata)).length > 0 ? result.billingMetadata : raw.billing_metadata
193
+ );
194
+ const historicalBillingMetadata2 = customerEmailBillingMetadata(
195
+ Object.keys(record(result.historicalBillingMetadata)).length > 0 ? result.historicalBillingMetadata : raw.historical_billing_metadata
196
+ );
197
+ const cacheHit2 = typeof raw.cache_hit === "boolean" ? raw.cache_hit : result.cacheHit === true;
198
+ const negativeCacheHit2 = typeof raw.negative_cache_hit === "boolean" ? raw.negative_cache_hit : result.negativeCacheHit === true;
199
+ const resultReturnedFrom = result.resultReturnedFrom ?? raw.result_returned_from ?? null;
200
+ const freshEnrichmentUsed = typeof raw.fresh_enrichment_used === "boolean" ? raw.fresh_enrichment_used : result.freshEnrichmentUsed === true;
201
+ const liveProviderCalled = typeof raw.live_provider_called === "boolean" ? raw.live_provider_called : result.liveProviderCalled === true;
202
+ const normalizedBillingMetadata = {
203
+ ...billingMetadata2,
204
+ ...resultReturnedFrom ? { result_returned_from: resultReturnedFrom } : {},
205
+ cache_hit: cacheHit2,
206
+ fresh_enrichment_used: freshEnrichmentUsed,
207
+ credits_used: base.credits_used,
208
+ live_provider_called: liveProviderCalled
209
+ };
210
+ const estimatedExternalCostUsd2 = safeNonNegativeNumber(
211
+ result.estimatedExternalCostUsd ?? raw.estimated_external_cost_usd
212
+ );
213
+ return {
214
+ ...base,
215
+ is_valid: verifiedForSending2,
216
+ verification_status: verificationStatus2,
217
+ deliverability_status: deliverabilityStatus2,
218
+ verification_verdict: verificationVerdict2,
219
+ verified_for_sending: verifiedForSending2,
220
+ is_deliverable: typeof result.isDeliverable === "boolean" ? result.isDeliverable && verifiedForSending2 : verifiedForSending2,
221
+ email_is_catch_all: isCatchAll2,
222
+ is_catch_all: isCatchAll2,
223
+ verification_source: result.verificationSource ?? raw.verification_source ?? result.providerUsed ?? "email_finder",
224
+ ...result.verificationObservedAt ?? raw.verification_observed_at ? { verification_observed_at: result.verificationObservedAt ?? raw.verification_observed_at } : {},
225
+ failure_reason: result.failureReason ?? raw.failure_reason ?? (verifiedForSending2 ? "none" : "no_verified_email_found"),
226
+ cache_hit: cacheHit2,
227
+ negative_cache_hit: negativeCacheHit2,
228
+ negative_cache_ttl_seconds: safeNonNegativeNumber(
229
+ result.negativeCacheTtlSeconds ?? raw.negative_cache_ttl_seconds
230
+ ),
231
+ cache_tier: result.cacheTier ?? raw.cache_tier ?? null,
232
+ result_returned_from: resultReturnedFrom,
233
+ fresh_enrichment_used: freshEnrichmentUsed,
234
+ live_provider_called: liveProviderCalled,
235
+ billing_metadata: normalizedBillingMetadata,
236
+ ...Object.keys(historicalBillingMetadata2).length > 0 ? { historical_billing_metadata: historicalBillingMetadata2 } : {},
237
+ ...estimatedExternalCostUsd2 !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd2 } : {},
238
+ ...result.runId ?? raw.run_id ? { run_id: result.runId ?? raw.run_id } : {},
239
+ ...raw.cache_publication_status ? {
240
+ cache_publication_status: raw.cache_publication_status,
241
+ cache_publication_retry_eligible: raw.cache_publication_retry_eligible === true,
242
+ ...raw.suggested_action ? { suggested_action: raw.suggested_action } : {}
243
+ } : {}
244
+ };
245
+ }
246
+ const verificationStatus = String(
247
+ result.verificationStatus ?? raw.verification_status ?? result.verificationVerdict ?? raw.verification_verdict ?? raw.deliverability_status ?? "unknown"
248
+ ).toLowerCase();
249
+ const deliverabilityStatus = String(
250
+ result.deliverabilityStatus ?? raw.deliverability_status ?? verificationStatus
251
+ ).toLowerCase();
252
+ const verificationVerdict = String(
253
+ result.verificationVerdict ?? raw.verification_verdict ?? verificationStatus
254
+ ).toLowerCase();
255
+ const verifiedForSending = typeof result.verifiedForSending === "boolean" ? result.verifiedForSending : raw.verified_for_sending === true;
256
+ const isDeliverable = typeof result.isDeliverable === "boolean" ? result.isDeliverable : raw.is_deliverable === true;
257
+ const isCatchAll = typeof result.isCatchAll === "boolean" ? result.isCatchAll : raw.email_is_catch_all === true || raw.is_catch_all === true;
258
+ const isRoleAccount = typeof result.isRoleAccount === "boolean" ? result.isRoleAccount : raw.is_role_account === true;
259
+ const isMalformed = typeof result.isMalformed === "boolean" ? result.isMalformed : raw.is_malformed === true;
260
+ const quality = typeof result.quality === "string" ? result.quality : typeof raw.quality === "string" ? raw.quality : null;
261
+ const recommendation = typeof result.recommendation === "string" ? result.recommendation : typeof raw.recommendation === "string" ? raw.recommendation : verifiedForSending ? "send" : ["unknown", "verifier_error"].includes(verificationStatus) ? "manual_review" : "do_not_send";
262
+ const cacheHit = typeof raw.cache_hit === "boolean" ? raw.cache_hit : result.cacheHit === true;
263
+ const negativeCacheHit = typeof raw.negative_cache_hit === "boolean" ? raw.negative_cache_hit : result.negativeCacheHit === true;
264
+ const negativeCacheTtlSeconds = safeNonNegativeNumber(
265
+ result.negativeCacheTtlSeconds ?? raw.negative_cache_ttl_seconds
266
+ );
267
+ const billingMetadata = customerEmailBillingMetadata(
268
+ Object.keys(record(result.billingMetadata)).length > 0 ? result.billingMetadata : raw.billing_metadata
269
+ );
270
+ const historicalBillingMetadata = customerEmailBillingMetadata(
271
+ Object.keys(record(result.historicalBillingMetadata)).length > 0 ? result.historicalBillingMetadata : raw.historical_billing_metadata
272
+ );
273
+ const verificationRunId = result.verificationRunId ?? raw.verification_run_id ?? raw.run_id;
274
+ const runId = result.runId ?? raw.run_id ?? verificationRunId;
275
+ const estimatedExternalCostUsd = safeNonNegativeNumber(
276
+ result.estimatedExternalCostUsd ?? raw.estimated_external_cost_usd
277
+ );
278
+ return {
279
+ ...base,
280
+ verification_status: verificationStatus,
281
+ deliverability_status: deliverabilityStatus,
282
+ verification_verdict: verificationVerdict,
283
+ verified_for_sending: verifiedForSending,
284
+ is_deliverable: isDeliverable,
285
+ email_is_catch_all: isCatchAll,
286
+ is_catch_all: isCatchAll,
287
+ is_role_account: isRoleAccount,
288
+ is_malformed: isMalformed,
289
+ quality,
290
+ recommendation,
291
+ smtp_status: result.smtpStatus ?? raw.smtp_status ?? (isCatchAll ? "accept_all" : verifiedForSending ? "accepted" : "not_checked"),
292
+ provider_status: result.providerStatus ?? raw.provider_status ?? verificationStatus,
293
+ verification_source: result.verificationSource ?? raw.verification_source ?? "email_verification",
294
+ ...result.verificationObservedAt ?? raw.verification_observed_at ? { verification_observed_at: result.verificationObservedAt ?? raw.verification_observed_at } : {},
295
+ failure_reason: result.failureReason ?? raw.failure_reason ?? (verifiedForSending ? "none" : isCatchAll ? "catch_all_domain_not_send_safe" : "verification_not_send_safe"),
296
+ cache_hit: cacheHit,
297
+ negative_cache_hit: negativeCacheHit,
298
+ negative_cache_ttl_seconds: negativeCacheTtlSeconds,
299
+ cache_tier: result.cacheTier ?? raw.cache_tier ?? null,
300
+ result_returned_from: result.resultReturnedFrom ?? raw.result_returned_from ?? null,
301
+ fresh_enrichment_used: typeof raw.fresh_enrichment_used === "boolean" ? raw.fresh_enrichment_used : result.freshEnrichmentUsed === true,
302
+ live_provider_called: typeof raw.live_provider_called === "boolean" ? raw.live_provider_called : result.liveProviderCalled === true,
303
+ billing_replayed: typeof raw.billing_replayed === "boolean" ? raw.billing_replayed : result.billingReplayed === true,
304
+ billing_metadata: billingMetadata,
305
+ ...Object.keys(historicalBillingMetadata).length > 0 ? { historical_billing_metadata: historicalBillingMetadata } : {},
306
+ ...estimatedExternalCostUsd !== null ? { estimated_external_cost_usd: estimatedExternalCostUsd } : {},
307
+ ...verificationRunId ? { verification_run_id: verificationRunId } : {},
308
+ ...runId ? { run_id: runId } : {},
309
+ ...raw.cache_publication_status ? {
310
+ cache_publication_status: raw.cache_publication_status,
311
+ cache_publication_retry_eligible: raw.cache_publication_retry_eligible === true,
312
+ ...raw.suggested_action ? { suggested_action: raw.suggested_action } : {}
313
+ } : {}
314
+ };
154
315
  }
155
- function customerEmailBatch(value) {
316
+ function customerEmailBatch(value, kind) {
156
317
  return {
157
318
  ...value,
158
319
  results: value.results.map((item) => {
159
320
  const row = record(item);
160
- return promoteBatchRecoveryMetadata(row.data ? { ...row, data: customerEmailResult(row.data) } : row);
321
+ return promoteBatchRecoveryMetadata(row.data ? { ...row, data: customerEmailResult(row.data, kind) } : row);
161
322
  })
162
323
  };
163
324
  }
@@ -199,14 +360,40 @@ function batchOptions() {
199
360
  return {
200
361
  concurrency: numberFlag("concurrency"),
201
362
  idempotencyKey: flag("idempotency-key"),
202
- compactDuplicates: !hasFlag("expand-duplicates")
363
+ compactDuplicates: !hasFlag("expand-duplicates"),
364
+ maxWaitMs: numberFlag("max-wait-ms"),
365
+ pollIntervalMs: numberFlag("poll-interval-ms")
203
366
  };
204
367
  }
205
- function rejectAsyncSignalFlags() {
206
- if (hasFlag("no-wait") || hasFlag("poll-interval-ms")) {
207
- fail("Company Signals and Signal to Copy are always synchronous; --no-wait and --poll-interval-ms are not supported.");
368
+ function rejectDurableNoWaitPollingFlags(label) {
369
+ if (!hasFlag("no-wait")) return;
370
+ const conflicts = ["max-wait-ms", "poll-interval-ms", "concurrency"].filter(hasFlag);
371
+ if (conflicts.length > 0) {
372
+ fail(`${label} --no-wait cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
208
373
  }
209
374
  }
375
+ function recoverableJobOutput(value, command) {
376
+ const resumeCommand = `signaliz ${command} --job-id ${value.jobId} --idempotency-key ${value.idempotencyKey}`;
377
+ const payload = {
378
+ success: true,
379
+ status: value.status,
380
+ capability: value.capability,
381
+ job_id: value.jobId,
382
+ idempotency_key: value.idempotencyKey,
383
+ total: value.total,
384
+ next_poll_after_seconds: value.nextPollAfterSeconds,
385
+ suggested_action: "check_job_status",
386
+ resume_command: resumeCommand
387
+ };
388
+ output(payload, () => {
389
+ process.stdout.write(`Durable ${command} batch submitted: ${value.jobId}
390
+ `);
391
+ process.stdout.write(`Idempotency key: ${value.idempotencyKey}
392
+ `);
393
+ process.stdout.write(`Resume with: ${resumeCommand}
394
+ `);
395
+ });
396
+ }
210
397
  function batchOutput(value) {
211
398
  if (hasFlag("jsonl")) {
212
399
  for (const result of value.results) {
@@ -493,12 +680,41 @@ async function auth(args) {
493
680
  fail("Usage: signaliz auth <login|set-key|workspace|status>");
494
681
  }
495
682
  async function findEmail() {
683
+ const jobId = flag("job-id");
684
+ if (jobId) {
685
+ const conflicts = [
686
+ "input",
687
+ "dry-run",
688
+ "company-domain",
689
+ "domain",
690
+ "full-name",
691
+ "first-name",
692
+ "last-name",
693
+ "linkedin-url",
694
+ "company-name",
695
+ "run-id",
696
+ "skip-cache",
697
+ "idempotency-key"
698
+ ].filter(hasFlag);
699
+ if (conflicts.length > 0) {
700
+ fail(`Find Email --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
701
+ }
702
+ const result2 = await createClient().resumeFindEmailBatch(jobId, {
703
+ pageSize: numberFlag("page-size"),
704
+ maxWaitMs: numberFlag("max-wait-ms"),
705
+ pollIntervalMs: numberFlag("poll-interval-ms"),
706
+ compactDuplicates: !hasFlag("expand-duplicates")
707
+ });
708
+ batchOutput(customerEmailBatch(result2, "find"));
709
+ return;
710
+ }
496
711
  const rows = readBatchInput();
497
712
  if (rows) {
498
713
  const requests = rows.map((value) => {
499
714
  const row = record(value);
500
715
  const runId2 = row.runId || row.run_id;
501
- if (runId2) return { runId: runId2 };
716
+ const idempotencyKey = row.idempotencyKey || row.idempotency_key;
717
+ if (runId2) return { runId: runId2, idempotencyKey };
502
718
  return {
503
719
  companyDomain: row.companyDomain || row.company_domain || row.domain,
504
720
  fullName: row.fullName || row.full_name || row.name,
@@ -506,7 +722,8 @@ async function findEmail() {
506
722
  lastName: row.lastName || row.last_name,
507
723
  linkedinUrl: row.linkedinUrl || row.linkedin_url || row.linkedin,
508
724
  companyName: row.companyName || row.company_name,
509
- skipCache: row.skipCache ?? row.skip_cache ?? hasFlag("skip-cache")
725
+ skipCache: row.skipCache ?? row.skip_cache ?? hasFlag("skip-cache"),
726
+ idempotencyKey
510
727
  };
511
728
  });
512
729
  if (hasFlag("dry-run")) {
@@ -515,7 +732,7 @@ async function findEmail() {
515
732
  return;
516
733
  }
517
734
  const result2 = await createClient().findEmails(requests, batchOptions());
518
- batchOutput(customerEmailBatch(result2));
735
+ batchOutput(customerEmailBatch(result2, "find"));
519
736
  return;
520
737
  }
521
738
  const companyDomain = flag("company-domain") || flag("domain");
@@ -543,7 +760,7 @@ async function findEmail() {
543
760
  return;
544
761
  }
545
762
  const result = await createClient().findEmail(request);
546
- const publicResult = customerEmailResult(result);
763
+ const publicResult = customerEmailResult(result, "find");
547
764
  if (outputFailedCoreProduct(publicResult, "No verified email found")) return;
548
765
  output(publicResult, () => {
549
766
  if (result.status === "processing" && result.runId) {
@@ -566,9 +783,45 @@ async function findEmail() {
566
783
  });
567
784
  }
568
785
  async function verifyEmail(args) {
786
+ const jobId = flag("job-id");
787
+ if (jobId) {
788
+ const positionalEmail = args[0] && !args[0].startsWith("--");
789
+ const conflicts = [
790
+ "input",
791
+ "dry-run",
792
+ "email",
793
+ "verification-run-id",
794
+ "skip-cache",
795
+ "idempotency-key",
796
+ "no-wait"
797
+ ].filter(hasFlag);
798
+ if (positionalEmail || conflicts.length > 0) {
799
+ const conflictList = [
800
+ ...positionalEmail ? ["email argument"] : [],
801
+ ...conflicts.map((name) => `--${name}`)
802
+ ];
803
+ fail(`Verify Email --job-id cannot be combined with ${conflictList.join(", ")}.`);
804
+ }
805
+ const result2 = await createClient().resumeVerifyEmailBatch(jobId, {
806
+ pageSize: numberFlag("page-size"),
807
+ maxWaitMs: numberFlag("max-wait-ms"),
808
+ pollIntervalMs: numberFlag("poll-interval-ms"),
809
+ compactDuplicates: !hasFlag("expand-duplicates")
810
+ });
811
+ batchOutput(customerEmailBatch(result2, "verify"));
812
+ return;
813
+ }
569
814
  const rows = readBatchInput();
570
815
  if (rows) {
571
- const emails = rows.map((value) => typeof value === "string" ? value : String(record(value).email || ""));
816
+ const emails = rows.map((value) => {
817
+ if (typeof value === "string") return value;
818
+ const row = record(value);
819
+ return {
820
+ email: String(row.email || ""),
821
+ skipCache: row.skipCache ?? row.skip_cache ?? hasFlag("skip-cache"),
822
+ idempotencyKey: row.idempotencyKey || row.idempotency_key
823
+ };
824
+ });
572
825
  if (hasFlag("dry-run")) {
573
826
  const result3 = await createClient().verifyEmails(emails, {
574
827
  ...batchOptions(),
@@ -582,7 +835,7 @@ async function verifyEmail(args) {
582
835
  ...batchOptions(),
583
836
  skipCache: hasFlag("skip-cache")
584
837
  });
585
- batchOutput(customerEmailBatch(result2));
838
+ batchOutput(customerEmailBatch(result2, "verify"));
586
839
  return;
587
840
  }
588
841
  const email = args[0] && !args[0].startsWith("--") ? args[0] : flag("email");
@@ -602,7 +855,7 @@ async function verifyEmail(args) {
602
855
  return;
603
856
  }
604
857
  const result = await createClient().verifyEmail(email, options);
605
- const publicResult = customerEmailResult(result);
858
+ const publicResult = customerEmailResult(result, "verify");
606
859
  if (outputFailedCoreProduct(publicResult, "Email verification failed")) return;
607
860
  output(publicResult, () => {
608
861
  if (result.status === "processing") {
@@ -619,15 +872,62 @@ async function verifyEmail(args) {
619
872
  process.stdout.write(`Success: ${publicResult.success}
620
873
  `);
621
874
  process.stdout.write(`Valid: ${publicResult.is_valid}
875
+ `);
876
+ process.stdout.write(`Verdict: ${publicResult.verification_status}
877
+ `);
878
+ process.stdout.write(`Recommendation: ${publicResult.recommendation}
879
+ `);
880
+ process.stdout.write(`Source: ${publicResult.verification_source}${publicResult.cache_hit === true ? " (cache)" : ""}
881
+ `);
882
+ if (publicResult.run_id) process.stdout.write(`Run ID: ${publicResult.run_id}
622
883
  `);
623
884
  process.stdout.write(`Credits used: ${publicResult.credits_used}
624
885
  `);
625
886
  });
626
887
  }
627
888
  async function companySignals() {
628
- rejectAsyncSignalFlags();
889
+ const jobId = flag("job-id");
890
+ if (jobId) {
891
+ const conflicts = [
892
+ "input",
893
+ "dry-run",
894
+ "domain",
895
+ "company-domain",
896
+ "company-name",
897
+ "signal-run-id",
898
+ "research-prompt",
899
+ "prompt",
900
+ "signal-types",
901
+ "target-signal-count",
902
+ "lookback-days",
903
+ "offline",
904
+ "shallow",
905
+ "search-mode",
906
+ "no-summary",
907
+ "outreach-intelligence",
908
+ "predictive-intelligence",
909
+ "skip-cache",
910
+ "include-candidate-evidence",
911
+ "no-wait",
912
+ "concurrency"
913
+ ].filter(hasFlag);
914
+ if (conflicts.length > 0) {
915
+ fail(`Company Signals --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
916
+ }
917
+ const result2 = await createClient().resumeCompanySignalBatch(jobId, {
918
+ pageSize: numberFlag("page-size"),
919
+ maxWaitMs: numberFlag("max-wait-ms"),
920
+ pollIntervalMs: numberFlag("poll-interval-ms"),
921
+ idempotencyKey: flag("idempotency-key"),
922
+ compactDuplicates: !hasFlag("expand-duplicates")
923
+ });
924
+ batchOutput(customerSignalBatch(result2));
925
+ return;
926
+ }
927
+ rejectDurableNoWaitPollingFlags("Company Signals");
629
928
  const rows = readBatchInput();
630
929
  if (rows) {
930
+ const durableBatch = rows.length > 25;
631
931
  const requests = rows.map((value) => {
632
932
  const row = record(value);
633
933
  const online = row.online ?? !hasFlag("offline");
@@ -647,18 +947,30 @@ async function companySignals() {
647
947
  enablePredictiveIntelligence: row.enablePredictiveIntelligence ?? row.enable_predictive_intelligence,
648
948
  skipCache: row.skipCache ?? row.skip_cache ?? hasFlag("skip-cache"),
649
949
  includeCandidateEvidence: row.includeCandidateEvidence ?? row.include_candidate_evidence ?? hasFlag("include-candidate-evidence"),
650
- maxWaitMs: row.maxWaitMs || row.max_wait_ms || numberFlag("max-wait-ms")
950
+ maxWaitMs: row.maxWaitMs || row.max_wait_ms || (durableBatch ? void 0 : numberFlag("max-wait-ms")),
951
+ idempotencyKey: row.idempotencyKey || row.idempotency_key
651
952
  };
652
953
  });
653
954
  if (hasFlag("dry-run")) {
955
+ if (hasFlag("no-wait")) fail("Company Signals --no-wait cannot be combined with --dry-run.");
654
956
  const result3 = await createClient().enrichCompanies(requests, { ...batchOptions(), dryRun: true });
655
957
  dryRunOutput(result3);
656
958
  return;
657
959
  }
960
+ if (hasFlag("no-wait")) {
961
+ const job = await createClient().enrichCompanies(requests, {
962
+ ...batchOptions(),
963
+ waitForResult: false
964
+ });
965
+ recoverableJobOutput(job, "company-signals");
966
+ return;
967
+ }
658
968
  const result2 = await createClient().enrichCompanies(requests, batchOptions());
659
969
  batchOutput(customerSignalBatch(result2));
660
970
  return;
661
971
  }
972
+ if (hasFlag("no-wait")) fail("Company Signals --no-wait requires --input with more than 25 homogeneous rows.");
973
+ if (hasFlag("poll-interval-ms")) fail("Company Signals --poll-interval-ms requires --input or --job-id.");
662
974
  const companyDomain = flag("domain") || flag("company-domain");
663
975
  const companyName = flag("company-name");
664
976
  const signalRunId = flag("signal-run-id");
@@ -807,7 +1119,7 @@ async function signals(args) {
807
1119
  const identityCost = guardrail.identityResolutionCostUsd === null ? "unavailable" : `$${guardrail.identityResolutionCostUsd}`;
808
1120
  const identityRequests = guardrail.identityProviderRequests;
809
1121
  process.stdout.write(
810
- `Identity resolution cost: ${identityCost}${identityRequests === void 0 ? "" : ` across ${identityRequests} fallback provider request${identityRequests === 1 ? "" : "s"}`}${guardrail.identityCostSource ? ` (${guardrail.identityCostSource})` : ""}.
1122
+ `Identity resolution cost: ${identityCost}${identityRequests === void 0 ? "" : ` across ${identityRequests} identity lookup request${identityRequests === 1 ? "" : "s"}`}${guardrail.identityCostSource ? ` (${guardrail.identityCostSource})` : ""}.
811
1123
  `
812
1124
  );
813
1125
  }
@@ -824,9 +1136,43 @@ async function signals(args) {
824
1136
  });
825
1137
  }
826
1138
  async function signalToCopy() {
827
- rejectAsyncSignalFlags();
1139
+ const jobId = flag("job-id");
1140
+ if (jobId) {
1141
+ const conflicts = [
1142
+ "input",
1143
+ "dry-run",
1144
+ "company-domain",
1145
+ "domain",
1146
+ "person-name",
1147
+ "title",
1148
+ "campaign-offer",
1149
+ "offer",
1150
+ "research-prompt",
1151
+ "prompt",
1152
+ "lookback-days",
1153
+ "signal-run-id",
1154
+ "skip-cache",
1155
+ "deep-search",
1156
+ "no-wait",
1157
+ "concurrency"
1158
+ ].filter(hasFlag);
1159
+ if (conflicts.length > 0) {
1160
+ fail(`Signal to Copy --job-id cannot be combined with ${conflicts.map((name) => `--${name}`).join(", ")}.`);
1161
+ }
1162
+ const result2 = await createClient().resumeSignalCopyBatch(jobId, {
1163
+ pageSize: numberFlag("page-size"),
1164
+ maxWaitMs: numberFlag("max-wait-ms"),
1165
+ pollIntervalMs: numberFlag("poll-interval-ms"),
1166
+ idempotencyKey: flag("idempotency-key"),
1167
+ compactDuplicates: !hasFlag("expand-duplicates")
1168
+ });
1169
+ batchOutput(customerSignalBatch(result2));
1170
+ return;
1171
+ }
1172
+ rejectDurableNoWaitPollingFlags("Signal to Copy");
828
1173
  const rows = readBatchInput();
829
1174
  if (rows) {
1175
+ const durableBatch = rows.length > 25;
830
1176
  const requests = rows.map((value) => {
831
1177
  const row = record(value);
832
1178
  return {
@@ -839,18 +1185,30 @@ async function signalToCopy() {
839
1185
  signalRunId: row.signalRunId || row.signal_run_id,
840
1186
  skipCache: row.skipCache ?? row.skip_cache ?? hasFlag("skip-cache"),
841
1187
  enableDeepSearch: row.enableDeepSearch ?? row.enable_deep_search ?? hasFlag("deep-search"),
842
- maxWaitMs: row.maxWaitMs || row.max_wait_ms || numberFlag("max-wait-ms")
1188
+ maxWaitMs: row.maxWaitMs || row.max_wait_ms || (durableBatch ? void 0 : numberFlag("max-wait-ms")),
1189
+ idempotencyKey: row.idempotencyKey || row.idempotency_key
843
1190
  };
844
1191
  });
845
1192
  if (hasFlag("dry-run")) {
1193
+ if (hasFlag("no-wait")) fail("Signal to Copy --no-wait cannot be combined with --dry-run.");
846
1194
  const result3 = await createClient().createSignalCopyBatch(requests, { ...batchOptions(), dryRun: true });
847
1195
  dryRunOutput(result3);
848
1196
  return;
849
1197
  }
1198
+ if (hasFlag("no-wait")) {
1199
+ const job = await createClient().createSignalCopyBatch(requests, {
1200
+ ...batchOptions(),
1201
+ waitForResult: false
1202
+ });
1203
+ recoverableJobOutput(job, "signal-to-copy");
1204
+ return;
1205
+ }
850
1206
  const result2 = await createClient().createSignalCopyBatch(requests, batchOptions());
851
1207
  batchOutput(customerSignalBatch(result2));
852
1208
  return;
853
1209
  }
1210
+ if (hasFlag("no-wait")) fail("Signal to Copy --no-wait requires --input with more than 25 available-data-only rows.");
1211
+ if (hasFlag("poll-interval-ms")) fail("Signal to Copy --poll-interval-ms requires --input or --job-id.");
854
1212
  const companyDomain = flag("company-domain") || flag("domain");
855
1213
  const personName = flag("person-name");
856
1214
  const title = flag("title");
@@ -988,6 +1346,7 @@ main().catch((error) => {
988
1346
  ...typeof signalizError?.details?.run_id === "string" ? { run_id: signalizError.details.run_id } : {},
989
1347
  ...typeof signalizError?.details?.verification_run_id === "string" ? { verification_run_id: signalizError.details.verification_run_id } : {},
990
1348
  ...typeof signalizError?.details?.job_id === "string" ? { job_id: signalizError.details.job_id } : {},
1349
+ ...typeof signalizError?.details?.idempotency_key === "string" ? { idempotency_key: signalizError.details.idempotency_key } : {},
991
1350
  ...typeof signalizError?.details?.suggested_action === "string" ? { suggested_action: signalizError.details.suggested_action } : {},
992
1351
  ...signalizError?.errorType ? { error_type: signalizError.errorType } : {},
993
1352
  ...signalizError?.details ? { details: signalizError.details } : {}
@@ -1028,6 +1387,13 @@ No automatic retry is recommended; review the provider outcome or contact suppor
1028
1387
  if (error instanceof import_sdk.SignalizError && typeof error.details?.job_id === "string") {
1029
1388
  const jobId = error.details.job_id;
1030
1389
  if (error.details.suggested_action === "check_job_status") {
1390
+ if (["find-email", "verify-email", "company-signals", "signal-to-copy"].includes(process.argv[2])) {
1391
+ const idempotencyKey = typeof error.details.idempotency_key === "string" ? error.details.idempotency_key : "";
1392
+ const keyFlag = idempotencyKey ? ` --idempotency-key ${idempotencyKey}` : "";
1393
+ fail(`${message}
1394
+ Recoverable job: ${jobId}
1395
+ Retry with: signaliz ${process.argv[2]} --job-id ${jobId}${keyFlag}`);
1396
+ }
1031
1397
  fail(`${message}
1032
1398
  Recoverable job: ${jobId}
1033
1399
  Resume this job through the REST API or SDK using job_id ${jobId}, or use --json for structured recovery metadata.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaliz/cli",
3
- "version": "1.0.57",
3
+ "version": "1.0.58",
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.61"
32
+ "@signaliz/sdk": "^1.0.62"
33
33
  },
34
34
  "devDependencies": {
35
35
  "tsup": "^8.0.0",