@signaliz/sdk 1.0.40 → 1.0.43

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
@@ -69,6 +69,17 @@ const copy = await signaliz.signalToCopy({
69
69
  skipCache: false,
70
70
  enableDeepSearch: false,
71
71
  });
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 });
72
83
  ```
73
84
 
74
85
  All four products support bounded batch execution over the same REST endpoints.
@@ -32,7 +32,7 @@ function parseError(status, body) {
32
32
  }
33
33
  function mapErrorType(code, status) {
34
34
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
35
- if (code === "VALIDATION_ERROR" || status === 400) return "validation";
35
+ if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
36
36
  if (code === "NOT_FOUND" || status === 404) return "not_found";
37
37
  if (code === "NO_SUPPORTED_SIGNAL") return "not_found";
38
38
  if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
@@ -60,6 +60,7 @@ var HttpClient = class {
60
60
  }
61
61
  async request(functionName, body, method = "POST") {
62
62
  let lastError;
63
+ const idempotencyKey = method === "POST" ? requestIdempotencyKey(body) : void 0;
63
64
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
64
65
  let timer;
65
66
  try {
@@ -77,7 +78,8 @@ var HttpClient = class {
77
78
  method,
78
79
  headers: {
79
80
  "Content-Type": "application/json",
80
- Authorization: `Bearer ${token}`
81
+ Authorization: `Bearer ${token}`,
82
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
81
83
  },
82
84
  signal: controller.signal
83
85
  };
@@ -242,6 +244,11 @@ var HttpClient = class {
242
244
  return this.accessTokenPromise;
243
245
  }
244
246
  };
247
+ function requestIdempotencyKey(body) {
248
+ const requested = typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "";
249
+ const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
250
+ return requested || `sdk_${nonce}`;
251
+ }
245
252
  function sleep(ms) {
246
253
  return new Promise((r) => setTimeout(r, ms));
247
254
  }
@@ -342,7 +349,7 @@ function mapMcpErrorType(error) {
342
349
  function mapErrorTypeFromCode(code) {
343
350
  if (!code) return "unknown";
344
351
  if (code === "RATE_LIMITED") return "rate_limited";
345
- if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
352
+ if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
346
353
  if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
347
354
  if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
348
355
  if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
@@ -351,6 +358,7 @@ function mapErrorTypeFromCode(code) {
351
358
  }
352
359
 
353
360
  // src/index.ts
361
+ var MAX_ROW_RATE_LIMIT_RETRIES = 2;
354
362
  var Signaliz = class {
355
363
  constructor(config) {
356
364
  this.client = new HttpClient(config);
@@ -521,17 +529,16 @@ var Signaliz = class {
521
529
  };
522
530
  }
523
531
  async signalToCopy(params) {
532
+ validateSignalToCopyParams(params);
524
533
  const request = signalToCopyRequestBody(params);
525
534
  let data = await this.client.post("api/v1/signal-to-copy", request);
526
- if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
535
+ if (params.waitForResult !== false && (data.signal_run_id || data.run_id) && isPendingSignalResponse(data)) {
527
536
  const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
528
537
  const startedAt = Date.now();
529
538
  while (Date.now() - startedAt < maxWaitMs) {
530
539
  if (!await waitForNextSignalPoll(data, params.pollIntervalMs, startedAt, maxWaitMs)) break;
531
- data = await this.client.post("api/v1/signal-to-copy", {
532
- ...request,
533
- signal_run_id: data.run_id
534
- });
540
+ const signalRunId = data.signal_run_id || data.run_id;
541
+ data = await this.client.post("api/v1/signal-to-copy", data.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...request, signal_run_id: signalRunId });
535
542
  if (!isPendingSignalResponse(data)) break;
536
543
  }
537
544
  if (isPendingSignalResponse(data)) {
@@ -542,6 +549,7 @@ var Signaliz = class {
542
549
  }
543
550
  async createSignalCopyBatch(requests, options) {
544
551
  validateBatchSize(requests);
552
+ requests.forEach(validateSignalToCopyParams);
545
553
  const startedAt = Date.now();
546
554
  const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
547
555
  request: signalToCopyRequestBody(params),
@@ -551,7 +559,7 @@ var Signaliz = class {
551
559
  }));
552
560
  const uniqueRequests = deduplicated.uniqueItems;
553
561
  const canUseRecoverableAvailableDataJob = uniqueRequests.length > 25 && uniqueRequests.every(
554
- (params) => params.waitForResult !== false && params.skipCache !== true && params.enableDeepSearch !== true && params.maxWaitMs === void 0 && params.pollIntervalMs === void 0
562
+ (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
555
563
  );
556
564
  const uniqueResults = canUseRecoverableAvailableDataJob ? await this.runAvailableSignalCopyBatchJob(uniqueRequests, options) : await this.runSignalCopyBatchChunks(uniqueRequests, options);
557
565
  const results = requests.map((_, index) => ({
@@ -738,7 +746,7 @@ var Signaliz = class {
738
746
  );
739
747
  nextPending.push({
740
748
  ...task,
741
- request: { ...task.request, signal_run_id: signalRunId }
749
+ request: item.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...task.request, signal_run_id: signalRunId }
742
750
  });
743
751
  }
744
752
  pending = nextPending;
@@ -746,7 +754,7 @@ var Signaliz = class {
746
754
  }
747
755
  return results;
748
756
  }
749
- async runCoreBatchRound(path, tasks, options, companyRecoverableBatch) {
757
+ async runCoreBatchRound(path, tasks, options, companyRecoverableBatch, rowRateLimitAttempt = 0) {
750
758
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options);
751
759
  const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
752
760
  const uniqueTasks = deduplicated.uniqueItems;
@@ -811,6 +819,25 @@ var Signaliz = class {
811
819
  };
812
820
  }
813
821
  }
822
+ if (rowRateLimitAttempt < MAX_ROW_RATE_LIMIT_RETRIES) {
823
+ const retryTasks = uniqueTasks.filter((_, index) => isRetryableRowRateLimit(uniqueResults[index]));
824
+ if (retryTasks.length > 0) {
825
+ const retryItems = uniqueResults.filter(isRetryableRowRateLimit);
826
+ await sleep2(rowRateLimitDelayMs(retryItems));
827
+ const retried = await this.runCoreBatchRound(
828
+ path,
829
+ retryTasks,
830
+ options,
831
+ void 0,
832
+ rowRateLimitAttempt + 1
833
+ );
834
+ const retriedByIndex = new Map(retried.map((item) => [item.index, item]));
835
+ for (let index = 0; index < uniqueResults.length; index += 1) {
836
+ const replacement = retriedByIndex.get(uniqueResults[index].index);
837
+ if (replacement) uniqueResults[index] = replacement;
838
+ }
839
+ }
840
+ }
814
841
  return tasks.map((task, index) => ({
815
842
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
816
843
  index: task.index
@@ -828,6 +855,21 @@ var Signaliz = class {
828
855
  };
829
856
  }
830
857
  };
858
+ function isRetryableRowRateLimit(item) {
859
+ if (!item || item.success || !item.raw || item.raw.retry_eligible === false) return false;
860
+ const code = String(item.raw.error_code || item.raw.code || "").trim().toUpperCase();
861
+ return code === "RATE_LIMITED" || /^RATE_\d+$/.test(code);
862
+ }
863
+ function rowRateLimitDelayMs(items) {
864
+ const delays = items.map((item) => {
865
+ const retryAfterMs = Number(item.raw?.retry_after_ms);
866
+ if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) return retryAfterMs;
867
+ const retryAfterSeconds = Number(item.raw?.retry_after_seconds ?? item.raw?.retry_after);
868
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) return retryAfterSeconds * 1e3;
869
+ return 6e4;
870
+ });
871
+ return Math.max(1, Math.min(6e4, Math.max(...delays)));
872
+ }
831
873
  function recoverableJobRowFailed(row) {
832
874
  return row.success === false || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
833
875
  }
@@ -1088,11 +1130,24 @@ function signalToCopyRequestBody(params) {
1088
1130
  enable_deep_search: params.enableDeepSearch
1089
1131
  });
1090
1132
  }
1133
+ function validateSignalToCopyParams(params) {
1134
+ if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1135
+ const missing = [
1136
+ ["companyDomain", params.companyDomain],
1137
+ ["personName", params.personName],
1138
+ ["title", params.title],
1139
+ ["campaignOffer", params.campaignOffer]
1140
+ ].filter(([, value]) => typeof value !== "string" || !value.trim()).map(([field]) => field);
1141
+ if (missing.length > 0) {
1142
+ throw new TypeError(`Signal to Copy requires signalRunId or: ${missing.join(", ")}`);
1143
+ }
1144
+ }
1091
1145
  function normalizeSignalToCopyResult(data) {
1092
1146
  return {
1093
1147
  success: data.success ?? true,
1094
1148
  status: data.status,
1095
1149
  runId: data.signal_run_id ?? data.run_id,
1150
+ resumeContextPersisted: data.resume_context_persisted,
1096
1151
  strongestSignal: data.strongest_signal ?? "",
1097
1152
  whyNow: data.why_now ?? "",
1098
1153
  subjectLine: data.subject_line ?? "",
@@ -1100,6 +1155,9 @@ function normalizeSignalToCopyResult(data) {
1100
1155
  confidence: data.confidence ?? 0,
1101
1156
  evidenceUrl: data.evidence_url ?? "",
1102
1157
  researchPrompt: data.research_prompt,
1158
+ empty: data.empty,
1159
+ emptyReason: data.empty_reason,
1160
+ researchMetadata: data.research_metadata,
1103
1161
  variations: (data.variations ?? []).map((variation) => ({
1104
1162
  style: variation.style,
1105
1163
  subjectLine: variation.subject_line ?? "",
package/dist/index.d.mts CHANGED
@@ -167,10 +167,10 @@ interface CompanySignalEnrichmentResult {
167
167
  raw: Record<string, unknown>;
168
168
  }
169
169
  interface SignalToCopyParams {
170
- companyDomain: string;
171
- personName: string;
172
- title: string;
173
- campaignOffer: string;
170
+ companyDomain?: string;
171
+ personName?: string;
172
+ title?: string;
173
+ campaignOffer?: string;
174
174
  researchPrompt?: string;
175
175
  signalRunId?: string;
176
176
  /** Bypass cached company signals and start fresh research. */
@@ -193,6 +193,7 @@ interface SignalToCopyResult {
193
193
  success: boolean;
194
194
  status?: string;
195
195
  runId?: string;
196
+ resumeContextPersisted?: boolean;
196
197
  strongestSignal: string;
197
198
  whyNow: string;
198
199
  subjectLine: string;
@@ -200,6 +201,9 @@ interface SignalToCopyResult {
200
201
  confidence: number;
201
202
  evidenceUrl: string;
202
203
  researchPrompt?: string;
204
+ empty?: boolean;
205
+ emptyReason?: string;
206
+ researchMetadata?: Record<string, unknown>;
203
207
  variations: SignalToCopyVariation[];
204
208
  raw: Record<string, unknown>;
205
209
  }
package/dist/index.d.ts CHANGED
@@ -167,10 +167,10 @@ interface CompanySignalEnrichmentResult {
167
167
  raw: Record<string, unknown>;
168
168
  }
169
169
  interface SignalToCopyParams {
170
- companyDomain: string;
171
- personName: string;
172
- title: string;
173
- campaignOffer: string;
170
+ companyDomain?: string;
171
+ personName?: string;
172
+ title?: string;
173
+ campaignOffer?: string;
174
174
  researchPrompt?: string;
175
175
  signalRunId?: string;
176
176
  /** Bypass cached company signals and start fresh research. */
@@ -193,6 +193,7 @@ interface SignalToCopyResult {
193
193
  success: boolean;
194
194
  status?: string;
195
195
  runId?: string;
196
+ resumeContextPersisted?: boolean;
196
197
  strongestSignal: string;
197
198
  whyNow: string;
198
199
  subjectLine: string;
@@ -200,6 +201,9 @@ interface SignalToCopyResult {
200
201
  confidence: number;
201
202
  evidenceUrl: string;
202
203
  researchPrompt?: string;
204
+ empty?: boolean;
205
+ emptyReason?: string;
206
+ researchMetadata?: Record<string, unknown>;
203
207
  variations: SignalToCopyVariation[];
204
208
  raw: Record<string, unknown>;
205
209
  }
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ function parseError(status, body) {
59
59
  }
60
60
  function mapErrorType(code, status) {
61
61
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
62
- if (code === "VALIDATION_ERROR" || status === 400) return "validation";
62
+ if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
63
63
  if (code === "NOT_FOUND" || status === 404) return "not_found";
64
64
  if (code === "NO_SUPPORTED_SIGNAL") return "not_found";
65
65
  if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
@@ -87,6 +87,7 @@ var HttpClient = class {
87
87
  }
88
88
  async request(functionName, body, method = "POST") {
89
89
  let lastError;
90
+ const idempotencyKey = method === "POST" ? requestIdempotencyKey(body) : void 0;
90
91
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
91
92
  let timer;
92
93
  try {
@@ -104,7 +105,8 @@ var HttpClient = class {
104
105
  method,
105
106
  headers: {
106
107
  "Content-Type": "application/json",
107
- Authorization: `Bearer ${token}`
108
+ Authorization: `Bearer ${token}`,
109
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
108
110
  },
109
111
  signal: controller.signal
110
112
  };
@@ -269,6 +271,11 @@ var HttpClient = class {
269
271
  return this.accessTokenPromise;
270
272
  }
271
273
  };
274
+ function requestIdempotencyKey(body) {
275
+ const requested = typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "";
276
+ const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
277
+ return requested || `sdk_${nonce}`;
278
+ }
272
279
  function sleep(ms) {
273
280
  return new Promise((r) => setTimeout(r, ms));
274
281
  }
@@ -369,7 +376,7 @@ function mapMcpErrorType(error) {
369
376
  function mapErrorTypeFromCode(code) {
370
377
  if (!code) return "unknown";
371
378
  if (code === "RATE_LIMITED") return "rate_limited";
372
- if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
379
+ if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
373
380
  if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
374
381
  if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
375
382
  if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
@@ -378,6 +385,7 @@ function mapErrorTypeFromCode(code) {
378
385
  }
379
386
 
380
387
  // src/index.ts
388
+ var MAX_ROW_RATE_LIMIT_RETRIES = 2;
381
389
  var Signaliz = class {
382
390
  constructor(config) {
383
391
  this.client = new HttpClient(config);
@@ -548,17 +556,16 @@ var Signaliz = class {
548
556
  };
549
557
  }
550
558
  async signalToCopy(params) {
559
+ validateSignalToCopyParams(params);
551
560
  const request = signalToCopyRequestBody(params);
552
561
  let data = await this.client.post("api/v1/signal-to-copy", request);
553
- if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
562
+ if (params.waitForResult !== false && (data.signal_run_id || data.run_id) && isPendingSignalResponse(data)) {
554
563
  const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
555
564
  const startedAt = Date.now();
556
565
  while (Date.now() - startedAt < maxWaitMs) {
557
566
  if (!await waitForNextSignalPoll(data, params.pollIntervalMs, startedAt, maxWaitMs)) break;
558
- data = await this.client.post("api/v1/signal-to-copy", {
559
- ...request,
560
- signal_run_id: data.run_id
561
- });
567
+ const signalRunId = data.signal_run_id || data.run_id;
568
+ data = await this.client.post("api/v1/signal-to-copy", data.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...request, signal_run_id: signalRunId });
562
569
  if (!isPendingSignalResponse(data)) break;
563
570
  }
564
571
  if (isPendingSignalResponse(data)) {
@@ -569,6 +576,7 @@ var Signaliz = class {
569
576
  }
570
577
  async createSignalCopyBatch(requests, options) {
571
578
  validateBatchSize(requests);
579
+ requests.forEach(validateSignalToCopyParams);
572
580
  const startedAt = Date.now();
573
581
  const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
574
582
  request: signalToCopyRequestBody(params),
@@ -578,7 +586,7 @@ var Signaliz = class {
578
586
  }));
579
587
  const uniqueRequests = deduplicated.uniqueItems;
580
588
  const canUseRecoverableAvailableDataJob = uniqueRequests.length > 25 && uniqueRequests.every(
581
- (params) => params.waitForResult !== false && params.skipCache !== true && params.enableDeepSearch !== true && params.maxWaitMs === void 0 && params.pollIntervalMs === void 0
589
+ (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
582
590
  );
583
591
  const uniqueResults = canUseRecoverableAvailableDataJob ? await this.runAvailableSignalCopyBatchJob(uniqueRequests, options) : await this.runSignalCopyBatchChunks(uniqueRequests, options);
584
592
  const results = requests.map((_, index) => ({
@@ -765,7 +773,7 @@ var Signaliz = class {
765
773
  );
766
774
  nextPending.push({
767
775
  ...task,
768
- request: { ...task.request, signal_run_id: signalRunId }
776
+ request: item.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...task.request, signal_run_id: signalRunId }
769
777
  });
770
778
  }
771
779
  pending = nextPending;
@@ -773,7 +781,7 @@ var Signaliz = class {
773
781
  }
774
782
  return results;
775
783
  }
776
- async runCoreBatchRound(path, tasks, options, companyRecoverableBatch) {
784
+ async runCoreBatchRound(path, tasks, options, companyRecoverableBatch, rowRateLimitAttempt = 0) {
777
785
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options);
778
786
  const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
779
787
  const uniqueTasks = deduplicated.uniqueItems;
@@ -838,6 +846,25 @@ var Signaliz = class {
838
846
  };
839
847
  }
840
848
  }
849
+ if (rowRateLimitAttempt < MAX_ROW_RATE_LIMIT_RETRIES) {
850
+ const retryTasks = uniqueTasks.filter((_, index) => isRetryableRowRateLimit(uniqueResults[index]));
851
+ if (retryTasks.length > 0) {
852
+ const retryItems = uniqueResults.filter(isRetryableRowRateLimit);
853
+ await sleep2(rowRateLimitDelayMs(retryItems));
854
+ const retried = await this.runCoreBatchRound(
855
+ path,
856
+ retryTasks,
857
+ options,
858
+ void 0,
859
+ rowRateLimitAttempt + 1
860
+ );
861
+ const retriedByIndex = new Map(retried.map((item) => [item.index, item]));
862
+ for (let index = 0; index < uniqueResults.length; index += 1) {
863
+ const replacement = retriedByIndex.get(uniqueResults[index].index);
864
+ if (replacement) uniqueResults[index] = replacement;
865
+ }
866
+ }
867
+ }
841
868
  return tasks.map((task, index) => ({
842
869
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
843
870
  index: task.index
@@ -855,6 +882,21 @@ var Signaliz = class {
855
882
  };
856
883
  }
857
884
  };
885
+ function isRetryableRowRateLimit(item) {
886
+ if (!item || item.success || !item.raw || item.raw.retry_eligible === false) return false;
887
+ const code = String(item.raw.error_code || item.raw.code || "").trim().toUpperCase();
888
+ return code === "RATE_LIMITED" || /^RATE_\d+$/.test(code);
889
+ }
890
+ function rowRateLimitDelayMs(items) {
891
+ const delays = items.map((item) => {
892
+ const retryAfterMs = Number(item.raw?.retry_after_ms);
893
+ if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) return retryAfterMs;
894
+ const retryAfterSeconds = Number(item.raw?.retry_after_seconds ?? item.raw?.retry_after);
895
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) return retryAfterSeconds * 1e3;
896
+ return 6e4;
897
+ });
898
+ return Math.max(1, Math.min(6e4, Math.max(...delays)));
899
+ }
858
900
  function recoverableJobRowFailed(row) {
859
901
  return row.success === false || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
860
902
  }
@@ -1115,11 +1157,24 @@ function signalToCopyRequestBody(params) {
1115
1157
  enable_deep_search: params.enableDeepSearch
1116
1158
  });
1117
1159
  }
1160
+ function validateSignalToCopyParams(params) {
1161
+ if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1162
+ const missing = [
1163
+ ["companyDomain", params.companyDomain],
1164
+ ["personName", params.personName],
1165
+ ["title", params.title],
1166
+ ["campaignOffer", params.campaignOffer]
1167
+ ].filter(([, value]) => typeof value !== "string" || !value.trim()).map(([field]) => field);
1168
+ if (missing.length > 0) {
1169
+ throw new TypeError(`Signal to Copy requires signalRunId or: ${missing.join(", ")}`);
1170
+ }
1171
+ }
1118
1172
  function normalizeSignalToCopyResult(data) {
1119
1173
  return {
1120
1174
  success: data.success ?? true,
1121
1175
  status: data.status,
1122
1176
  runId: data.signal_run_id ?? data.run_id,
1177
+ resumeContextPersisted: data.resume_context_persisted,
1123
1178
  strongestSignal: data.strongest_signal ?? "",
1124
1179
  whyNow: data.why_now ?? "",
1125
1180
  subjectLine: data.subject_line ?? "",
@@ -1127,6 +1182,9 @@ function normalizeSignalToCopyResult(data) {
1127
1182
  confidence: data.confidence ?? 0,
1128
1183
  evidenceUrl: data.evidence_url ?? "",
1129
1184
  researchPrompt: data.research_prompt,
1185
+ empty: data.empty,
1186
+ emptyReason: data.empty_reason,
1187
+ researchMetadata: data.research_metadata,
1130
1188
  variations: (data.variations ?? []).map((variation) => ({
1131
1189
  style: variation.style,
1132
1190
  subjectLine: variation.subject_line ?? "",
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Signaliz,
3
3
  SignalizError
4
- } from "./chunk-A7B63XMZ.mjs";
4
+ } from "./chunk-HQS7N57T.mjs";
5
5
  export {
6
6
  Signaliz,
7
7
  SignalizError
@@ -63,7 +63,7 @@ function parseError(status, body) {
63
63
  }
64
64
  function mapErrorType(code, status) {
65
65
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
66
- if (code === "VALIDATION_ERROR" || status === 400) return "validation";
66
+ if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
67
67
  if (code === "NOT_FOUND" || status === 404) return "not_found";
68
68
  if (code === "NO_SUPPORTED_SIGNAL") return "not_found";
69
69
  if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
@@ -91,6 +91,7 @@ var HttpClient = class {
91
91
  }
92
92
  async request(functionName, body, method = "POST") {
93
93
  let lastError;
94
+ const idempotencyKey = method === "POST" ? requestIdempotencyKey(body) : void 0;
94
95
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
95
96
  let timer;
96
97
  try {
@@ -108,7 +109,8 @@ var HttpClient = class {
108
109
  method,
109
110
  headers: {
110
111
  "Content-Type": "application/json",
111
- Authorization: `Bearer ${token}`
112
+ Authorization: `Bearer ${token}`,
113
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
112
114
  },
113
115
  signal: controller.signal
114
116
  };
@@ -273,6 +275,11 @@ var HttpClient = class {
273
275
  return this.accessTokenPromise;
274
276
  }
275
277
  };
278
+ function requestIdempotencyKey(body) {
279
+ const requested = typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "";
280
+ const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
281
+ return requested || `sdk_${nonce}`;
282
+ }
276
283
  function sleep(ms) {
277
284
  return new Promise((r) => setTimeout(r, ms));
278
285
  }
@@ -373,7 +380,7 @@ function mapMcpErrorType(error) {
373
380
  function mapErrorTypeFromCode(code) {
374
381
  if (!code) return "unknown";
375
382
  if (code === "RATE_LIMITED") return "rate_limited";
376
- if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
383
+ if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
377
384
  if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
378
385
  if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
379
386
  if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
@@ -382,6 +389,7 @@ function mapErrorTypeFromCode(code) {
382
389
  }
383
390
 
384
391
  // src/index.ts
392
+ var MAX_ROW_RATE_LIMIT_RETRIES = 2;
385
393
  var Signaliz = class {
386
394
  constructor(config) {
387
395
  this.client = new HttpClient(config);
@@ -552,17 +560,16 @@ var Signaliz = class {
552
560
  };
553
561
  }
554
562
  async signalToCopy(params) {
563
+ validateSignalToCopyParams(params);
555
564
  const request = signalToCopyRequestBody(params);
556
565
  let data = await this.client.post("api/v1/signal-to-copy", request);
557
- if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
566
+ if (params.waitForResult !== false && (data.signal_run_id || data.run_id) && isPendingSignalResponse(data)) {
558
567
  const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
559
568
  const startedAt = Date.now();
560
569
  while (Date.now() - startedAt < maxWaitMs) {
561
570
  if (!await waitForNextSignalPoll(data, params.pollIntervalMs, startedAt, maxWaitMs)) break;
562
- data = await this.client.post("api/v1/signal-to-copy", {
563
- ...request,
564
- signal_run_id: data.run_id
565
- });
571
+ const signalRunId = data.signal_run_id || data.run_id;
572
+ data = await this.client.post("api/v1/signal-to-copy", data.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...request, signal_run_id: signalRunId });
566
573
  if (!isPendingSignalResponse(data)) break;
567
574
  }
568
575
  if (isPendingSignalResponse(data)) {
@@ -573,6 +580,7 @@ var Signaliz = class {
573
580
  }
574
581
  async createSignalCopyBatch(requests, options) {
575
582
  validateBatchSize(requests);
583
+ requests.forEach(validateSignalToCopyParams);
576
584
  const startedAt = Date.now();
577
585
  const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
578
586
  request: signalToCopyRequestBody(params),
@@ -582,7 +590,7 @@ var Signaliz = class {
582
590
  }));
583
591
  const uniqueRequests = deduplicated.uniqueItems;
584
592
  const canUseRecoverableAvailableDataJob = uniqueRequests.length > 25 && uniqueRequests.every(
585
- (params) => params.waitForResult !== false && params.skipCache !== true && params.enableDeepSearch !== true && params.maxWaitMs === void 0 && params.pollIntervalMs === void 0
593
+ (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
586
594
  );
587
595
  const uniqueResults = canUseRecoverableAvailableDataJob ? await this.runAvailableSignalCopyBatchJob(uniqueRequests, options) : await this.runSignalCopyBatchChunks(uniqueRequests, options);
588
596
  const results = requests.map((_, index) => ({
@@ -769,7 +777,7 @@ var Signaliz = class {
769
777
  );
770
778
  nextPending.push({
771
779
  ...task,
772
- request: { ...task.request, signal_run_id: signalRunId }
780
+ request: item.resume_context_persisted === true ? { signal_run_id: signalRunId } : { ...task.request, signal_run_id: signalRunId }
773
781
  });
774
782
  }
775
783
  pending = nextPending;
@@ -777,7 +785,7 @@ var Signaliz = class {
777
785
  }
778
786
  return results;
779
787
  }
780
- async runCoreBatchRound(path2, tasks, options, companyRecoverableBatch) {
788
+ async runCoreBatchRound(path2, tasks, options, companyRecoverableBatch, rowRateLimitAttempt = 0) {
781
789
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options);
782
790
  const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
783
791
  const uniqueTasks = deduplicated.uniqueItems;
@@ -842,6 +850,25 @@ var Signaliz = class {
842
850
  };
843
851
  }
844
852
  }
853
+ if (rowRateLimitAttempt < MAX_ROW_RATE_LIMIT_RETRIES) {
854
+ const retryTasks = uniqueTasks.filter((_, index) => isRetryableRowRateLimit(uniqueResults[index]));
855
+ if (retryTasks.length > 0) {
856
+ const retryItems = uniqueResults.filter(isRetryableRowRateLimit);
857
+ await sleep2(rowRateLimitDelayMs(retryItems));
858
+ const retried = await this.runCoreBatchRound(
859
+ path2,
860
+ retryTasks,
861
+ options,
862
+ void 0,
863
+ rowRateLimitAttempt + 1
864
+ );
865
+ const retriedByIndex = new Map(retried.map((item) => [item.index, item]));
866
+ for (let index = 0; index < uniqueResults.length; index += 1) {
867
+ const replacement = retriedByIndex.get(uniqueResults[index].index);
868
+ if (replacement) uniqueResults[index] = replacement;
869
+ }
870
+ }
871
+ }
845
872
  return tasks.map((task, index) => ({
846
873
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
847
874
  index: task.index
@@ -859,6 +886,21 @@ var Signaliz = class {
859
886
  };
860
887
  }
861
888
  };
889
+ function isRetryableRowRateLimit(item) {
890
+ if (!item || item.success || !item.raw || item.raw.retry_eligible === false) return false;
891
+ const code = String(item.raw.error_code || item.raw.code || "").trim().toUpperCase();
892
+ return code === "RATE_LIMITED" || /^RATE_\d+$/.test(code);
893
+ }
894
+ function rowRateLimitDelayMs(items) {
895
+ const delays = items.map((item) => {
896
+ const retryAfterMs = Number(item.raw?.retry_after_ms);
897
+ if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) return retryAfterMs;
898
+ const retryAfterSeconds = Number(item.raw?.retry_after_seconds ?? item.raw?.retry_after);
899
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) return retryAfterSeconds * 1e3;
900
+ return 6e4;
901
+ });
902
+ return Math.max(1, Math.min(6e4, Math.max(...delays)));
903
+ }
862
904
  function recoverableJobRowFailed(row) {
863
905
  return row.success === false || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
864
906
  }
@@ -1119,11 +1161,24 @@ function signalToCopyRequestBody(params) {
1119
1161
  enable_deep_search: params.enableDeepSearch
1120
1162
  });
1121
1163
  }
1164
+ function validateSignalToCopyParams(params) {
1165
+ if (typeof params.signalRunId === "string" && params.signalRunId.trim()) return;
1166
+ const missing = [
1167
+ ["companyDomain", params.companyDomain],
1168
+ ["personName", params.personName],
1169
+ ["title", params.title],
1170
+ ["campaignOffer", params.campaignOffer]
1171
+ ].filter(([, value]) => typeof value !== "string" || !value.trim()).map(([field]) => field);
1172
+ if (missing.length > 0) {
1173
+ throw new TypeError(`Signal to Copy requires signalRunId or: ${missing.join(", ")}`);
1174
+ }
1175
+ }
1122
1176
  function normalizeSignalToCopyResult(data) {
1123
1177
  return {
1124
1178
  success: data.success ?? true,
1125
1179
  status: data.status,
1126
1180
  runId: data.signal_run_id ?? data.run_id,
1181
+ resumeContextPersisted: data.resume_context_persisted,
1127
1182
  strongestSignal: data.strongest_signal ?? "",
1128
1183
  whyNow: data.why_now ?? "",
1129
1184
  subjectLine: data.subject_line ?? "",
@@ -1131,6 +1186,9 @@ function normalizeSignalToCopyResult(data) {
1131
1186
  confidence: data.confidence ?? 0,
1132
1187
  evidenceUrl: data.evidence_url ?? "",
1133
1188
  researchPrompt: data.research_prompt,
1189
+ empty: data.empty,
1190
+ emptyReason: data.empty_reason,
1191
+ researchMetadata: data.research_metadata,
1134
1192
  variations: (data.variations ?? []).map((variation) => ({
1135
1193
  style: variation.style,
1136
1194
  subjectLine: variation.subject_line ?? "",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Signaliz
4
- } from "./chunk-A7B63XMZ.mjs";
4
+ } from "./chunk-HQS7N57T.mjs";
5
5
 
6
6
  // src/mcp-config.ts
7
7
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaliz/sdk",
3
- "version": "1.0.40",
3
+ "version": "1.0.43",
4
4
  "description": "Signaliz SDK for Find Email, Verify Email, Company Signal Enrichment, and Signal to Copy AI.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",