@signaliz/sdk 1.0.46 → 1.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,15 @@ const signaliz = new Signaliz({
20
20
  apiKey: process.env.SIGNALIZ_API_KEY,
21
21
  });
22
22
 
23
+ // Every product supports a strict no-spend preflight. Batch methods accept the
24
+ // same option and return one plan for the full input.
25
+ const plan = await signaliz.findEmail({
26
+ companyDomain: 'example.com',
27
+ fullName: 'Jane Doe',
28
+ dryRun: true,
29
+ });
30
+ console.log(plan.estimatedCredits.max, plan.creditsCharged); // 2, 0
31
+
23
32
  const found = await signaliz.findEmail({
24
33
  companyDomain: 'example.com',
25
34
  fullName: 'Jane Doe',
@@ -50,15 +59,6 @@ const signals = await signaliz.enrichCompanySignals({
50
59
  console.log(signals.signals[0].evidencePublishedDate);
51
60
  console.log(signals.signals[0].signalFeedSource, signals.signals[0].derived);
52
61
 
53
- // Persist a no-wait run ID and resume it later without duplicate spend.
54
- const queuedSignals = await signaliz.enrichCompanySignals({
55
- companyDomain: 'example.com',
56
- waitForResult: false,
57
- });
58
- const resumedSignals = await signaliz.enrichCompanySignals({
59
- signalRunId: queuedSignals.signalRunId,
60
- });
61
-
62
62
  const copy = await signaliz.signalToCopy({
63
63
  companyDomain: 'example.com',
64
64
  personName: 'Jane Doe',
@@ -70,16 +70,8 @@ const copy = await signaliz.signalToCopy({
70
70
  enableDeepSearch: false,
71
71
  });
72
72
 
73
- // Persist a queued copy ID and resume with that ID alone. Signaliz restores
74
- // the recipient, title, offer, prompt, and underlying research run.
75
- const queuedCopy = await signaliz.signalToCopy({
76
- companyDomain: 'example.com',
77
- personName: 'Jane Doe',
78
- title: 'VP Sales',
79
- campaignOffer: 'pipeline intelligence',
80
- waitForResult: false,
81
- });
82
- const resumedCopy = await signaliz.signalToCopy({ signalRunId: queuedCopy.runId });
73
+ // Company Signals and Signal to Copy are hard synchronous contracts: these
74
+ // methods return complete data or throw a terminal error in this same call.
83
75
  ```
84
76
 
85
77
  All four products support bounded batch execution over the same REST endpoints.
@@ -104,29 +96,28 @@ Failed rows preserve `errorCode`, `retryEligible`, and `retryAfterSeconds`
104
96
  when the REST API supplies them, including after automatic row retries are
105
97
  exhausted.
106
98
 
107
- For an ambiguous submission failure, retry with the same
108
- `idempotencyKey` to recover the original job without duplicate work.
99
+ For an ambiguous single request or batch submission failure, retry with the
100
+ same `idempotencyKey` to recover the original request or supported email job without duplicate
101
+ work.
109
102
 
110
- Each batch accepts up to 5,000 items. Company Signal Enrichment transparently
111
- resumes long-running research through the same `/company-signals` endpoint;
112
- callers never need an internal Trigger.dev status URL. The SDK follows the
113
- server's adaptive polling hints by default and still honors `pollIntervalMs`
114
- when a caller explicitly overrides the cadence.
103
+ Each batch accepts up to 5,000 items. For Company Signals and Signal to Copy,
104
+ the SDK chunks inputs into synchronous requests of at most 25 and waits for
105
+ every chunk. It never returns queued work, job IDs, or polling instructions.
115
106
  Exact duplicate tasks are single-flighted across each full batch without
116
107
  changing input order or result cardinality. Set `compactDuplicates: true` to
117
108
  return repeated successful rows as `duplicateOf` references to the zero-based
118
109
  canonical input index; the default remains expanded for compatibility. Find Email and Verify Email
119
110
  batches
120
111
  larger than 25 use one cache-aware recoverable REST job with 500-row result
121
- pages. Uniform Company Signal Enrichment batches larger than 25 use one
122
- cache-aware recoverable REST job with 25-row result pages; heterogeneous,
123
- no-wait, and per-run recovery inputs keep the 25-row request path. Signal to
124
- Copy batches larger than 25 use one recoverable REST job when they can reuse
125
- available signal data; fresh or deep-search requests keep the 25-row path.
126
- Signal to Copy also shares company research across copy recipients.
112
+ pages. Company Signal Enrichment and Signal to Copy always use client-side
113
+ 25-row chunks with terminal per-row results. Signal to Copy also shares company
114
+ research across copy recipients.
127
115
 
128
116
  Company Signal Enrichment defaults `online` and `enableDeepSearch` to `true`
129
- unless the caller explicitly disables them. Signal to Copy AI returns three
117
+ unless the caller explicitly disables them. Its `searchMode` defaults to
118
+ `balanced`; use `fast` for the primary low-latency search lane only or
119
+ `coverage` when breadth matters more than the sub-cent average budget target.
120
+ Signal to Copy AI returns three
130
121
  complete, evidence-backed email variations when supported signals are available.
131
122
  Signal to Copy is cache-first and defaults `enableDeepSearch` to `false` so it
132
123
  normally finishes within an agent turn; set `skipCache` or `enableDeepSearch`
@@ -378,10 +378,17 @@ var Signaliz = class {
378
378
  }
379
379
  async findEmail(params) {
380
380
  const data = await this.client.post("api/v1/find-email", findEmailRequestBody(params));
381
- return normalizeFindEmailResult(data);
381
+ return isCoreProductDryRun(data) ? normalizeCoreProductDryRunResult(data) : normalizeFindEmailResult(data);
382
382
  }
383
383
  async findEmails(params, options) {
384
384
  validateBatchSize(params);
385
+ if (options?.dryRun === true) {
386
+ return this.runCoreProductDryRun(
387
+ "api/v1/find-email",
388
+ params.map(findEmailRequestBody),
389
+ options.idempotencyKey
390
+ );
391
+ }
385
392
  const startedAt = Date.now();
386
393
  const round = await this.runCoreBatchRound(
387
394
  "api/v1/find-email",
@@ -400,9 +407,12 @@ var Signaliz = class {
400
407
  const request = compact({
401
408
  email: email || void 0,
402
409
  verification_run_id: options?.verificationRunId,
403
- skip_cache: options?.skipCache
410
+ skip_cache: options?.skipCache,
411
+ dry_run: options?.dryRun,
412
+ idempotency_key: options?.idempotencyKey
404
413
  });
405
414
  let data = await this.client.post("api/v1/verify-email", request);
415
+ if (isCoreProductDryRun(data)) return normalizeCoreProductDryRunResult(data);
406
416
  if (options?.waitForResult !== false && isPendingSignalResponse(data) && data.verification_run_id) {
407
417
  const maxWaitMs = Math.max(1e3, options?.maxWaitMs ?? 10 * 6e4);
408
418
  const startedAt = Date.now();
@@ -422,6 +432,13 @@ var Signaliz = class {
422
432
  }
423
433
  async verifyEmails(emails, options) {
424
434
  validateBatchSize(emails);
435
+ if (options?.dryRun === true) {
436
+ return this.runCoreProductDryRun(
437
+ "api/v1/verify-email",
438
+ emails.map((email) => ({ email, skip_cache: options.skipCache })),
439
+ options.idempotencyKey
440
+ );
441
+ }
425
442
  const startedAt = Date.now();
426
443
  const round = await this.runCoreBatchRound(
427
444
  "api/v1/verify-email",
@@ -441,100 +458,37 @@ var Signaliz = class {
441
458
  }
442
459
  async enrichCompanySignals(params) {
443
460
  const request = companySignalRequestBody(params);
444
- let data = await this.client.post("api/v1/company-signals", request);
445
- if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
446
- const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
447
- const startedAt = Date.now();
448
- while (Date.now() - startedAt < maxWaitMs) {
449
- if (!await waitForNextSignalPoll(data, params.pollIntervalMs, startedAt, maxWaitMs)) break;
450
- const status = await this.client.post("api/v1/company-signals", {
451
- ...request,
452
- signal_run_id: data.run_id
453
- });
454
- if (isCompletedSignalRun(status)) {
455
- data = status.output ?? status.result ?? status.data ?? status;
456
- break;
457
- }
458
- if (isFailedSignalRun(status)) {
459
- throw new Error(`Company signal enrichment ${data.run_id} failed with status ${status.status || "unknown"}`);
460
- }
461
- if (!isPendingSignalResponse(status)) {
462
- data = status.output ?? status.result ?? status.data ?? status;
463
- break;
464
- }
465
- data = status;
466
- }
467
- if (isPendingSignalResponse(data)) {
468
- throw new Error(`Company signal enrichment ${data.run_id} did not complete within ${maxWaitMs}ms`);
469
- }
470
- }
461
+ const data = await this.client.post("api/v1/company-signals", request);
462
+ if (isCoreProductDryRun(data)) return normalizeCoreProductDryRunResult(data);
463
+ assertTerminalSignalResponse(data, "Company Signals");
471
464
  return normalizeCompanySignalResult(params, data);
472
465
  }
473
466
  async enrichCompanies(companies, options) {
474
467
  validateBatchSize(companies);
468
+ if (options?.dryRun === true) {
469
+ return this.runCoreProductDryRun(
470
+ "api/v1/company-signals",
471
+ companies.map(companySignalRequestBody),
472
+ options.idempotencyKey
473
+ );
474
+ }
475
475
  const startedAt = Date.now();
476
- const results = new Array(companies.length);
477
476
  const initialTasks = companies.map((params, index) => ({
478
477
  index,
479
478
  request: companySignalRequestBody(params)
480
479
  }));
481
- const recoverableBatch = companySignalRecoverableBatchOptions(companies, initialTasks);
482
- let pending = initialTasks;
483
- let firstRound = true;
484
- while (pending.length > 0) {
485
- const round = await this.runCoreBatchRound(
486
- "api/v1/company-signals",
487
- pending,
488
- options,
489
- firstRound ? recoverableBatch : void 0
490
- );
491
- firstRound = false;
492
- const taskByIndex = new Map(pending.map((task) => [task.index, task]));
493
- const nextPending = [];
494
- let nextDelayMs = 6e4;
495
- for (const item of round) {
496
- const params = companies[item.index];
497
- const task = taskByIndex.get(item.index);
498
- if (!item.success || !item.raw) {
499
- results[item.index] = batchItemFailure(
500
- item.index,
501
- item.error || "Company Signal batch request failed",
502
- item.raw ?? item
503
- );
504
- continue;
505
- }
506
- if (!isPendingSignalResponse(item.raw) || params.waitForResult === false) {
507
- results[item.index] = { index: item.index, success: true, data: normalizeCompanySignalResult(params, item.raw) };
508
- continue;
509
- }
510
- const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
511
- if (Date.now() - startedAt >= maxWaitMs) {
512
- results[item.index] = {
513
- index: item.index,
514
- success: false,
515
- error: `Company signal enrichment ${item.raw.signal_run_id || item.raw.run_id || ""} did not complete within ${maxWaitMs}ms`
516
- };
517
- continue;
518
- }
519
- const signalRunId = item.raw.signal_run_id || item.raw.run_id;
520
- if (!signalRunId) {
521
- results[item.index] = { index: item.index, success: false, error: "Company signal enrichment is processing without a signal_run_id" };
522
- continue;
523
- }
524
- nextDelayMs = Math.min(
525
- nextDelayMs,
526
- signalPollDelayMs(item.raw, params.pollIntervalMs),
527
- maxWaitMs - (Date.now() - startedAt)
528
- );
529
- nextPending.push({
530
- ...task,
531
- request: { ...task.request, signal_run_id: signalRunId }
532
- });
480
+ const round = await this.runCoreBatchRound("api/v1/company-signals", initialTasks, options);
481
+ const results = round.map((item) => {
482
+ if (!item.success || !item.raw) {
483
+ return batchItemFailure(item.index, item.error || "Company Signal batch request failed", item.raw ?? item);
533
484
  }
534
- if (nextPending.length === 0) break;
535
- await sleep2(Math.max(1, nextDelayMs));
536
- pending = nextPending;
537
- }
485
+ try {
486
+ assertTerminalSignalResponse(item.raw, "Company Signals");
487
+ return { index: item.index, success: true, data: normalizeCompanySignalResult(companies[item.index], item.raw) };
488
+ } catch (error) {
489
+ return batchItemFailure(item.index, error instanceof Error ? error.message : String(error), item.raw);
490
+ }
491
+ });
538
492
  const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
539
493
  const succeeded = compactedResults.filter((item) => item.success).length;
540
494
  return {
@@ -548,37 +502,27 @@ var Signaliz = class {
548
502
  async signalToCopy(params) {
549
503
  validateSignalToCopyParams(params);
550
504
  const request = signalToCopyRequestBody(params);
551
- let data = await this.client.post("api/v1/signal-to-copy", request);
552
- if (params.waitForResult !== false && (data.signal_run_id || data.run_id) && isPendingSignalResponse(data)) {
553
- const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
554
- const startedAt = Date.now();
555
- while (Date.now() - startedAt < maxWaitMs) {
556
- if (!await waitForNextSignalPoll(data, params.pollIntervalMs, startedAt, maxWaitMs)) break;
557
- const signalRunId = data.signal_run_id || data.run_id;
558
- data = await this.client.post("api/v1/signal-to-copy", data.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...request, signal_run_id: signalRunId });
559
- if (!isPendingSignalResponse(data)) break;
560
- }
561
- if (isPendingSignalResponse(data)) {
562
- throw new Error(`Signal Copy ${data.run_id} did not complete within ${maxWaitMs}ms`);
563
- }
564
- }
505
+ const data = await this.client.post("api/v1/signal-to-copy", request);
506
+ if (isCoreProductDryRun(data)) return normalizeCoreProductDryRunResult(data);
507
+ assertTerminalSignalResponse(data, "Signal to Copy");
565
508
  return normalizeSignalToCopyResult(data);
566
509
  }
567
510
  async createSignalCopyBatch(requests, options) {
568
511
  validateBatchSize(requests);
569
512
  requests.forEach(validateSignalToCopyParams);
513
+ if (options?.dryRun === true) {
514
+ return this.runCoreProductDryRun(
515
+ "api/v1/signal-to-copy",
516
+ requests.map(signalToCopyRequestBody),
517
+ options.idempotencyKey
518
+ );
519
+ }
570
520
  const startedAt = Date.now();
571
521
  const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
572
- request: signalToCopyRequestBody(params),
573
- wait_for_result: params.waitForResult,
574
- max_wait_ms: params.maxWaitMs,
575
- poll_interval_ms: params.pollIntervalMs
522
+ request: signalToCopyRequestBody(params)
576
523
  }));
577
524
  const uniqueRequests = deduplicated.uniqueItems;
578
- const canUseRecoverableAvailableDataJob = uniqueRequests.length > 25 && uniqueRequests.every(
579
- (params) => (!params.signalRunId || !params.signalRunId.startsWith("copy_") && [params.companyDomain, params.personName, params.title, params.campaignOffer].every((value) => typeof value === "string" && Boolean(value.trim()))) && params.waitForResult !== false && params.skipCache !== true && params.enableDeepSearch !== true && params.maxWaitMs === void 0 && params.pollIntervalMs === void 0
580
- );
581
- const uniqueResults = canUseRecoverableAvailableDataJob ? await this.runAvailableSignalCopyBatchJob(uniqueRequests, options) : await this.runSignalCopyBatchChunks(uniqueRequests, options);
525
+ const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options);
582
526
  const results = requests.map((_, index) => ({
583
527
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
584
528
  index
@@ -593,6 +537,17 @@ var Signaliz = class {
593
537
  results: compactedResults
594
538
  };
595
539
  }
540
+ async runCoreProductDryRun(path, requests, idempotencyKey) {
541
+ const data = await this.client.post(path, compact({
542
+ requests,
543
+ dry_run: true,
544
+ idempotency_key: idempotencyKey
545
+ }));
546
+ if (!isCoreProductDryRun(data)) {
547
+ throw new Error(`${path} dry run returned a live-result contract`);
548
+ }
549
+ return normalizeCoreProductDryRunResult(data);
550
+ }
596
551
  async runSignalCopyBatchChunks(uniqueRequests, options) {
597
552
  const chunks = [];
598
553
  for (let offset = 0; offset < uniqueRequests.length; offset += 25) {
@@ -623,22 +578,6 @@ var Signaliz = class {
623
578
  }
624
579
  return uniqueResults;
625
580
  }
626
- async runAvailableSignalCopyBatchJob(requests, options) {
627
- const rows = await this.runRecoverableCoreBatchJob(
628
- "api/v1/signal-to-copy",
629
- requests.map(signalToCopyRequestBody),
630
- "Signal to Copy",
631
- 500,
632
- 20 * 6e4,
633
- options?.idempotencyKey
634
- );
635
- const results = new Array(requests.length);
636
- for (const row of rows) {
637
- const index = Number(row.index);
638
- results[index] = recoverableJobRowFailed(row) ? batchItemFailure(index, row.error || row.message || "Signal to Copy failed", row) : { index, success: true, data: normalizeSignalToCopyResult(row) };
639
- }
640
- return results;
641
- }
642
581
  async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey) {
643
582
  const startedAt = Date.now();
644
583
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
@@ -710,77 +649,35 @@ var Signaliz = class {
710
649
  return rows;
711
650
  }
712
651
  async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0) {
713
- const startedAt = Date.now();
714
652
  const results = new Array(requests.length);
715
653
  const rateLimited = [];
716
- let pending = requests.map((params, index) => ({
717
- index,
718
- params,
719
- request: signalToCopyRequestBody(params)
720
- }));
721
- while (pending.length > 0) {
722
- const data = await this.client.post("api/v1/signal-to-copy", {
723
- requests: pending.map((item) => item.request),
724
- concurrency
725
- });
726
- if (!Array.isArray(data.results) || data.results.length !== pending.length) {
727
- throw new Error("Signal to Copy batch returned an invalid result count");
728
- }
729
- const nextPending = [];
730
- let nextDelayMs = 6e4;
731
- for (let index = 0; index < pending.length; index += 1) {
732
- const task = pending[index];
733
- const item = data.results[index];
734
- if (item.success === false) {
735
- if (rowRateLimitAttempt < MAX_ROW_RATE_LIMIT_RETRIES && isRetryableRowRateLimit({
736
- index: task.index,
737
- success: false,
738
- raw: item
739
- })) {
740
- rateLimited.push({ index: task.index, params: task.params, raw: item });
741
- continue;
742
- }
743
- results[task.index] = batchItemFailure(
744
- task.index,
745
- item.error || item.message || "Signal to Copy failed",
746
- item
747
- );
748
- continue;
749
- }
750
- if (!isPendingSignalResponse(item)) {
751
- results[task.index] = { index: task.index, success: true, data: normalizeSignalToCopyResult(item) };
752
- continue;
753
- }
754
- if (task.params.waitForResult === false) {
755
- results[task.index] = { index: task.index, success: true, data: normalizeSignalToCopyResult(item) };
756
- continue;
757
- }
758
- const maxWaitMs = Math.max(1e3, task.params.maxWaitMs ?? 20 * 6e4);
759
- if (Date.now() - startedAt >= maxWaitMs) {
760
- results[task.index] = {
761
- index: task.index,
762
- success: false,
763
- error: `Signal Copy ${item.signal_run_id || item.run_id || ""} did not complete within ${maxWaitMs}ms`
764
- };
765
- continue;
766
- }
767
- const signalRunId = item.signal_run_id || item.run_id;
768
- if (!signalRunId) {
769
- results[task.index] = { index: task.index, success: false, error: "Signal Copy is processing without a signal_run_id" };
654
+ const data = await this.client.post("api/v1/signal-to-copy", {
655
+ requests: requests.map(signalToCopyRequestBody),
656
+ concurrency
657
+ });
658
+ if (!Array.isArray(data.results) || data.results.length !== requests.length) {
659
+ throw new Error("Signal to Copy batch returned an invalid result count");
660
+ }
661
+ for (let index = 0; index < requests.length; index += 1) {
662
+ const item = data.results[index];
663
+ if (item.success === false) {
664
+ if (rowRateLimitAttempt < MAX_ROW_RATE_LIMIT_RETRIES && isRetryableRowRateLimit({
665
+ index,
666
+ success: false,
667
+ raw: item
668
+ })) {
669
+ rateLimited.push({ index, params: requests[index], raw: item });
770
670
  continue;
771
671
  }
772
- nextDelayMs = Math.min(
773
- nextDelayMs,
774
- signalPollDelayMs(item, task.params.pollIntervalMs),
775
- maxWaitMs - (Date.now() - startedAt)
776
- );
777
- nextPending.push({
778
- ...task,
779
- request: item.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...task.request, signal_run_id: signalRunId }
780
- });
672
+ results[index] = batchItemFailure(index, item.error || item.message || "Signal to Copy failed", item);
673
+ continue;
674
+ }
675
+ try {
676
+ assertTerminalSignalResponse(item, "Signal to Copy");
677
+ results[index] = { index, success: true, data: normalizeSignalToCopyResult(item) };
678
+ } catch (error) {
679
+ results[index] = batchItemFailure(index, error instanceof Error ? error.message : String(error), item);
781
680
  }
782
- pending = nextPending;
783
- if (pending.length > 0) await sleep2(nextDelayMs);
784
681
  }
785
682
  if (rateLimited.length > 0) {
786
683
  await sleep2(rowRateLimitDelayMs(rateLimited.map((item) => ({
@@ -799,12 +696,12 @@ var Signaliz = class {
799
696
  }
800
697
  return results;
801
698
  }
802
- async runCoreBatchRound(path, tasks, options, companyRecoverableBatch, rowRateLimitAttempt = 0) {
699
+ async runCoreBatchRound(path, tasks, options, rowRateLimitAttempt = 0) {
803
700
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options);
804
701
  const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
805
702
  const uniqueTasks = deduplicated.uniqueItems;
806
- const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : companyRecoverableBatch;
807
- if (recoverableBatch && uniqueTasks.length > 25) {
703
+ const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
704
+ if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
808
705
  const rows = await this.runRecoverableCoreBatchJob(
809
706
  path,
810
707
  uniqueTasks.map((task) => task.request),
@@ -876,7 +773,6 @@ var Signaliz = class {
876
773
  path,
877
774
  retryTasks,
878
775
  options,
879
- void 0,
880
776
  rowRateLimitAttempt + 1
881
777
  );
882
778
  const retriedByIndex = new Map(retried.map((item) => [item.index, item]));
@@ -951,9 +847,34 @@ function findEmailRequestBody(params) {
951
847
  last_name: params.lastName,
952
848
  linkedin_url: params.linkedinUrl,
953
849
  company_name: params.companyName,
954
- skip_cache: params.skipCache
850
+ skip_cache: params.skipCache,
851
+ dry_run: params.dryRun,
852
+ idempotency_key: params.idempotencyKey
955
853
  });
956
854
  }
855
+ function isCoreProductDryRun(data) {
856
+ return data.dry_run === true && data.status === "planned";
857
+ }
858
+ function normalizeCoreProductDryRunResult(data) {
859
+ const estimatedCredits = data.estimated_credits && typeof data.estimated_credits === "object" ? data.estimated_credits : { min: 0, max: 0 };
860
+ return {
861
+ success: true,
862
+ dryRun: true,
863
+ status: "planned",
864
+ capability: String(data.capability || ""),
865
+ inputCount: Number(data.input_count || 0),
866
+ recoveryReadCount: Number(data.recovery_read_count || 0),
867
+ plan: data.plan && typeof data.plan === "object" ? data.plan : {},
868
+ estimate: data.estimate && typeof data.estimate === "object" ? data.estimate : {},
869
+ estimatedCredits: {
870
+ min: Number(estimatedCredits.min || 0),
871
+ max: Number(estimatedCredits.max || 0)
872
+ },
873
+ creditsCharged: 0,
874
+ sideEffects: Array.isArray(data.side_effects) ? data.side_effects : [],
875
+ raw: data
876
+ };
877
+ }
957
878
  function normalizeFindEmailResult(data) {
958
879
  const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
959
880
  const found = Boolean(email) && data.found !== false;
@@ -1015,39 +936,17 @@ function companySignalRequestBody(params) {
1015
936
  lookback_days: params.lookbackDays,
1016
937
  online,
1017
938
  enable_deep_search: params.enableDeepSearch ?? online,
939
+ search_mode: params.searchMode ?? "balanced",
1018
940
  include_summary: params.includeSummary,
1019
941
  enable_outreach_intelligence: params.enableOutreachIntelligence,
1020
942
  enable_predictive_intelligence: params.enablePredictiveIntelligence,
1021
943
  skip_cache: params.skipCache,
1022
- // REST always returns a resumable run instead of holding the connection;
1023
- // waitForResult controls the SDK polling loop.
1024
- wait_for_result: false,
1025
- max_wait_ms: params.maxWaitMs
944
+ wait_for_result: true,
945
+ max_wait_ms: params.maxWaitMs,
946
+ dry_run: params.dryRun,
947
+ idempotency_key: params.idempotencyKey
1026
948
  });
1027
949
  }
1028
- function companySignalRecoverableBatchOptions(companies, tasks) {
1029
- if (companies.length <= 25 || companies.some((params) => params.waitForResult === false || Boolean(params.signalRunId))) {
1030
- return void 0;
1031
- }
1032
- const maxWaitMs = Math.max(1e3, companies[0].maxWaitMs ?? 20 * 6e4);
1033
- if (companies.some((params) => Math.max(1e3, params.maxWaitMs ?? 20 * 6e4) !== maxWaitMs)) {
1034
- return void 0;
1035
- }
1036
- const configKey = (request) => {
1037
- const {
1038
- company_domain: _companyDomain,
1039
- company_name: _companyName,
1040
- signal_run_id: _signalRunId,
1041
- wait_for_result: _waitForResult,
1042
- max_wait_ms: _maxWaitMs,
1043
- ...config
1044
- } = request;
1045
- return JSON.stringify(config);
1046
- };
1047
- const sharedConfig = configKey(tasks[0].request);
1048
- if (tasks.some((task) => configKey(task.request) !== sharedConfig)) return void 0;
1049
- return { label: "Company Signals", pageSize: 25, maxWaitMs };
1050
- }
1051
950
  function normalizeCompanySignalResult(params, data) {
1052
951
  const rawSignals = data.signals ?? data.signal_feed ?? [];
1053
952
  return {
@@ -1194,7 +1093,10 @@ function signalToCopyRequestBody(params) {
1194
1093
  research_prompt: params.researchPrompt,
1195
1094
  signal_run_id: params.signalRunId,
1196
1095
  skip_cache: params.skipCache,
1197
- enable_deep_search: params.enableDeepSearch
1096
+ enable_deep_search: params.enableDeepSearch,
1097
+ max_wait_ms: params.maxWaitMs,
1098
+ dry_run: params.dryRun,
1099
+ idempotency_key: params.idempotencyKey
1198
1100
  });
1199
1101
  }
1200
1102
  function validateSignalToCopyParams(params) {
@@ -1239,11 +1141,10 @@ function normalizeSignalToCopyResult(data) {
1239
1141
  function isPendingSignalResponse(data) {
1240
1142
  return ["triggered", "queued", "processing", "pending", "pending_version", "waiting_for_deploy"].includes(String(data.status || "").toLowerCase());
1241
1143
  }
1242
- function isCompletedSignalRun(data) {
1243
- return ["completed", "success", "succeeded"].includes(String(data.status || "").toLowerCase());
1244
- }
1245
- function isFailedSignalRun(data) {
1246
- return ["failed", "error", "cancelled", "canceled", "crashed", "timed_out"].includes(String(data.status || "").toLowerCase());
1144
+ function assertTerminalSignalResponse(data, capability) {
1145
+ if (isPendingSignalResponse(data) || data.job_id) {
1146
+ throw new Error(`${capability} violated the synchronous response contract.`);
1147
+ }
1247
1148
  }
1248
1149
  function signalPollDelayMs(data, override) {
1249
1150
  if (override !== void 0) return Math.max(250, override);