oilpriceapi 0.9.0 → 0.10.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/README.md CHANGED
@@ -26,6 +26,7 @@ The official Node.js/TypeScript SDK for [OilPriceAPI](https://www.oilpriceapi.co
26
26
  - 🔔 **NEW v0.5.0** - Price alerts with webhook notifications
27
27
  - 📊 **NEW v0.7.0** - Futures, storage, rig counts, analytics, drilling intelligence, webhooks, and energy intelligence
28
28
  - 🧰 **NEW** - Typed ICE Brent / Gasoil / WTI & gas/carbon futures helpers, `spreads`, `indicators`, and raw HTTP responses (`client.raw.*` with status + headers)
29
+ - 🤖 **NEW v0.10.0** - `getMarketBrief()` (multi-commodity structured + narrative summary) and agent `subscriptions` (persistent watches + event polling)
29
30
 
30
31
  ## Installation
31
32
 
@@ -645,8 +646,8 @@ logs.forEach((log) => {
645
646
 
646
647
  Stream live price updates over a persistent WebSocket connection instead of
647
648
  polling. Streaming uses the server's ActionCable `/cable` endpoint and the
648
- `EnergyPricesChannel`, and is available on the **Reservoir Mastery
649
- (Professional+)** plan.
649
+ `EnergyPricesChannel`, and is available on the **Professional plan
650
+ ($99/mo) or higher**.
650
651
 
651
652
  The `client.stream.prices()` method returns a subscription handle (an
652
653
  `EventEmitter`). It performs the ActionCable handshake, answers server pings,
@@ -704,6 +705,81 @@ process.on("SIGINT", () => {
704
705
 
705
706
  See [`examples/streaming.ts`](./examples/streaming.ts) for a complete runnable example.
706
707
 
708
+ ### Market Brief (New in v0.10.0)
709
+
710
+ A single call returns a structured, multi-commodity market summary — latest
711
+ price, 24h change, freshness, and a 1-month forecast band per commodity — with
712
+ an optional natural-language narrative. Counts as one request against your quota.
713
+
714
+ ```typescript
715
+ import { OilPriceAPI } from "oilpriceapi";
716
+
717
+ const client = new OilPriceAPI({ apiKey: "your_api_key" });
718
+
719
+ const brief = await client.getMarketBrief(["BRENT_CRUDE_USD", "WTI_USD"]);
720
+
721
+ for (const c of brief.commodities) {
722
+ console.log(`${c.name}: $${c.price} (${c.change_24h_pct}% 24h)`);
723
+ if (c.forecast_1m) {
724
+ console.log(
725
+ ` 1m forecast: ${c.forecast_1m.point} [${c.forecast_1m.low}–${c.forecast_1m.high}]`,
726
+ );
727
+ }
728
+ }
729
+
730
+ // Include a natural-language narrative
731
+ const withNarrative = await client.getMarketBrief(["BRENT_CRUDE_USD"], {
732
+ narrative: true,
733
+ });
734
+ console.log(withNarrative.narrative);
735
+ ```
736
+
737
+ ### Agent Subscriptions / Watches (New in v0.10.0)
738
+
739
+ Persistent server-side "watches" evaluate a set of commodity codes on a recurring
740
+ interval and emit events you can poll for — ideal for autonomous agents that want
741
+ change notifications without holding an open connection. The friendly `interval`
742
+ ("5m" / "15m" / "1h" / "daily", or a number of seconds) is mapped to the API's
743
+ `interval_seconds`. Polling for events does **not** consume your request quota.
744
+
745
+ ```typescript
746
+ import { OilPriceAPI } from "oilpriceapi";
747
+
748
+ const client = new OilPriceAPI({ apiKey: "your_api_key" });
749
+
750
+ // Create a watch (source defaults to "sdk-node"; pass `tool` for attribution)
751
+ const watch = await client.subscriptions.create({
752
+ name: "Crude desk",
753
+ codes: ["BRENT_CRUDE_USD", "WTI_USD"],
754
+ interval: "5m",
755
+ tool: "my-trading-bot",
756
+ });
757
+
758
+ // List all watches
759
+ const watches = await client.subscriptions.list();
760
+ console.log(`You have ${watches.length} watch(es)`);
761
+
762
+ // Poll for new events using a cursor (does not burn quota)
763
+ let cursor = 0;
764
+ while (true) {
765
+ const {
766
+ events,
767
+ cursor: next,
768
+ has_more,
769
+ } = await client.subscriptions.events({
770
+ since: cursor,
771
+ });
772
+ for (const event of events) {
773
+ console.log(`event #${event.seq} on ${event.code} (${event.type})`);
774
+ }
775
+ cursor = next;
776
+ if (!has_more) break;
777
+ }
778
+
779
+ // Remove a watch when you're done
780
+ await client.subscriptions.delete(watch.id);
781
+ ```
782
+
707
783
  ### Advanced Configuration
708
784
 
709
785
  ```typescript
@@ -21,6 +21,7 @@ const spreads_js_1 = require("./resources/spreads.js");
21
21
  const indicators_js_1 = require("./resources/indicators.js");
22
22
  const raw_js_1 = require("./resources/raw.js");
23
23
  const streaming_js_1 = require("./resources/streaming.js");
24
+ const subscriptions_js_1 = require("./resources/subscriptions.js");
24
25
  /**
25
26
  * Official Node.js client for Oil Price API
26
27
  *
@@ -80,6 +81,7 @@ class OilPriceAPI {
80
81
  this.indicators = new indicators_js_1.IndicatorsResource(this);
81
82
  this.raw = new raw_js_1.RawResource(this);
82
83
  this.stream = new streaming_js_1.StreamingResource(this);
84
+ this.subscriptions = new subscriptions_js_1.SubscriptionsResource(this);
83
85
  }
84
86
  /**
85
87
  * Log debug messages if debug mode is enabled
@@ -215,6 +217,14 @@ class OilPriceAPI {
215
217
  if (this.appName) {
216
218
  headers["X-App-Name"] = this.appName;
217
219
  }
220
+ // Per-request headers (e.g. MCP attribution X-OPA-Source / X-OPA-Tool).
221
+ if (options?.headers) {
222
+ Object.entries(options.headers).forEach(([key, value]) => {
223
+ if (value !== undefined && value !== null) {
224
+ headers[key] = value;
225
+ }
226
+ });
227
+ }
218
228
  const fetchOptions = {
219
229
  method: options?.method || "GET",
220
230
  headers,
@@ -524,6 +534,45 @@ class OilPriceAPI {
524
534
  async getCommodity(code) {
525
535
  return this.request(`/v1/commodities/${code}`, {});
526
536
  }
537
+ /**
538
+ * Get a multi-commodity market brief (OilPriceAPI #3245 Phase 1a).
539
+ *
540
+ * Returns a structured summary (latest price, 24h change, freshness, and a
541
+ * 1-month forecast band) for each requested commodity, optionally with a
542
+ * natural-language narrative. Counts as a single request against your quota,
543
+ * like `/v1/prices/batch`. The per-tier cap on `codes` is enforced server-side.
544
+ *
545
+ * @param codes - Commodity codes (e.g. ["BRENT_CRUDE_USD", "WTI_USD"]). Shorthand
546
+ * codes like "WTI"/"BRENT" are accepted and resolved server-side.
547
+ * @param options - `{ narrative }` to request the natural-language summary.
548
+ * @returns The structured (and optional narrative) market brief.
549
+ *
550
+ * @throws {ValidationError} If `codes` is empty.
551
+ *
552
+ * @example
553
+ * ```typescript
554
+ * const brief = await client.getMarketBrief(['BRENT_CRUDE_USD', 'WTI_USD']);
555
+ * for (const c of brief.commodities) {
556
+ * console.log(`${c.name}: $${c.price} (${c.change_24h_pct}%)`);
557
+ * }
558
+ *
559
+ * // With narrative
560
+ * const withText = await client.getMarketBrief(['BRENT_CRUDE_USD'], { narrative: true });
561
+ * console.log(withText.narrative);
562
+ * ```
563
+ */
564
+ async getMarketBrief(codes, options) {
565
+ if (!Array.isArray(codes) || codes.length === 0) {
566
+ throw new errors_js_1.ValidationError("codes is required and must be a non-empty array of commodity codes");
567
+ }
568
+ const params = {
569
+ codes: codes.join(","),
570
+ };
571
+ if (options?.narrative) {
572
+ params.narrative = "true";
573
+ }
574
+ return this.request("/v1/market-brief", params);
575
+ }
527
576
  /**
528
577
  * Fetch live sample prices from the public, no-auth demo endpoint.
529
578
  *
package/dist/cjs/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @packageDocumentation
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.ENERGY_PRICES_CHANNEL = exports.PriceStreamSubscription = exports.StreamingResource = exports.DataSourcesResource = exports.WebhooksResource = exports.EIFracFocusResource = exports.EIWellPermitsResource = exports.EIForecastsResource = exports.EIDrillingProductivityResource = exports.EIOPECProductionResource = exports.EIOilInventoriesResource = exports.EIRigCountsResource = exports.EnergyIntelligenceResource = exports.DrillingIntelligenceResource = exports.DataQualityResource = exports.ForecastsResource = exports.AnalyticsResource = exports.BunkerFuelsResource = exports.RigCountsResource = exports.StorageResource = exports.FuturesResource = exports.CommoditiesResource = exports.AlertsResource = exports.DieselResource = exports.TimeoutError = exports.ValidationError = exports.ServerError = exports.NotFoundError = exports.RateLimitError = exports.AuthenticationError = exports.OilPriceAPIError = exports.RawResource = exports.IndicatorsResource = exports.SpreadsResource = exports.FuturesContractFamily = exports.FUTURES_FAMILY_SLUGS = exports.FUTURES_CONTRACTS = exports.SDK_NAME = exports.SDK_VERSION = exports.OilPriceAPI = void 0;
10
+ exports.ENERGY_PRICES_CHANNEL = exports.PriceStreamSubscription = exports.StreamingResource = exports.DataSourcesResource = exports.WebhooksResource = exports.EIFracFocusResource = exports.EIWellPermitsResource = exports.EIForecastsResource = exports.EIDrillingProductivityResource = exports.EIOPECProductionResource = exports.EIOilInventoriesResource = exports.EIRigCountsResource = exports.EnergyIntelligenceResource = exports.DrillingIntelligenceResource = exports.DataQualityResource = exports.ForecastsResource = exports.AnalyticsResource = exports.BunkerFuelsResource = exports.RigCountsResource = exports.StorageResource = exports.FuturesResource = exports.CommoditiesResource = exports.AlertsResource = exports.DieselResource = exports.TimeoutError = exports.ValidationError = exports.ServerError = exports.NotFoundError = exports.RateLimitError = exports.AuthenticationError = exports.OilPriceAPIError = exports.intervalToSeconds = exports.SubscriptionsResource = exports.RawResource = exports.IndicatorsResource = exports.SpreadsResource = exports.FuturesContractFamily = exports.FUTURES_FAMILY_SLUGS = exports.FUTURES_CONTRACTS = exports.SDK_NAME = exports.SDK_VERSION = exports.OilPriceAPI = void 0;
11
11
  exports.verifyWebhookSignature = verifyWebhookSignature;
12
12
  const node_crypto_1 = require("node:crypto");
13
13
  var client_js_1 = require("./client.js");
@@ -25,6 +25,9 @@ var indicators_js_1 = require("./resources/indicators.js");
25
25
  Object.defineProperty(exports, "IndicatorsResource", { enumerable: true, get: function () { return indicators_js_1.IndicatorsResource; } });
26
26
  var raw_js_1 = require("./resources/raw.js");
27
27
  Object.defineProperty(exports, "RawResource", { enumerable: true, get: function () { return raw_js_1.RawResource; } });
28
+ var subscriptions_js_1 = require("./resources/subscriptions.js");
29
+ Object.defineProperty(exports, "SubscriptionsResource", { enumerable: true, get: function () { return subscriptions_js_1.SubscriptionsResource; } });
30
+ Object.defineProperty(exports, "intervalToSeconds", { enumerable: true, get: function () { return subscriptions_js_1.intervalToSeconds; } });
28
31
  var errors_js_1 = require("./errors.js");
29
32
  Object.defineProperty(exports, "OilPriceAPIError", { enumerable: true, get: function () { return errors_js_1.OilPriceAPIError; } });
30
33
  Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return errors_js_1.AuthenticationError; } });
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.FuturesResource = exports.FuturesContractFamily = exports.FUTURES_FAMILY_SLUGS = exports.FUTURES_CONTRACTS = void 0;
10
+ exports.resolveFuturesFamilySlug = resolveFuturesFamilySlug;
10
11
  const errors_js_1 = require("../errors.js");
11
12
  /**
12
13
  * Ergonomic contract codes for the most-requested futures families (issue #1).
@@ -48,15 +49,34 @@ exports.FUTURES_CONTRACTS = {
48
49
  * path segment used by the typed family helpers.
49
50
  */
50
51
  exports.FUTURES_FAMILY_SLUGS = {
51
- [exports.FUTURES_CONTRACTS.BRENT]: "ice-brent",
52
- [exports.FUTURES_CONTRACTS.WTI]: "ice-wti",
53
- [exports.FUTURES_CONTRACTS.GASOIL]: "ice-gasoil",
54
- [exports.FUTURES_CONTRACTS.NATURAL_GAS]: "natural-gas",
55
- [exports.FUTURES_CONTRACTS.TTF_GAS]: "ttf-gas",
56
- [exports.FUTURES_CONTRACTS.LNG_JKM]: "lng-jkm",
57
- [exports.FUTURES_CONTRACTS.EUA_CARBON]: "eua-carbon",
58
- [exports.FUTURES_CONTRACTS.UK_CARBON]: "uk-carbon",
52
+ [exports.FUTURES_CONTRACTS.BRENT]: "ice-brent", // BZ
53
+ [exports.FUTURES_CONTRACTS.WTI]: "ice-wti", // CL
54
+ [exports.FUTURES_CONTRACTS.GASOIL]: "ice-gasoil", // G
55
+ QS: "ice-gasoil", // ICE Gasoil also trades under the QS ticker prefix
56
+ [exports.FUTURES_CONTRACTS.NATURAL_GAS]: "natural-gas", // NG
57
+ [exports.FUTURES_CONTRACTS.TTF_GAS]: "ttf-gas", // TTF
58
+ [exports.FUTURES_CONTRACTS.LNG_JKM]: "lng-jkm", // JKM
59
+ [exports.FUTURES_CONTRACTS.EUA_CARBON]: "eua-carbon", // EUA
60
+ [exports.FUTURES_CONTRACTS.UK_CARBON]: "uk-carbon", // UKA
59
61
  };
62
+ /**
63
+ * Resolve a futures contract code (e.g. `"BZ"`, `"QS"`) or an already-valid
64
+ * family slug (e.g. `"ice-brent"`) to its `/v1/futures/{slug}` path segment.
65
+ *
66
+ * Matching is case-insensitive for codes. Returns `null` if the input maps to
67
+ * neither a known code nor a known family slug.
68
+ */
69
+ function resolveFuturesFamilySlug(codeOrSlug) {
70
+ const trimmed = codeOrSlug.trim();
71
+ // Direct code match (case-insensitive — codes are upper-case).
72
+ const byCode = exports.FUTURES_FAMILY_SLUGS[trimmed.toUpperCase()];
73
+ if (byCode)
74
+ return byCode;
75
+ // Already a valid family slug?
76
+ const lower = trimmed.toLowerCase();
77
+ const isSlug = Object.values(exports.FUTURES_FAMILY_SLUGS).includes(lower);
78
+ return isSlug ? lower : null;
79
+ }
60
80
  /**
61
81
  * Typed helper for a single futures contract family (e.g., ICE Brent, Gasoil).
62
82
  *
@@ -80,9 +100,12 @@ class FuturesContractFamily {
80
100
  }
81
101
  /**
82
102
  * Get the latest price for this contract family.
103
+ *
104
+ * Latest is served from the bare slug path `GET /v1/futures/{slug}` —
105
+ * there is NO `/latest` suffix (that path 404s).
83
106
  */
84
107
  async latest() {
85
- return this.client["request"](`/v1/futures/${this.slug}/latest`, {});
108
+ return this.client["request"](`/v1/futures/${this.slug}`, {});
86
109
  }
87
110
  /**
88
111
  * Get historical prices for this contract family.
@@ -156,16 +179,13 @@ exports.FuturesContractFamily = FuturesContractFamily;
156
179
  *
157
180
  * const client = new OilPriceAPI({ apiKey: 'your_key' });
158
181
  *
159
- * // Get latest price
160
- * const latest = await client.futures.latest('CL.1');
182
+ * // Get the latest curve by contract code (resolves to GET /v1/futures/ice-wti)
183
+ * const latest = await client.futures.latest('CL');
161
184
  * console.log(`${latest.contract}: $${latest.price}`);
162
185
  *
163
- * // Get OHLC data
164
- * const ohlc = await client.futures.ohlc('CL.1', '2024-01-15');
165
- * console.log(`High: $${ohlc.high}, Low: $${ohlc.low}`);
166
- *
167
- * // Get futures curve
168
- * const curve = await client.futures.curve('CL');
186
+ * // Typed family helpers are the most ergonomic option:
187
+ * const brent = await client.futures.brent().latest();
188
+ * const curve = await client.futures.brent().curve();
169
189
  * curve.curve.forEach(point => {
170
190
  * console.log(`${point.months_out}mo: $${point.price}`);
171
191
  * });
@@ -176,25 +196,42 @@ class FuturesResource {
176
196
  this.client = client;
177
197
  }
178
198
  /**
179
- * Get latest price for a futures contract
199
+ * Get the latest curve/quote for a futures contract family.
180
200
  *
181
- * @param contract - Contract symbol (e.g., "CL.1", "BZ.2")
182
- * @returns Latest futures price data
201
+ * Accepts an ergonomic contract code (e.g. `"BZ"`, `"CL"`, `"QS"`) or a
202
+ * family slug (e.g. `"ice-brent"`). The code is resolved to its family slug
203
+ * and the request is sent to `GET /v1/futures/{slug}` — the bare slug path,
204
+ * with NO `/latest` suffix (the suffixed path 404s).
183
205
  *
184
- * @throws {NotFoundError} If contract not found
206
+ * Supported codes: BZ (Brent), CL (WTI), G/QS (Gasoil), NG (Natural Gas),
207
+ * TTF, JKM, EUA, UKA. Slugs: ice-brent, ice-wti, ice-gasoil, natural-gas,
208
+ * ttf-gas, lng-jkm, eua-carbon, uk-carbon.
209
+ *
210
+ * @param contract - Contract code (e.g. "BZ") or family slug (e.g. "ice-brent").
211
+ * @returns Latest futures price/curve data
212
+ *
213
+ * @throws {ValidationError} If the code/slug is empty or unrecognized.
185
214
  * @throws {OilPriceAPIError} If API request fails
186
215
  *
187
216
  * @example
188
217
  * ```typescript
189
- * const price = await client.futures.latest('CL.1');
190
- * console.log(`WTI Front Month: $${price.price}`);
218
+ * import { FUTURES_CONTRACTS } from 'oilpriceapi';
219
+ * const price = await client.futures.latest(FUTURES_CONTRACTS.BRENT); // "BZ"
220
+ * const wti = await client.futures.latest('ice-wti');
191
221
  * ```
192
222
  */
193
223
  async latest(contract) {
194
224
  if (!contract || typeof contract !== "string") {
195
225
  throw new errors_js_1.ValidationError("Contract symbol must be a non-empty string");
196
226
  }
197
- return this.client["request"](`/v1/futures/${contract}`, {});
227
+ const slug = resolveFuturesFamilySlug(contract);
228
+ if (!slug) {
229
+ throw new errors_js_1.ValidationError(`Unknown futures contract "${contract}". Use a contract code ` +
230
+ `(BZ, CL, G, QS, NG, TTF, JKM, EUA, UKA) or a family slug ` +
231
+ `(ice-brent, ice-wti, ice-gasoil, natural-gas, ttf-gas, lng-jkm, ` +
232
+ `eua-carbon, uk-carbon).`);
233
+ }
234
+ return this.client["request"](`/v1/futures/${slug}`, {});
198
235
  }
199
236
  /**
200
237
  * Get historical prices for a futures contract
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Market Brief types (OilPriceAPI #3245 Phase 1a)
4
+ *
5
+ * A multi-commodity structured (+ optional narrative) market summary composed
6
+ * from existing price + forecast data. Served by `GET /v1/market-brief` and
7
+ * surfaced on the client as {@link OilPriceAPI.getMarketBrief}.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,7 +5,7 @@
5
5
  * Real-time price streaming via the OilPriceAPI ActionCable endpoint
6
6
  * (`wss://api.oilpriceapi.com/cable`).
7
7
  *
8
- * Streaming is a **Reservoir Mastery (Professional+)** feature. Connections
8
+ * Streaming is a **Professional plan ($99/mo) or higher** feature. Connections
9
9
  * authenticate with your API key and subscribe to the `EnergyPricesChannel`,
10
10
  * which pushes an initial `welcome` snapshot followed by live `price_update`
11
11
  * and (for drilling-tier accounts) `rig_count_update` messages.
@@ -163,8 +163,8 @@ class PriceStreamSubscription extends node_events_1.EventEmitter {
163
163
  return;
164
164
  }
165
165
  if (transportType === "reject_subscription") {
166
- this.emit("error", new Error("WebSocket subscription rejected. Streaming requires a Reservoir Mastery " +
167
- "(Professional+) plan and a valid API key."));
166
+ this.emit("error", new Error("WebSocket subscription rejected. Streaming requires a Professional " +
167
+ "plan ($99/mo) or higher and a valid API key."));
168
168
  return;
169
169
  }
170
170
  if (transportType === "disconnect") {
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * Agent Subscriptions ("Watches") Resource
4
+ *
5
+ * Persistent server-side "watches" that evaluate a set of commodity codes on a
6
+ * recurring interval and emit events an agent can poll for (OilPriceAPI #3245
7
+ * Phase 2). Designed for autonomous agents (MCP, schedulers, bots) that want
8
+ * change notifications without holding an open connection.
9
+ *
10
+ * The poll endpoint (`events`) does NOT consume the monthly request quota and
11
+ * has its own generous rate-limit lane, so agents can poll frequently.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.SubscriptionsResource = void 0;
15
+ exports.intervalToSeconds = intervalToSeconds;
16
+ const errors_js_1 = require("../errors.js");
17
+ /** Default attribution source for watches created via this SDK. */
18
+ const DEFAULT_SOURCE = "sdk-node";
19
+ /** Named interval presets mapped to seconds. */
20
+ const INTERVAL_PRESETS = {
21
+ "5m": 300,
22
+ "15m": 900,
23
+ "1h": 3600,
24
+ hourly: 3600,
25
+ daily: 86400,
26
+ };
27
+ /**
28
+ * Convert a friendly interval ("5m" / "1h" / "daily" / 300) into seconds.
29
+ *
30
+ * Accepts:
31
+ * - presets: "5m", "15m", "1h", "hourly", "daily"
32
+ * - unit expressions: "<n>s", "<n>m", "<n>h", "<n>d"
33
+ * - a raw number of seconds
34
+ *
35
+ * @internal Exported for unit testing of the mapping.
36
+ */
37
+ function intervalToSeconds(interval) {
38
+ if (interval === undefined) {
39
+ return INTERVAL_PRESETS["5m"];
40
+ }
41
+ if (typeof interval === "number") {
42
+ if (!Number.isFinite(interval) || interval <= 0) {
43
+ throw new errors_js_1.ValidationError("interval (seconds) must be a positive number");
44
+ }
45
+ return Math.floor(interval);
46
+ }
47
+ const key = interval.trim().toLowerCase();
48
+ if (key in INTERVAL_PRESETS) {
49
+ return INTERVAL_PRESETS[key];
50
+ }
51
+ // Unit expression: <number><unit> where unit ∈ s/m/h/d.
52
+ const match = /^(\d+)\s*(s|m|h|d)$/.exec(key);
53
+ if (match) {
54
+ const value = parseInt(match[1], 10);
55
+ const unit = match[2];
56
+ const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
57
+ const seconds = value * multipliers[unit];
58
+ if (seconds <= 0) {
59
+ throw new errors_js_1.ValidationError("interval must be greater than zero");
60
+ }
61
+ return seconds;
62
+ }
63
+ throw new errors_js_1.ValidationError(`Invalid interval "${interval}". Use a preset ("5m", "15m", "1h", "daily"), ` +
64
+ `a unit expression ("30s", "10m", "2h", "1d"), or a number of seconds.`);
65
+ }
66
+ /**
67
+ * Agent Subscriptions ("Watches") Resource
68
+ *
69
+ * Manage persistent, recurring watches over commodity codes and poll for the
70
+ * events they emit.
71
+ *
72
+ * **Example:**
73
+ * ```typescript
74
+ * import { OilPriceAPI } from 'oilpriceapi';
75
+ *
76
+ * const client = new OilPriceAPI({ apiKey: 'your_key' });
77
+ *
78
+ * // Create a watch that evaluates Brent + WTI every 5 minutes
79
+ * const watch = await client.subscriptions.create({
80
+ * name: 'Crude desk',
81
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
82
+ * interval: '5m',
83
+ * });
84
+ *
85
+ * // List all watches
86
+ * const watches = await client.subscriptions.list();
87
+ *
88
+ * // Poll for new events
89
+ * let cursor = 0;
90
+ * const { events, cursor: next } = await client.subscriptions.events({ since: cursor });
91
+ * cursor = next;
92
+ *
93
+ * // Remove a watch
94
+ * await client.subscriptions.delete(watch.id);
95
+ * ```
96
+ */
97
+ class SubscriptionsResource {
98
+ constructor(client) {
99
+ this.client = client;
100
+ }
101
+ /**
102
+ * List all subscriptions/watches for the authenticated user.
103
+ *
104
+ * @returns Array of subscriptions, newest first.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const subscriptions = await client.subscriptions.list();
109
+ * console.log(`You have ${subscriptions.length} watches`);
110
+ * ```
111
+ */
112
+ async list() {
113
+ const response = await this.client["request"]("/v1/subscriptions", {});
114
+ return Array.isArray(response) ? response : (response.subscriptions ?? []);
115
+ }
116
+ /**
117
+ * Create a new subscription/watch.
118
+ *
119
+ * Maps the friendly `interval` ("5m" / "1h" / "daily" / seconds) to the
120
+ * API's `interval_seconds`, and forwards optional attribution as
121
+ * `X-OPA-Source` / `X-OPA-Tool` headers (source defaults to `"sdk-node"`).
122
+ *
123
+ * @param params - Watch configuration. `codes` is required.
124
+ * @returns The created subscription.
125
+ *
126
+ * @throws {ValidationError} If `codes` is empty or `interval` is invalid.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const watch = await client.subscriptions.create({
131
+ * name: 'Crude desk',
132
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
133
+ * interval: '1h',
134
+ * tool: 'my-trading-bot',
135
+ * });
136
+ * ```
137
+ */
138
+ async create(params) {
139
+ if (!params || !Array.isArray(params.codes) || params.codes.length === 0) {
140
+ throw new errors_js_1.ValidationError("codes is required and must be a non-empty array of commodity codes");
141
+ }
142
+ if (params.codes.some((c) => typeof c !== "string" || c.trim() === "")) {
143
+ throw new errors_js_1.ValidationError("every code must be a non-empty string");
144
+ }
145
+ const intervalSeconds = intervalToSeconds(params.interval);
146
+ const body = {
147
+ codes: params.codes,
148
+ interval_seconds: intervalSeconds,
149
+ };
150
+ if (params.name !== undefined) {
151
+ body.name = params.name;
152
+ }
153
+ if (params.deliverWebhook !== undefined) {
154
+ body.deliver_webhook = params.deliverWebhook;
155
+ }
156
+ const headers = {
157
+ "X-OPA-Source": params.source ?? DEFAULT_SOURCE,
158
+ };
159
+ if (params.tool !== undefined) {
160
+ headers["X-OPA-Tool"] = params.tool;
161
+ }
162
+ const response = await this.client["request"]("/v1/subscriptions", {}, { method: "POST", body, headers });
163
+ return "subscription" in response ? response.subscription : response;
164
+ }
165
+ /**
166
+ * Delete a subscription/watch.
167
+ *
168
+ * @param id - The subscription ID to delete.
169
+ *
170
+ * @throws {ValidationError} If `id` is not a non-empty string.
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * await client.subscriptions.delete(watch.id);
175
+ * ```
176
+ */
177
+ async delete(id) {
178
+ if (!id || typeof id !== "string") {
179
+ throw new errors_js_1.ValidationError("Subscription ID must be a non-empty string");
180
+ }
181
+ await this.client["request"](`/v1/subscriptions/${id}`, {}, { method: "DELETE" });
182
+ }
183
+ /**
184
+ * Poll for events emitted by your watches.
185
+ *
186
+ * Returns events with `seq` greater than the supplied cursor, ordered
187
+ * ascending. This endpoint does NOT consume the monthly request quota.
188
+ *
189
+ * @param options - Cursor (`since`), optional `watchId`, and `limit`.
190
+ * @returns The next cursor, a `has_more` flag, and the events.
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * let cursor = 0;
195
+ * while (true) {
196
+ * const { events, cursor: next, has_more } = await client.subscriptions.events({ since: cursor });
197
+ * for (const ev of events) handle(ev);
198
+ * cursor = next;
199
+ * if (!has_more) break;
200
+ * }
201
+ * ```
202
+ */
203
+ async events(options = {}) {
204
+ const params = {};
205
+ if (options.since !== undefined) {
206
+ params.since = String(options.since);
207
+ }
208
+ if (options.watchId !== undefined) {
209
+ params.watch_id = options.watchId;
210
+ }
211
+ if (options.limit !== undefined) {
212
+ params.limit = String(options.limit);
213
+ }
214
+ return this.client["request"]("/v1/subscriptions/events", params);
215
+ }
216
+ }
217
+ exports.SubscriptionsResource = SubscriptionsResource;
@@ -11,7 +11,7 @@ exports.buildUserAgent = buildUserAgent;
11
11
  * - X-Client-Version header
12
12
  * - Package.json (should match)
13
13
  */
14
- exports.SDK_VERSION = "0.9.0";
14
+ exports.SDK_VERSION = "0.10.0";
15
15
  /**
16
16
  * SDK identifier used in User-Agent and X-Api-Client headers
17
17
  */
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { OilPriceAPIConfig, Price, LatestPricesOptions, HistoricalPricesOptions, Commodity, CommoditiesResponse, CategoriesResponse, DataConnectorPrice, DataConnectorOptions, DemoPricesResponse, DemoCommoditiesResponse } from "./types.js";
2
+ import type { MarketBrief, MarketBriefOptions } from "./resources/market-brief.js";
2
3
  import { DieselResource } from "./resources/diesel.js";
3
4
  import { AlertsResource } from "./resources/alerts.js";
4
5
  import { CommoditiesResource } from "./resources/commodities.js";
@@ -17,6 +18,7 @@ import { SpreadsResource } from "./resources/spreads.js";
17
18
  import { IndicatorsResource } from "./resources/indicators.js";
18
19
  import { RawResource } from "./resources/raw.js";
19
20
  import { StreamingResource } from "./resources/streaming.js";
21
+ import { SubscriptionsResource } from "./resources/subscriptions.js";
20
22
  /**
21
23
  * Raw HTTP response wrapper.
22
24
  *
@@ -144,9 +146,14 @@ export declare class OilPriceAPI {
144
146
  /**
145
147
  * Real-time price streaming resource (WebSocket / ActionCable).
146
148
  *
147
- * Streaming requires a Reservoir Mastery (Professional+) plan.
149
+ * Streaming requires a Professional plan ($99/mo) or higher.
148
150
  */
149
151
  readonly stream: StreamingResource;
152
+ /**
153
+ * Agent subscriptions ("watches") resource — persistent recurring watches
154
+ * over commodity codes plus an event poll endpoint (#3245 Phase 2).
155
+ */
156
+ readonly subscriptions: SubscriptionsResource;
150
157
  constructor(config?: OilPriceAPIConfig);
151
158
  /**
152
159
  * Log debug messages if debug mode is enabled
@@ -311,6 +318,34 @@ export declare class OilPriceAPI {
311
318
  * ```
312
319
  */
313
320
  getCommodity(code: string): Promise<Commodity>;
321
+ /**
322
+ * Get a multi-commodity market brief (OilPriceAPI #3245 Phase 1a).
323
+ *
324
+ * Returns a structured summary (latest price, 24h change, freshness, and a
325
+ * 1-month forecast band) for each requested commodity, optionally with a
326
+ * natural-language narrative. Counts as a single request against your quota,
327
+ * like `/v1/prices/batch`. The per-tier cap on `codes` is enforced server-side.
328
+ *
329
+ * @param codes - Commodity codes (e.g. ["BRENT_CRUDE_USD", "WTI_USD"]). Shorthand
330
+ * codes like "WTI"/"BRENT" are accepted and resolved server-side.
331
+ * @param options - `{ narrative }` to request the natural-language summary.
332
+ * @returns The structured (and optional narrative) market brief.
333
+ *
334
+ * @throws {ValidationError} If `codes` is empty.
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * const brief = await client.getMarketBrief(['BRENT_CRUDE_USD', 'WTI_USD']);
339
+ * for (const c of brief.commodities) {
340
+ * console.log(`${c.name}: $${c.price} (${c.change_24h_pct}%)`);
341
+ * }
342
+ *
343
+ * // With narrative
344
+ * const withText = await client.getMarketBrief(['BRENT_CRUDE_USD'], { narrative: true });
345
+ * console.log(withText.narrative);
346
+ * ```
347
+ */
348
+ getMarketBrief(codes: string[], options?: MarketBriefOptions): Promise<MarketBrief>;
314
349
  /**
315
350
  * Fetch live sample prices from the public, no-auth demo endpoint.
316
351
  *