@oneshot-agent/sdk 0.50.0 → 0.52.0

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/dist/index.js CHANGED
@@ -65,7 +65,7 @@ const approvals_1 = require("./approvals");
65
65
  // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
66
66
  // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
67
67
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
68
- const SDK_VERSION = '0.50.0';
68
+ const SDK_VERSION = '0.52.0';
69
69
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
70
70
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
71
71
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -145,6 +145,8 @@ function validateBudgetConfig(budgets) {
145
145
  fraction(budgets.pauseAt, 'pauseAt');
146
146
  return budgets;
147
147
  }
148
+ /** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
149
+ const PAID_ROUTE_OPTION_KEYS = ['timeout', 'totalTimeoutMs', 'onRequestCreated', 'onAccepted', 'signal', 'onStatusUpdate', 'wait', 'waitForPhones', 'phoneTimeoutSec', 'idempotencyKey', 'approvalId', 'maxCost'];
148
150
  class OneShot {
149
151
  /**
150
152
  * Async factory — required for CDP wallets (account creation is async).
@@ -455,48 +457,48 @@ class OneShot {
455
457
  this.validate(options.topic, 'topic');
456
458
  return this.tool('research', { ...options });
457
459
  }
458
- async peopleSearch(options) {
459
- return this.tool('research/people', { ...options, limit: options.limit ?? 100 });
460
+ async personFind(options) {
461
+ return this.tool('find/person', { ...options, limit: options.limit ?? 100 });
460
462
  }
461
- async enrichProfile(options) {
463
+ async personProfile(options) {
462
464
  if (!options.linkedin_url && !options.email && !options.name) {
463
465
  throw new errors_2.ValidationError('At least one of linkedin_url, email, or name is required', 'identifier');
464
466
  }
465
- return this.tool('enrich/profile', { ...options });
467
+ return this.tool('lookup/person-profile', { ...options });
466
468
  }
467
- async companySearch(options = {}) {
468
- return this.tool('research/company', { ...options, limit: options.limit ?? 10 });
469
+ async companyFind(options = {}) {
470
+ return this.tool('find/company', { ...options, limit: options.limit ?? 10 });
469
471
  }
470
- async enrichCompany(options) {
472
+ async companyProfile(options) {
471
473
  if (!options.domain && !options.name && !options.linkedin_url && !options.ticker) {
472
474
  throw new errors_2.ValidationError('At least one of domain, name, linkedin_url, or ticker is required', 'identifier');
473
475
  }
474
- return this.tool('enrich/company', { ...options });
476
+ return this.tool('lookup/company-profile', { ...options });
475
477
  }
476
478
  /**
477
479
  * Discover local businesses (restaurants, contractors, practices) by
478
480
  * category/keywords × location. Flat price per search, not per row.
479
481
  */
480
- async localSearch(options) {
482
+ async businessFind(options) {
481
483
  if (!options.location || options.location.length === 0) {
482
484
  throw new errors_2.ValidationError('location is required (e.g. ["Austin, TX"])', 'location');
483
485
  }
484
486
  if (!(options.category && options.category.length) && !(options.keywords && options.keywords.length)) {
485
487
  throw new errors_2.ValidationError('At least one of category or keywords is required', 'category');
486
488
  }
487
- return this.tool('local/search', { ...options, limit: options.limit ?? 100 });
489
+ return this.tool('find/business', { ...options, limit: options.limit ?? 100 });
488
490
  }
489
491
  /**
490
492
  * Resolve a business name + one locating field to its domain, phone,
491
493
  * category and operating status. A miss resolves with `found: false`
492
494
  * (a completed job), never a rejection.
493
495
  */
494
- async localResolve(options) {
496
+ async businessResolve(options) {
495
497
  this.validate(options.name, 'name');
496
498
  if (!options.address && !options.city && !options.postal_code && !options.phone) {
497
499
  throw new errors_2.ValidationError('name plus at least one of address, city, postal_code, or phone is required', 'address');
498
500
  }
499
- return this.tool('local/resolve', { ...options });
501
+ return this.tool('resolve/business', { ...options });
500
502
  }
501
503
  /**
502
504
  * Federal contract opportunities (SAM.gov) by NAICS code — Sources Sought
@@ -505,7 +507,7 @@ class OneShot {
505
507
  * `data_as_of` on the result is the snapshot behind the rows (SAM.gov's
506
508
  * daily extract publish time, or "now" when the live API served).
507
509
  */
508
- async govSolicitations(options) {
510
+ async recordSolicitationsFind(options) {
509
511
  if (!options.naics || options.naics.length === 0) {
510
512
  throw new errors_2.ValidationError('naics is required (one or more 6-digit codes, e.g. ["541511"])', 'naics');
511
513
  }
@@ -513,53 +515,53 @@ class OneShot {
513
515
  if (bad !== undefined) {
514
516
  throw new errors_2.ValidationError(`NAICS codes are 6 digits (got ${JSON.stringify(bad)})`, 'naics');
515
517
  }
516
- return this.tool('gov/solicitations', {
518
+ return this.tool('find/record-solicitations', {
517
519
  ...options,
518
520
  notice_types: options.notice_types ?? ['r', 'p'],
519
521
  limit: options.limit ?? 100,
520
522
  });
521
523
  }
522
- async findEmail(options) {
524
+ async personEmail(options) {
523
525
  this.validate(options.company_domain, 'company_domain');
524
526
  if (!options.full_name && !(options.first_name && options.last_name)) {
525
527
  throw new errors_2.ValidationError('Either full_name or both first_name and last_name required', 'name');
526
528
  }
527
- return this.tool('enrich/email', { ...options });
529
+ return this.tool('lookup/person-email', { ...options });
528
530
  }
529
- async verifyEmail(options) {
531
+ async personEmailStatus(options) {
530
532
  this.validate(options.email, 'email');
531
- return this.tool('verify/email', { ...options });
533
+ return this.tool('lookup/person-email-status', { ...options });
532
534
  }
533
- async deepResearchPerson(options) {
535
+ async personDossier(options) {
534
536
  if (!options.email && !options.social_media_url && !options.name) {
535
537
  throw new errors_2.ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
536
538
  }
537
- return this.tool('research/person', { ...options });
539
+ return this.tool('lookup/person-dossier', { ...options });
538
540
  }
539
- async socialProfiles(options) {
541
+ async personSocialAccounts(options) {
540
542
  if (!options.email && !options.social_media_url) {
541
543
  throw new errors_2.ValidationError('At least one of email or social_media_url is required', 'identifier');
542
544
  }
543
- return this.tool('research/social', { ...options });
545
+ return this.tool('lookup/person-social-accounts', { ...options });
544
546
  }
545
- async articleSearch(options) {
547
+ async personArticles(options) {
546
548
  this.validate(options.name, 'name');
547
549
  this.validate(options.company, 'company');
548
- return this.tool('research/articles', { ...options });
550
+ return this.tool('lookup/person-articles', { ...options });
549
551
  }
550
- async personNewsfeed(options) {
552
+ async personPosts(options) {
551
553
  this.validate(options.social_media_url, 'social_media_url');
552
- return this.tool('research/newsfeed', { ...options });
554
+ return this.tool('lookup/person-posts', { ...options });
553
555
  }
554
556
  async personInterests(options) {
555
557
  if (!options.email && !options.phone && !options.social_media_url) {
556
558
  throw new errors_2.ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
557
559
  }
558
- return this.tool('research/interests', { ...options });
560
+ return this.tool('lookup/person-interests', { ...options });
559
561
  }
560
- async personInteractions(options) {
562
+ async personFollowers(options) {
561
563
  this.validate(options.social_media_url, 'social_media_url');
562
- return this.tool('research/interactions', { ...options });
564
+ return this.tool('lookup/person-followers', { ...options });
563
565
  }
564
566
  async inboxList(options = {}) {
565
567
  const qs = this.buildQuery({
@@ -1295,6 +1297,95 @@ class OneShot {
1295
1297
  this.validate(callId, 'callId');
1296
1298
  return this.signedJson('GET', `/v1/tools/voice/inbound/${encodeURIComponent(callId)}`, undefined, 'read', 'Failed to get inbound call');
1297
1299
  }
1300
+ /**
1301
+ * Call a declared route of the API by method and path (#1580), the one generic call for a route that has no
1302
+ * typed method. Follows the Python client's `call_free_get/post/patch/put/delete`:
1303
+ *
1304
+ * - `path` is the route, with `{name}` (or `:name`) for a path parameter, filled from `params` and URL-encoded
1305
+ * (`/v1/compute/{goal_id}/budget`). It must start with `/v1/` and hold no `..` or `?`.
1306
+ * - `query` is the query string (null/undefined left out; booleans `true`/`false`; objects and arrays as JSON).
1307
+ * - `body` is the JSON body of a `POST`, `PUT` or `PATCH` (an object field is sent as it is); a `GET` or
1308
+ * `DELETE` sends none.
1309
+ * - A free route is authorised like the other free calls: a signed proof (read scope for `GET`, write scope for
1310
+ * any other method) or, in an access-token session, the bearer token. Returns the `data` field of
1311
+ * `{ success, data }`, or the whole body when there is none; an empty answer is `{ success: true }`.
1312
+ * - `paid: true` sends the route through the quote-to-pay flow of {@link OneShot.tool} (a `POST` only), with the
1313
+ * {@link ToolOptions} (`maxCost`, `idempotencyKey`, `wait`, `signal`, ...) given in `options`.
1314
+ *
1315
+ * @example
1316
+ * ```typescript
1317
+ * const budget = await agent.route('GET', '/v1/compute/{goal_id}/budget', { params: { goal_id: 'goal_01HX' } });
1318
+ * const changes = await agent.route('POST', '/v1/tools/changes', { paid: true, body: { attribute: 'vehicle_identification', key: '1HGCM82633A004352', since: '2026-01-01' } });
1319
+ * ```
1320
+ */
1321
+ async route(method, path, options = {}) {
1322
+ const { params = {}, query = {}, body, paid = false, ...toolOptions } = options;
1323
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method))
1324
+ throw new errors_2.ValidationError(`Not an HTTP method of the API: ${method}`, 'method');
1325
+ if (!path.startsWith('/v1/') || path.includes('..') || path.includes('?') || path.includes('#')) {
1326
+ throw new errors_2.ValidationError(`Not a route of the API: ${path}`, 'path');
1327
+ }
1328
+ const filled = path.replace(/\{([A-Za-z0-9_]+)\}|:([A-Za-z0-9_]+)/g, (_match, braced, colon) => {
1329
+ const name = braced ?? colon;
1330
+ const value = params[name];
1331
+ if ((typeof value !== 'string' && typeof value !== 'number') || String(value) === '') {
1332
+ throw new errors_2.ValidationError(`${name} is required to call ${method} ${path}`, name);
1333
+ }
1334
+ return encodeURIComponent(String(value));
1335
+ });
1336
+ const qs = new URLSearchParams();
1337
+ for (const [key, value] of Object.entries(query)) {
1338
+ if (value === undefined || value === null)
1339
+ continue;
1340
+ qs.set(key, typeof value === 'boolean' ? (value ? 'true' : 'false') : typeof value === 'object' ? JSON.stringify(value) : String(value));
1341
+ }
1342
+ const target = qs.size ? `${filled}?${qs.toString()}` : filled;
1343
+ if (paid) {
1344
+ if (method !== 'POST')
1345
+ throw new errors_2.ValidationError(`A paid route is called with POST, not ${method}`, 'method');
1346
+ // executeToolRequestImpl strips these keys out of the payload as SDK options, so a body field of the same
1347
+ // name would be dropped silently: refuse it, and tell the caller to send it as an option or rename it.
1348
+ for (const name of PAID_ROUTE_OPTION_KEYS) {
1349
+ if (body && name in body)
1350
+ throw new errors_2.ValidationError(`The body field "${name}" of ${path} is an option of the SDK call and would not reach the API`, name);
1351
+ }
1352
+ return this.executeToolRequest(target, { ...(body ?? {}), ...toolOptions });
1353
+ }
1354
+ const sendsBody = method !== 'GET' && method !== 'DELETE';
1355
+ const scope = (0, deadline_1.deadlineScope)(toolOptions.signal, filled.startsWith('/v1/tools/browser/profiles') ? 150000 : 30000);
1356
+ try {
1357
+ const request = (async () => {
1358
+ const response = await fetch(`${this.baseUrl}${target}`, {
1359
+ method,
1360
+ headers: { ...(sendsBody ? { 'Content-Type': 'application/json' } : {}), ...(await this.signedReadHeaders(method === 'GET' ? 'read' : 'write')) },
1361
+ ...(sendsBody ? { body: JSON.stringify(body ?? {}) } : {}),
1362
+ signal: scope.signal,
1363
+ });
1364
+ if (!response.ok) {
1365
+ if (response.status === 409 && filled.startsWith('/v1/tools/linkedin/')) {
1366
+ const text = await response.clone().text();
1367
+ let code;
1368
+ try {
1369
+ code = JSON.parse(text)?.error;
1370
+ }
1371
+ catch { /* Not JSON. */ }
1372
+ if (code === 'connect_required')
1373
+ throw new errors_1.LinkedInConnectRequiredError(text);
1374
+ }
1375
+ await this.failFromResponse(`${method} ${filled} failed`, response);
1376
+ }
1377
+ const text = await response.text();
1378
+ if (response.status === 204 || !text)
1379
+ return { success: true };
1380
+ const json = JSON.parse(text);
1381
+ return (json && typeof json === 'object' && 'data' in json ? json.data : json);
1382
+ })();
1383
+ return await (0, deadline_1.abortable)(request, scope.signal);
1384
+ }
1385
+ finally {
1386
+ scope.close();
1387
+ }
1388
+ }
1298
1389
  /** Signed free request returning the `data` field of `{ success, data }`. */
1299
1390
  async signedJson(method, path, body, scope, failure) {
1300
1391
  const response = await fetch(`${this.baseUrl}${path}`, {
@@ -1701,11 +1792,17 @@ class OneShot {
1701
1792
  return json.data;
1702
1793
  }
1703
1794
  /**
1704
- * Top up budget for a recurring compute goal.
1795
+ * Top up budget for a recurring compute goal, or for a goal (one-off or recurring) that
1796
+ * ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
1705
1797
  *
1706
1798
  * Uses the quote-then-pay flow: the first call gets a 402 with the top-up price,
1707
1799
  * the second call (with payment) credits the budget. The amount IS the price.
1708
1800
  *
1801
+ * Funding a goal that ran out of budget sets it back to `active` and wakes it; it keeps its
1802
+ * id, tasks, workspace, outputs, done-check and deadline. A goal that ended any other way, or
1803
+ * whose deadline has passed, is refused (`invalid_state` / `deadline_passed`). While the first
1804
+ * ending's refund is still settling the call fails with a retryable `refund_pending` (409).
1805
+ *
1709
1806
  * @example
1710
1807
  * ```typescript
1711
1808
  * const result = await agent.fundComputeGoal('goal_01HX...', 10.00);
@@ -1900,7 +1997,7 @@ class OneShot {
1900
1997
  * await agent.tagReceiptValue('rcpt_01HX...', { type: 'revenue', amount: 5.00, label: 'Sale from lead' });
1901
1998
  *
1902
1999
  * // By the request_id you already kept from the call:
1903
- * const r = await agent.verifyEmail({ email: 'x@y.com' });
2000
+ * const r = await agent.personEmailStatus({ email: 'x@y.com' });
1904
2001
  * await agent.tagReceiptValue({ requestId: r.request_id }, { type: 'lead', amount: 1 });
1905
2002
  *
1906
2003
  * // By cadence correlation key — one call attributes a closed deal to the whole sequence:
@@ -2484,7 +2581,7 @@ class OneShot {
2484
2581
  }
2485
2582
  async executeToolRequest(endpoint, options, quoteId) {
2486
2583
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2487
- const reliable = /(?:^|\/)(enrich\/(profile|email)|verify\/email|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2584
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2488
2585
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2489
2586
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2490
2587
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2511,7 +2608,7 @@ class OneShot {
2511
2608
  }
2512
2609
  }
2513
2610
  async executeToolRequestImpl(endpoint, options, quoteId, context = { phase: "initialization" }) {
2514
- const { totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, approvalId, maxCost, ...payload } = options;
2611
+ const { timeout, totalTimeoutMs, onRequestCreated, onAccepted, signal, onStatusUpdate, wait = true, waitForPhones, phoneTimeoutSec, idempotencyKey, approvalId, maxCost, ...payload } = options;
2515
2612
  const extraHeaders = {
2516
2613
  ...this.maxCostHeader(maxCost),
2517
2614
  ...this.idempotencyHeader(idempotencyKey),
@@ -2562,6 +2659,7 @@ class OneShot {
2562
2659
  const { accepted, resource, extensions } = this.parsePaymentRequired(paymentRequiredHeader);
2563
2660
  // Fallback: parse legacy body format for amount display
2564
2661
  const data = await response.json();
2662
+ quoteId ?? (quoteId = data.context?.quote_id);
2565
2663
  if (this._accessToken) {
2566
2664
  // Credits-only session: never sign. Let the server settle it.
2567
2665
  this.checkAbortBeforePayment(signal);
@@ -2609,7 +2707,7 @@ class OneShot {
2609
2707
  if (!wait) {
2610
2708
  return { ...result, idempotencyKey };
2611
2709
  }
2612
- const completed = await this.pollJob(result.request_id, options.timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
2710
+ const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined);
2613
2711
  if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
2614
2712
  return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
2615
2713
  }
@@ -2738,8 +2836,7 @@ class OneShot {
2738
2836
  * has a pending async webhook and the webhook URL is configured; the webhook
2739
2837
  * handler flips it to false (or removes it) once phones arrive.
2740
2838
  *
2741
- * Tolerates two shapes: the unwrapped result `{phones_pending}` and the
2742
- * deep_research_person wrapper `{result: {phones_pending}}`.
2839
+ * Accepts legacy flat results and person_dossier's result.profile.data.
2743
2840
  */
2744
2841
  _isPhonesPending(result) {
2745
2842
  if (!result || typeof result !== 'object')
@@ -2750,6 +2847,9 @@ class OneShot {
2750
2847
  const inner = r.result;
2751
2848
  if (inner && typeof inner === 'object' && inner.phones_pending === true)
2752
2849
  return true;
2850
+ const profile = inner?.profile;
2851
+ if (profile?.data?.phones_pending === true)
2852
+ return true;
2753
2853
  return false;
2754
2854
  }
2755
2855
  /**
@@ -3075,7 +3175,7 @@ class OneShot {
3075
3175
  // alone (401 proof_required).
3076
3176
  const unsignedStub = payment?.payload?.signature === '0x';
3077
3177
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3078
- const proofScope = /(enrich\/(profile|email)|verify\/email|physical-mail\/send)$/.test(endpoint)
3178
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send)$/.test(endpoint)
3079
3179
  ? 'submit'
3080
3180
  : askedForProof ? 'read' : 'write';
3081
3181
  const headers = {
@@ -3260,7 +3360,7 @@ class OneShot {
3260
3360
  }
3261
3361
  catch (err) {
3262
3362
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3263
- if (!/(enrich\/(profile|email)|verify\/email|physical-mail\/send|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3363
+ if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3264
3364
  this.releaseUsdcReservation(signed.reservation);
3265
3365
  throw err;
3266
3366
  }