xapi-to 0.1.20 → 0.1.22

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
@@ -26,7 +26,8 @@ npx skills add xapi-labs/xapi-cli
26
26
  ```
27
27
 
28
28
  This installs the bundled [`xapi` skill](skills/xapi), which teaches the agent
29
- how to call social, search, crypto, and AI data through this CLI. Then just ask
29
+ how to call social, search, domains/DNS, crypto, BlockPI RPC, Binance Web3, and
30
+ AI services through this CLI. Then just ask
30
31
  — "what's the price of BTC" — and it takes it from there. Set up a key first;
31
32
  see [Quick Start](#quick-start).
32
33
 
@@ -111,6 +112,13 @@ WebSocket client. Active SSE and raw downloads may run longer than 60 seconds,
111
112
  but abort after 60 seconds without data by default. Set
112
113
  `XAPI_TRANSFER_IDLE_TIMEOUT_MS` to change that idle timeout.
113
114
 
115
+ GPT Live is a WebSocket protocol and cannot be invoked with `xapi-to call`.
116
+ Read [the WebSocket Gateway guide](skills/xapi/guides/ws_gateway.md) and use a
117
+ real WebSocket client. The packaged
118
+ [`examples/openai-gpt-live-text.mjs`](examples/openai-gpt-live-text.mjs)
119
+ demonstrates `session.start`, managed Responses delegation, text events, and a
120
+ graceful `session.close` without placing an xAPI key in source or CLI arguments.
121
+
114
122
  ### Async Task Commands
115
123
 
116
124
  Task helpers built on top of the `task.poll` capability.
@@ -121,6 +129,114 @@ xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 # wait un
121
129
  xapi-to task wait 550e8400-e29b-41d4-a716-446655440000 --interval 1s --timeout 10m
122
130
  ```
123
131
 
132
+ ### Provider: Import → Configure → Submit → Wait
133
+
134
+ `provider` manages APIs owned by your account. It uses `XAPI_API_HOST`
135
+ (default `api.xapi.to`) and the same saved or environment API key as other
136
+ commands. In the xAPI Console API Keys settings, grant `service:create`,
137
+ `service:read`, `service:update`, and `service:publish`. Legacy `allowRegister`
138
+ only grants creation, not the remaining lifecycle permissions. Missing
139
+ permissions return a nonzero exit with the required scope.
140
+
141
+ Start from [examples/provider/openapi.json](examples/provider/openapi.json),
142
+ replace its upstream URL, service details, and endpoint contract, then run:
143
+
144
+ ```bash
145
+ # Inspect current rules; no API key required
146
+ xapi-to provider spec-rules --format pretty
147
+
148
+ # Import a raw OpenAPI 3.0.3 JSON object (not a {openApiSpec: ...} envelope)
149
+ xapi-to provider import --file openapi.json > imported.json
150
+
151
+ # Use the serviceId and revisionId from imported.json (jq is optional)
152
+ PROVIDER_SERVICE_ID=$(jq -er '.serviceId' imported.json)
153
+ PROVIDER_REVISION_ID=$(jq -er '.revisionId' imported.json)
154
+
155
+ # Save version configuration to move the draft revision to SANDBOX.
156
+ # config.json can be {"description":"Initial release"} when the imported
157
+ # endpoints, authentication, and pricing are already complete.
158
+ xapi-to provider update "$PROVIDER_SERVICE_ID" \
159
+ --revision "$PROVIDER_REVISION_ID" --file config.json
160
+
161
+ # Submit the specified revision, then wait for the actual publication result
162
+ xapi-to provider submit "$PROVIDER_SERVICE_ID" \
163
+ --revision "$PROVIDER_REVISION_ID" --changelog "Initial release"
164
+ xapi-to provider wait "$PROVIDER_SERVICE_ID" \
165
+ --revision "$PROVIDER_REVISION_ID" --interval 2s --timeout 10m
166
+
167
+ # Inspect owned services, configuration, version overview, or review reports
168
+ xapi-to provider list --format table
169
+ xapi-to provider get "$PROVIDER_SERVICE_ID" --format pretty
170
+ xapi-to provider versions "$PROVIDER_SERVICE_ID" --format pretty
171
+ xapi-to provider review "$PROVIDER_SERVICE_ID" --revision "$PROVIDER_REVISION_ID"
172
+ ```
173
+
174
+ When scripting these steps, stop on nonzero exit (for example, use `set -e`).
175
+ Import returns the backend validation/preview plus `serviceId`, `revisionId`,
176
+ and `state`. An HTTP 201 with `success: false` is a validation failure and exits
177
+ nonzero; its structured validation errors are preserved. Registration creates
178
+ a new service each time. If a response is lost, inspect `provider list` before
179
+ retrying to avoid duplicate services.
180
+
181
+ For an authenticated upstream, store credentials in a local JSON object such
182
+ as `{"Authorization":"Bearer YOUR_UPSTREAM_KEY"}` and pass
183
+ `--private-headers-file private-headers.json` to `provider import`. Keep this
184
+ file out of version control. `--file -` and `--private-headers-file -` accept
185
+ stdin, but only one input can consume stdin per command. Files must be JSON;
186
+ YAML and URL imports are not supported in this command group.
187
+
188
+ `provider update --revision <id>` reads a version configuration object, using the backend
189
+ fields `description`, `baseUrl`, `baseUrls`, `authType`, `privateHeaders`,
190
+ `authConfig`, `openApiSpec`, `endpoints`, and `status`. Prefer structured
191
+ `privateHeaders` for upstream credentials. Endpoint fields include billing
192
+ configuration such as `billingType` and `costPerCall`. Update does not accept
193
+ a raw OpenAPI document; the nested backend field is `openApiSpec: {spec: ...}`.
194
+ Saving that field alone does not re-import endpoint definitions; configure
195
+ `endpoints` explicitly when changing the contract.
196
+
197
+ - `--mode merge` (default) sends PATCH and preserves omitted fields/endpoints.
198
+ Existing endpoint edits require `id`, e.g.
199
+ `{"endpoints":[{"id":"ENDPOINT_ID","costPerCall":"0.002"}]}`.
200
+ - `--allow-new-endpoints` explicitly permits ID-less merge entries to create
201
+ endpoints. Repeating such a merge can create duplicates.
202
+ - `--mode replace` sends PUT. If `endpoints` is provided, it replaces the
203
+ endpoint list; include every endpoint you intend to keep. Omitted fields
204
+ otherwise follow backend PUT semantics. Use full configuration for replacement.
205
+
206
+ `get` retains its existing service response; `--version v1.0` selects the
207
+ configuration returned by the backend. Find endpoint IDs in
208
+ `currentVersion.endpoints`; `provider versions` returns working revision IDs
209
+ in `majors[].working.id`. For an already-published API, use the existing
210
+ `provider revision start <service-id> <major>` command to create a working revision.
211
+ `provider update` without `--revision` continues to update service metadata
212
+ and rate limits. Existing `version update`, `publish`, and positional `review`
213
+ commands remain available. The `submit` and `review --revision` forms are
214
+ additional onboarding commands.
215
+
216
+ Updates to `IN_REVIEW`, `PUBLISHED`, or `SUSPENDED` revisions return a conflict;
217
+ the backend enforces this check under a transaction lock. When updating
218
+ `privateHeaders`, send the complete desired map: it replaces the old map and
219
+ rebuilds the derived authentication configuration. An empty map without an
220
+ explicit `authConfig` clears those credentials. Omitting both fields preserves them.
221
+
222
+ `submit` returns `{serviceId, revisionId, submission}`. A successful submission
223
+ does not guarantee publication. `wait` checks the requested revision, succeeds
224
+ only for `PUBLISHED`, and outputs the review report with `success` and `reason`.
225
+ `--changelog` is limited to 2,000 characters by both the CLI and backend.
226
+ Rejection, a draft/sandbox/suspended revision, or a legacy manual-review hold
227
+ exit nonzero. Pending review continues until publication, the timeout (default
228
+ 10 minutes), or optional `--max-attempts`. Timeout and attempt-limit results
229
+ include the last received report; polling can be resumed with the same IDs.
230
+ The deadline also bounds in-flight HTTP requests and retry delays.
231
+
232
+ Reads retry transient errors; writes are never automatically retried. The CLI
233
+ redacts credential fields and known credential values from provider output.
234
+ Redacted reads are for inspection and must not be submitted unchanged as
235
+ configuration. After an ambiguous write failure, use `list`, `get`, or `review`
236
+ to inspect the result before repeating the operation. No npm release is implied
237
+ by a local source checkout; use `bun run src/index.ts provider ...` or build and
238
+ run `node dist/index.js provider ...` while testing unreleased changes.
239
+
124
240
  ### Sandbox Commands
125
241
 
126
242
  Sandbox commands provide an AI-friendly cloud computer lifecycle. The fastest
@@ -327,11 +443,58 @@ xapi-to register --referral-code xapito # register with an invit
327
443
  xapi-to register xapito # positional shorthand for --referral-code
328
444
  xapi-to register --force # replace an existing file-based key
329
445
  xapi-to balance # show USD balance
446
+ xapi-to usage <request-id> # finalized cost + balance-after receipt
447
+ xapi-to usage wait <request-id> --timeout 1m # poll until a streaming receipt is finalized
448
+ xapi-to earnings # spendable balance + provider earnings
449
+ xapi-to earnings list --status SETTLED --limit 20 # provider earning records
450
+ xapi-to earnings transfer 1 --idempotency-key reinvest-001 # reinvest settled earnings
330
451
  xapi-to topup # generate payment URL
331
452
  xapi-to topup --method stripe --amount 10 # stripe, $10
332
453
  xapi-to topup --method x402 # x402 (USDC on Base)
333
454
  ```
334
455
 
456
+ `earnings` summary/list require the `earnings:read` scope on the current key;
457
+ `earnings transfer` requires `earnings:transfer`. Transfers are one-way and
458
+ idempotent: reuse a key only when retrying the same amount.
459
+
460
+ ### Provider Management
461
+
462
+ Provider commands use scoped `XAPI-KEY` routes for service and release
463
+ management without a JWT exchange:
464
+
465
+ ```bash
466
+ xapi-to provider list
467
+ xapi-to provider create --file ./service.json
468
+ xapi-to provider update <service-id> --about-file ./ABOUT.md --website https://example.com
469
+ xapi-to provider update <service-id> --rate-limit-requests 100 --rate-limit-period-seconds 60
470
+ xapi-to provider update <service-id> --clear-rate-limit
471
+ xapi-to provider versions <service-id>
472
+ xapi-to provider revision start <service-id> 1
473
+ xapi-to provider version update <service-id> <version-id> --file ./contract.json
474
+ xapi-to provider diff <service-id> 1
475
+ xapi-to provider publish <service-id> <revision-id> --changelog-file ./CHANGELOG.md
476
+ xapi-to provider metrics <service-id> --days 7
477
+ xapi-to provider events --after '<opaque-next-cursor>'
478
+ ```
479
+
480
+ Service rate limits are optional and supported only for proxied services. Both
481
+ numeric flags are required when setting a limit; the quota is shared by all API
482
+ keys belonging to the same user for that service.
483
+
484
+ Service usage tutorials are Skill packages. Scaffold one from the serving
485
+ contract, submit it for review, wait for publication, then link it:
486
+
487
+ ```bash
488
+ xapi-to provider skill scaffold <service-id> --output ./my-skill/SKILL.md
489
+ xapi-to skill submit --dir ./my-skill
490
+ xapi-to skill wait <submission-id> --timeout 10m
491
+ xapi-to provider skill link <service-id> <skill-id>
492
+ xapi-to provider skill fingerprint <service-id> --skill-version-id <version-id>
493
+ ```
494
+
495
+ Run `xapi-to provider --help` and `xapi-to skill --help` for the complete
496
+ lifecycle, rollback, deletion, GitHub import, scope, and safety options.
497
+
335
498
  ### Config
336
499
 
337
500
  ```bash
@@ -265,8 +265,25 @@ function parseRetryAfterMs(res) {
265
265
  const at = Date.parse(header);
266
266
  return Number.isFinite(at) ? Math.max(0, at - Date.now()) : void 0;
267
267
  }
268
- function sleep(ms) {
269
- return new Promise((resolve2) => setTimeout(resolve2, ms));
268
+ function abortError(signal) {
269
+ const reason = signal?.reason;
270
+ return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
271
+ }
272
+ function sleep(ms, signal) {
273
+ if (signal?.aborted) return Promise.reject(abortError(signal));
274
+ return new Promise((resolve2, reject) => {
275
+ let timer;
276
+ const onAbort = () => {
277
+ if (timer !== void 0) clearTimeout(timer);
278
+ signal?.removeEventListener("abort", onAbort);
279
+ reject(abortError(signal));
280
+ };
281
+ timer = setTimeout(() => {
282
+ signal?.removeEventListener("abort", onAbort);
283
+ resolve2();
284
+ }, ms);
285
+ signal?.addEventListener("abort", onAbort, { once: true });
286
+ });
270
287
  }
271
288
  async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0) {
272
289
  assertAllowedHost(url);
@@ -294,7 +311,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
294
311
  if (isRetryableStatus(res.status) && attempt < retries) {
295
312
  await res.text().catch(() => "");
296
313
  clearTimeout(timer);
297
- await sleep(backoffDelayMs(attempt, retryAfterMs));
314
+ await sleep(backoffDelayMs(attempt, retryAfterMs), callerSignal);
298
315
  attempt++;
299
316
  continue;
300
317
  }
@@ -327,7 +344,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
327
344
  if (timedOut) {
328
345
  const timeoutError = new RequestTimeoutError(timeoutMs);
329
346
  if (attempt < retries) {
330
- await sleep(backoffDelayMs(attempt));
347
+ await sleep(backoffDelayMs(attempt), callerSignal);
331
348
  attempt++;
332
349
  continue;
333
350
  }
@@ -335,7 +352,7 @@ async function request(url, options, timeoutMs = DEFAULT_TIMEOUT_MS, retries = 0
335
352
  }
336
353
  if (isRetryableNetworkError(e) && attempt < retries) {
337
354
  clearTimeout(timer);
338
- await sleep(backoffDelayMs(attempt));
355
+ await sleep(backoffDelayMs(attempt), callerSignal);
339
356
  attempt++;
340
357
  continue;
341
358
  }
@@ -351,6 +368,21 @@ function headers(apiKey) {
351
368
  if (apiKey) h["XAPI-Key"] = apiKey;
352
369
  return h;
353
370
  }
371
+ function apiKeyApiRequest(apiHost, apiKey, path, options = {}) {
372
+ const method = options.method ?? "GET";
373
+ const requestHeaders = { "XAPI-KEY": apiKey };
374
+ if (options.body !== void 0) requestHeaders["Content-Type"] = "application/json";
375
+ return request(
376
+ `${scheme(apiHost)}://${apiHost}${path}`,
377
+ {
378
+ method,
379
+ headers: requestHeaders,
380
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
381
+ },
382
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
383
+ options.retries ?? 0
384
+ );
385
+ }
354
386
  function baseUrl(opts) {
355
387
  return `${scheme(opts.actionHost)}://${opts.actionHost}`;
356
388
  }
@@ -657,10 +689,10 @@ async function initiateOAuth(apiKeyId, providerId, jwtToken, apiHost, scopes) {
657
689
  }
658
690
  );
659
691
  }
660
- async function listOAuthBindings(jwtToken, apiHost) {
692
+ async function listOAuthBindings(jwtToken, apiHost, signal) {
661
693
  return request(
662
694
  `${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
663
- { method: "GET", headers: jwtHeaders(jwtToken) },
695
+ { method: "GET", headers: jwtHeaders(jwtToken), signal },
664
696
  DEFAULT_TIMEOUT_MS,
665
697
  IDEMPOTENT_RETRIES
666
698
  );
@@ -794,24 +826,43 @@ var sandboxAudit = (opts, id, kind, page = 1, pageSize = 100) => sandboxRequest(
794
826
  );
795
827
  async function sandboxWait(opts, id, wanted, timeoutMs = 3e5, intervalMs = 2e3, signal) {
796
828
  const deadline = Date.now() + timeoutMs;
829
+ const deadlineController = new AbortController();
830
+ const abortFromCaller = () => deadlineController.abort();
831
+ const deadlineTimer = setTimeout(() => deadlineController.abort(), Math.max(0, timeoutMs));
832
+ if (signal?.aborted) deadlineController.abort();
833
+ else signal?.addEventListener("abort", abortFromCaller, { once: true });
797
834
  let last;
798
- while (Date.now() < deadline) {
799
- if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
800
- last = await sandboxGet(opts, id, signal);
801
- const state = String(last.observedState || "");
802
- if (wanted.includes(state)) return last;
803
- if (["FAILED", "TERMINATED"].includes(state) && !wanted.includes(state)) {
804
- throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(" or ")}`);
835
+ try {
836
+ while (Date.now() < deadline) {
837
+ if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
838
+ try {
839
+ last = await sandboxGet(opts, id, deadlineController.signal);
840
+ } catch (error) {
841
+ if (signal?.aborted) {
842
+ throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(" or ")}`);
843
+ }
844
+ if (Date.now() >= deadline) break;
845
+ throw error;
846
+ }
847
+ if (Date.now() >= deadline) break;
848
+ const state = String(last.observedState || "");
849
+ if (wanted.includes(state)) return last;
850
+ if (["FAILED", "TERMINATED"].includes(state) && !wanted.includes(state)) {
851
+ throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(" or ")}`);
852
+ }
853
+ await new Promise((resolve2) => {
854
+ const done = () => {
855
+ clearTimeout(timer);
856
+ signal?.removeEventListener("abort", done);
857
+ resolve2();
858
+ };
859
+ const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
860
+ signal?.addEventListener("abort", done, { once: true });
861
+ });
805
862
  }
806
- await new Promise((resolve2) => {
807
- const done = () => {
808
- clearTimeout(timer);
809
- signal?.removeEventListener("abort", done);
810
- resolve2();
811
- };
812
- const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
813
- signal?.addEventListener("abort", done, { once: true });
814
- });
863
+ } finally {
864
+ clearTimeout(deadlineTimer);
865
+ signal?.removeEventListener("abort", abortFromCaller);
815
866
  }
816
867
  throw new Error(
817
868
  `sandbox ${id} did not enter ${wanted.join(" or ")} within ${timeoutMs}ms (last state: ${last?.observedState || "unknown"})`
@@ -835,6 +886,7 @@ export {
835
886
  HttpError,
836
887
  isRetryableRequestError,
837
888
  request,
889
+ apiKeyApiRequest,
838
890
  actionList,
839
891
  actionSearch,
840
892
  actionCategories,