oilpriceapi 0.9.1 → 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; } });
@@ -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.1";
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
  *
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { OilPriceAPIError, AuthenticationError, RateLimitError, NotFoundError, ServerError, TimeoutError, } from "./errors.js";
1
+ import { OilPriceAPIError, AuthenticationError, RateLimitError, NotFoundError, ServerError, TimeoutError, ValidationError, } from "./errors.js";
2
2
  import { DieselResource } from "./resources/diesel.js";
3
3
  import { AlertsResource } from "./resources/alerts.js";
4
4
  import { CommoditiesResource } from "./resources/commodities.js";
@@ -18,6 +18,7 @@ import { SpreadsResource } from "./resources/spreads.js";
18
18
  import { IndicatorsResource } from "./resources/indicators.js";
19
19
  import { RawResource } from "./resources/raw.js";
20
20
  import { StreamingResource } from "./resources/streaming.js";
21
+ import { SubscriptionsResource } from "./resources/subscriptions.js";
21
22
  /**
22
23
  * Official Node.js client for Oil Price API
23
24
  *
@@ -77,6 +78,7 @@ export class OilPriceAPI {
77
78
  this.indicators = new IndicatorsResource(this);
78
79
  this.raw = new RawResource(this);
79
80
  this.stream = new StreamingResource(this);
81
+ this.subscriptions = new SubscriptionsResource(this);
80
82
  }
81
83
  /**
82
84
  * Log debug messages if debug mode is enabled
@@ -212,6 +214,14 @@ export class OilPriceAPI {
212
214
  if (this.appName) {
213
215
  headers["X-App-Name"] = this.appName;
214
216
  }
217
+ // Per-request headers (e.g. MCP attribution X-OPA-Source / X-OPA-Tool).
218
+ if (options?.headers) {
219
+ Object.entries(options.headers).forEach(([key, value]) => {
220
+ if (value !== undefined && value !== null) {
221
+ headers[key] = value;
222
+ }
223
+ });
224
+ }
215
225
  const fetchOptions = {
216
226
  method: options?.method || "GET",
217
227
  headers,
@@ -521,6 +531,45 @@ export class OilPriceAPI {
521
531
  async getCommodity(code) {
522
532
  return this.request(`/v1/commodities/${code}`, {});
523
533
  }
534
+ /**
535
+ * Get a multi-commodity market brief (OilPriceAPI #3245 Phase 1a).
536
+ *
537
+ * Returns a structured summary (latest price, 24h change, freshness, and a
538
+ * 1-month forecast band) for each requested commodity, optionally with a
539
+ * natural-language narrative. Counts as a single request against your quota,
540
+ * like `/v1/prices/batch`. The per-tier cap on `codes` is enforced server-side.
541
+ *
542
+ * @param codes - Commodity codes (e.g. ["BRENT_CRUDE_USD", "WTI_USD"]). Shorthand
543
+ * codes like "WTI"/"BRENT" are accepted and resolved server-side.
544
+ * @param options - `{ narrative }` to request the natural-language summary.
545
+ * @returns The structured (and optional narrative) market brief.
546
+ *
547
+ * @throws {ValidationError} If `codes` is empty.
548
+ *
549
+ * @example
550
+ * ```typescript
551
+ * const brief = await client.getMarketBrief(['BRENT_CRUDE_USD', 'WTI_USD']);
552
+ * for (const c of brief.commodities) {
553
+ * console.log(`${c.name}: $${c.price} (${c.change_24h_pct}%)`);
554
+ * }
555
+ *
556
+ * // With narrative
557
+ * const withText = await client.getMarketBrief(['BRENT_CRUDE_USD'], { narrative: true });
558
+ * console.log(withText.narrative);
559
+ * ```
560
+ */
561
+ async getMarketBrief(codes, options) {
562
+ if (!Array.isArray(codes) || codes.length === 0) {
563
+ throw new ValidationError("codes is required and must be a non-empty array of commodity codes");
564
+ }
565
+ const params = {
566
+ codes: codes.join(","),
567
+ };
568
+ if (options?.narrative) {
569
+ params.narrative = "true";
570
+ }
571
+ return this.request("/v1/market-brief", params);
572
+ }
524
573
  /**
525
574
  * Fetch live sample prices from the public, no-auth demo endpoint.
526
575
  *
package/dist/index.d.ts CHANGED
@@ -26,6 +26,9 @@ export type { MonthlyForecast, ForecastAccuracy, ArchivedForecast } from "./reso
26
26
  export type { DataQualitySummary, DataQualityReportMeta, DataQualityReport, } from "./resources/data-quality.js";
27
27
  export type { DrillingIntelligenceData, LatestDrillingData, DrillingSummary, DrillingTrend, FracSpreadData, WellPermitData, DUCWellData, CompletionData, WellsDrilledData, BasinDrillingData, } from "./resources/drilling.js";
28
28
  export type { WellTimelineEvent, WellTimeline, RigCountRecord, RigCountByBasin, RigCountByState, HistoricalRigCount, OilInventoryRecord, OilInventorySummary, InventoryByProduct, HistoricalInventory, CushingInventory, OPECProductionRecord, TotalOPECProduction, ProductionByCountry, HistoricalProduction, TopProducer, DrillingProductivityRecord, DrillingProductivitySummary, DUCWellInventory, ProductivityByBasin, HistoricalProductivity, ProductivityTrend, ForecastRecord, ForecastSummary, PriceForecast, ProductionForecast, HistoricalForecast, ForecastComparison, WellPermitRecord, WellPermitSummary, PermitsByState, PermitsByOperator, PermitsByFormation, WellPermitSearchQuery, FracFocusRecord, FracFocusSummary, DisclosuresByState, DisclosuresByOperator, ChemicalUsage, WellChemical, FracFocusSearchQuery, } from "./resources/ei/index.js";
29
+ export type { MarketBrief, MarketBriefCommodity, MarketBriefForecast, MarketBriefOptions, } from "./resources/market-brief.js";
30
+ export type { Subscription, SubscriptionStatus, SubscriptionSource, SubscriptionInterval, CreateSubscriptionParams, SubscriptionEvent, SubscriptionEventsResult, SubscriptionEventsOptions, } from "./resources/subscriptions.js";
31
+ export { SubscriptionsResource, intervalToSeconds } from "./resources/subscriptions.js";
29
32
  export type { WebhookEndpoint, CreateWebhookParams, UpdateWebhookParams, WebhookTestResponse as WebhookTestResult, WebhookEvent, } from "./resources/webhooks.js";
30
33
  export type { DataSourceType, DataSourceStatus, DataSource, CreateDataSourceParams, UpdateDataSourceParams, DataSourceTestResponse, DataSourceLog, DataSourceHealth, CredentialRotationResponse, } from "./resources/data-sources.js";
31
34
  export { OilPriceAPIError, AuthenticationError, RateLimitError, NotFoundError, ServerError, ValidationError, TimeoutError, } from "./errors.js";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { FUTURES_CONTRACTS, FUTURES_FAMILY_SLUGS, FuturesContractFamily, } from
12
12
  export { SpreadsResource } from "./resources/spreads.js";
13
13
  export { IndicatorsResource } from "./resources/indicators.js";
14
14
  export { RawResource } from "./resources/raw.js";
15
+ export { SubscriptionsResource, intervalToSeconds } from "./resources/subscriptions.js";
15
16
  export { OilPriceAPIError, AuthenticationError, RateLimitError, NotFoundError, ServerError, ValidationError, TimeoutError, } from "./errors.js";
16
17
  export { DieselResource } from "./resources/diesel.js";
17
18
  export { AlertsResource } from "./resources/alerts.js";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Market Brief types (OilPriceAPI #3245 Phase 1a)
3
+ *
4
+ * A multi-commodity structured (+ optional narrative) market summary composed
5
+ * from existing price + forecast data. Served by `GET /v1/market-brief` and
6
+ * surfaced on the client as {@link OilPriceAPI.getMarketBrief}.
7
+ */
8
+ /**
9
+ * Short-horizon (1-month) point forecast for a commodity in a market brief.
10
+ */
11
+ export interface MarketBriefForecast {
12
+ /** Central point estimate. */
13
+ point: number;
14
+ /** Low end of the forecast band. */
15
+ low: number;
16
+ /** High end of the forecast band. */
17
+ high: number;
18
+ /** Model confidence label (e.g. "low", "medium", "high"). */
19
+ confidence: string;
20
+ }
21
+ /**
22
+ * One commodity's slice of a market brief.
23
+ */
24
+ export interface MarketBriefCommodity {
25
+ /** Commodity code (e.g. "BRENT_CRUDE_USD"). */
26
+ code: string;
27
+ /** Human-readable commodity name. */
28
+ name: string;
29
+ /** Latest price. */
30
+ price: number;
31
+ /** ISO currency code (e.g. "USD"). */
32
+ currency: string;
33
+ /** Price unit (e.g. "barrel"). */
34
+ unit: string;
35
+ /** 24h percentage change. */
36
+ change_24h_pct: number;
37
+ /** 24h absolute change. */
38
+ change_24h_abs: number;
39
+ /** ISO timestamp the price was observed. */
40
+ as_of: string;
41
+ /** Upstream data source. */
42
+ source: string;
43
+ /** True when the price is stale (no fresh update). */
44
+ stale: boolean;
45
+ /** 1-month forecast band, when available. */
46
+ forecast_1m: MarketBriefForecast | null;
47
+ }
48
+ /**
49
+ * A multi-commodity market brief.
50
+ *
51
+ * Returned by {@link OilPriceAPI.getMarketBrief}.
52
+ */
53
+ export interface MarketBrief {
54
+ /** ISO timestamp the brief was generated. */
55
+ as_of: string;
56
+ /** The (resolved, canonical) codes included in the brief. */
57
+ codes: string[];
58
+ /** Per-commodity structured summary. */
59
+ commodities: MarketBriefCommodity[];
60
+ /** Optional natural-language narrative (only when `narrative: true`). */
61
+ narrative?: string;
62
+ }
63
+ /**
64
+ * Options for {@link OilPriceAPI.getMarketBrief}.
65
+ */
66
+ export interface MarketBriefOptions {
67
+ /**
68
+ * Request a natural-language narrative alongside the structured data.
69
+ * Defaults to `false`.
70
+ */
71
+ narrative?: boolean;
72
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Market Brief types (OilPriceAPI #3245 Phase 1a)
3
+ *
4
+ * A multi-commodity structured (+ optional narrative) market summary composed
5
+ * from existing price + forecast data. Served by `GET /v1/market-brief` and
6
+ * surfaced on the client as {@link OilPriceAPI.getMarketBrief}.
7
+ */
8
+ export {};
@@ -4,7 +4,7 @@
4
4
  * Real-time price streaming via the OilPriceAPI ActionCable endpoint
5
5
  * (`wss://api.oilpriceapi.com/cable`).
6
6
  *
7
- * Streaming is a **Reservoir Mastery (Professional+)** feature. Connections
7
+ * Streaming is a **Professional plan ($99/mo) or higher** feature. Connections
8
8
  * authenticate with your API key and subscribe to the `EnergyPricesChannel`,
9
9
  * which pushes an initial `welcome` snapshot followed by live `price_update`
10
10
  * and (for drilling-tier accounts) `rig_count_update` messages.
@@ -105,7 +105,7 @@ export interface WelcomeMessage {
105
105
  };
106
106
  }
107
107
  /**
108
- * Rig-count update broadcast (drilling / Reservoir Mastery accounts).
108
+ * Rig-count update broadcast (drilling / Professional+ accounts).
109
109
  */
110
110
  export interface RigCountUpdateMessage {
111
111
  type: "rig_count_update";
@@ -4,7 +4,7 @@
4
4
  * Real-time price streaming via the OilPriceAPI ActionCable endpoint
5
5
  * (`wss://api.oilpriceapi.com/cable`).
6
6
  *
7
- * Streaming is a **Reservoir Mastery (Professional+)** feature. Connections
7
+ * Streaming is a **Professional plan ($99/mo) or higher** feature. Connections
8
8
  * authenticate with your API key and subscribe to the `EnergyPricesChannel`,
9
9
  * which pushes an initial `welcome` snapshot followed by live `price_update`
10
10
  * and (for drilling-tier accounts) `rig_count_update` messages.
@@ -157,8 +157,8 @@ export class PriceStreamSubscription extends EventEmitter {
157
157
  return;
158
158
  }
159
159
  if (transportType === "reject_subscription") {
160
- this.emit("error", new Error("WebSocket subscription rejected. Streaming requires a Reservoir Mastery " +
161
- "(Professional+) plan and a valid API key."));
160
+ this.emit("error", new Error("WebSocket subscription rejected. Streaming requires a Professional " +
161
+ "plan ($99/mo) or higher and a valid API key."));
162
162
  return;
163
163
  }
164
164
  if (transportType === "disconnect") {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Agent Subscriptions ("Watches") Resource
3
+ *
4
+ * Persistent server-side "watches" that evaluate a set of commodity codes on a
5
+ * recurring interval and emit events an agent can poll for (OilPriceAPI #3245
6
+ * Phase 2). Designed for autonomous agents (MCP, schedulers, bots) that want
7
+ * change notifications without holding an open connection.
8
+ *
9
+ * The poll endpoint (`events`) does NOT consume the monthly request quota and
10
+ * has its own generous rate-limit lane, so agents can poll frequently.
11
+ */
12
+ import type { OilPriceAPI } from "../client.js";
13
+ /**
14
+ * Lifecycle status of a subscription/watch.
15
+ */
16
+ export type SubscriptionStatus = "active" | "paused";
17
+ /**
18
+ * Attribution source recorded on a watch. Defaults to `"sdk-node"` when created
19
+ * via this SDK. The API canonicalizes unknown values to `"api"`.
20
+ */
21
+ export type SubscriptionSource = string;
22
+ /**
23
+ * A persistent agent subscription ("watch").
24
+ *
25
+ * Returned by {@link SubscriptionsResource.list} and
26
+ * {@link SubscriptionsResource.create}.
27
+ */
28
+ export interface Subscription {
29
+ /** Unique watch identifier (UUID). */
30
+ id: string;
31
+ /** User-friendly watch name. */
32
+ name: string | null;
33
+ /** Commodity codes this watch evaluates (e.g. ["BRENT_CRUDE_USD"]). */
34
+ codes: string[];
35
+ /** Evaluation cadence in seconds. */
36
+ interval_seconds: number;
37
+ /** Lifecycle status. */
38
+ status: SubscriptionStatus;
39
+ /** Whether matching events are also delivered via webhook. */
40
+ deliver_webhook: boolean;
41
+ /** Attribution source (e.g. "sdk-node", "mcp", "api"). */
42
+ source: string;
43
+ /** Attribution tool name, if any. */
44
+ tool_name: string | null;
45
+ /** ISO timestamp the watch was last evaluated, or null. */
46
+ last_evaluated_at: string | null;
47
+ /** ISO timestamp the watch is next scheduled to run, or null. */
48
+ next_run_at: string | null;
49
+ /** ISO timestamp when the watch was created. */
50
+ created_at: string;
51
+ }
52
+ /**
53
+ * A friendly interval expression accepted by {@link SubscriptionsResource.create}.
54
+ *
55
+ * Either a preset string ("5m", "15m", "1h", "daily") or an explicit number of
56
+ * seconds.
57
+ */
58
+ export type SubscriptionInterval = "5m" | "15m" | "1h" | "daily" | (string & {}) | number;
59
+ /**
60
+ * Parameters for creating a new subscription/watch.
61
+ */
62
+ export interface CreateSubscriptionParams {
63
+ /** Commodity codes to watch (e.g. ["BRENT_CRUDE_USD", "WTI_USD"]). Required. */
64
+ codes: string[];
65
+ /**
66
+ * Evaluation cadence. A friendly preset ("5m" / "15m" / "1h" / "daily"), a
67
+ * `<n>m` / `<n>h` / `<n>d` / `<n>s` expression, or a number of seconds.
68
+ * Defaults to "5m" when omitted.
69
+ */
70
+ interval?: SubscriptionInterval;
71
+ /** Optional friendly watch name. */
72
+ name?: string;
73
+ /** Whether to also deliver matching events via webhook. */
74
+ deliverWebhook?: boolean;
75
+ /**
76
+ * Attribution source → `X-OPA-Source` header. Defaults to `"sdk-node"`.
77
+ */
78
+ source?: string;
79
+ /** Attribution tool name → `X-OPA-Tool` header. */
80
+ tool?: string;
81
+ }
82
+ /**
83
+ * A single event emitted by a watch evaluation.
84
+ *
85
+ * The exact payload depends on the event type; common fields are surfaced here
86
+ * while the full server payload is preserved via the index signature.
87
+ */
88
+ export interface SubscriptionEvent {
89
+ /** Monotonic per-user sequence number; use as the `since` cursor. */
90
+ seq: number;
91
+ /** The watch that produced this event. */
92
+ watch_id: string;
93
+ /** Event type (e.g. "evaluated", "threshold_crossed"). */
94
+ type?: string;
95
+ /** Commodity code the event concerns, if applicable. */
96
+ code?: string;
97
+ /** ISO timestamp the event was emitted. */
98
+ created_at?: string;
99
+ /** Any additional server-provided fields. */
100
+ [key: string]: unknown;
101
+ }
102
+ /**
103
+ * Response from {@link SubscriptionsResource.events}.
104
+ */
105
+ export interface SubscriptionEventsResult {
106
+ /** The highest `seq` returned; pass as `since` on the next poll. */
107
+ cursor: number;
108
+ /** True if more events are available beyond this page. */
109
+ has_more: boolean;
110
+ /** Events with `seq > since`, ordered ascending by `seq`. */
111
+ events: SubscriptionEvent[];
112
+ }
113
+ /**
114
+ * Options for {@link SubscriptionsResource.events}.
115
+ */
116
+ export interface SubscriptionEventsOptions {
117
+ /** Return only events with `seq` greater than this cursor. Defaults to 0. */
118
+ since?: number;
119
+ /** Restrict to a single watch by id. */
120
+ watchId?: string;
121
+ /** Max events to return (1-500, server default 100). */
122
+ limit?: number;
123
+ }
124
+ /**
125
+ * Convert a friendly interval ("5m" / "1h" / "daily" / 300) into seconds.
126
+ *
127
+ * Accepts:
128
+ * - presets: "5m", "15m", "1h", "hourly", "daily"
129
+ * - unit expressions: "<n>s", "<n>m", "<n>h", "<n>d"
130
+ * - a raw number of seconds
131
+ *
132
+ * @internal Exported for unit testing of the mapping.
133
+ */
134
+ export declare function intervalToSeconds(interval: SubscriptionInterval | undefined): number;
135
+ /**
136
+ * Agent Subscriptions ("Watches") Resource
137
+ *
138
+ * Manage persistent, recurring watches over commodity codes and poll for the
139
+ * events they emit.
140
+ *
141
+ * **Example:**
142
+ * ```typescript
143
+ * import { OilPriceAPI } from 'oilpriceapi';
144
+ *
145
+ * const client = new OilPriceAPI({ apiKey: 'your_key' });
146
+ *
147
+ * // Create a watch that evaluates Brent + WTI every 5 minutes
148
+ * const watch = await client.subscriptions.create({
149
+ * name: 'Crude desk',
150
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
151
+ * interval: '5m',
152
+ * });
153
+ *
154
+ * // List all watches
155
+ * const watches = await client.subscriptions.list();
156
+ *
157
+ * // Poll for new events
158
+ * let cursor = 0;
159
+ * const { events, cursor: next } = await client.subscriptions.events({ since: cursor });
160
+ * cursor = next;
161
+ *
162
+ * // Remove a watch
163
+ * await client.subscriptions.delete(watch.id);
164
+ * ```
165
+ */
166
+ export declare class SubscriptionsResource {
167
+ private client;
168
+ constructor(client: OilPriceAPI);
169
+ /**
170
+ * List all subscriptions/watches for the authenticated user.
171
+ *
172
+ * @returns Array of subscriptions, newest first.
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * const subscriptions = await client.subscriptions.list();
177
+ * console.log(`You have ${subscriptions.length} watches`);
178
+ * ```
179
+ */
180
+ list(): Promise<Subscription[]>;
181
+ /**
182
+ * Create a new subscription/watch.
183
+ *
184
+ * Maps the friendly `interval` ("5m" / "1h" / "daily" / seconds) to the
185
+ * API's `interval_seconds`, and forwards optional attribution as
186
+ * `X-OPA-Source` / `X-OPA-Tool` headers (source defaults to `"sdk-node"`).
187
+ *
188
+ * @param params - Watch configuration. `codes` is required.
189
+ * @returns The created subscription.
190
+ *
191
+ * @throws {ValidationError} If `codes` is empty or `interval` is invalid.
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const watch = await client.subscriptions.create({
196
+ * name: 'Crude desk',
197
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
198
+ * interval: '1h',
199
+ * tool: 'my-trading-bot',
200
+ * });
201
+ * ```
202
+ */
203
+ create(params: CreateSubscriptionParams): Promise<Subscription>;
204
+ /**
205
+ * Delete a subscription/watch.
206
+ *
207
+ * @param id - The subscription ID to delete.
208
+ *
209
+ * @throws {ValidationError} If `id` is not a non-empty string.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * await client.subscriptions.delete(watch.id);
214
+ * ```
215
+ */
216
+ delete(id: string): Promise<void>;
217
+ /**
218
+ * Poll for events emitted by your watches.
219
+ *
220
+ * Returns events with `seq` greater than the supplied cursor, ordered
221
+ * ascending. This endpoint does NOT consume the monthly request quota.
222
+ *
223
+ * @param options - Cursor (`since`), optional `watchId`, and `limit`.
224
+ * @returns The next cursor, a `has_more` flag, and the events.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * let cursor = 0;
229
+ * while (true) {
230
+ * const { events, cursor: next, has_more } = await client.subscriptions.events({ since: cursor });
231
+ * for (const ev of events) handle(ev);
232
+ * cursor = next;
233
+ * if (!has_more) break;
234
+ * }
235
+ * ```
236
+ */
237
+ events(options?: SubscriptionEventsOptions): Promise<SubscriptionEventsResult>;
238
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Agent Subscriptions ("Watches") Resource
3
+ *
4
+ * Persistent server-side "watches" that evaluate a set of commodity codes on a
5
+ * recurring interval and emit events an agent can poll for (OilPriceAPI #3245
6
+ * Phase 2). Designed for autonomous agents (MCP, schedulers, bots) that want
7
+ * change notifications without holding an open connection.
8
+ *
9
+ * The poll endpoint (`events`) does NOT consume the monthly request quota and
10
+ * has its own generous rate-limit lane, so agents can poll frequently.
11
+ */
12
+ import { ValidationError } from "../errors.js";
13
+ /** Default attribution source for watches created via this SDK. */
14
+ const DEFAULT_SOURCE = "sdk-node";
15
+ /** Named interval presets mapped to seconds. */
16
+ const INTERVAL_PRESETS = {
17
+ "5m": 300,
18
+ "15m": 900,
19
+ "1h": 3600,
20
+ hourly: 3600,
21
+ daily: 86400,
22
+ };
23
+ /**
24
+ * Convert a friendly interval ("5m" / "1h" / "daily" / 300) into seconds.
25
+ *
26
+ * Accepts:
27
+ * - presets: "5m", "15m", "1h", "hourly", "daily"
28
+ * - unit expressions: "<n>s", "<n>m", "<n>h", "<n>d"
29
+ * - a raw number of seconds
30
+ *
31
+ * @internal Exported for unit testing of the mapping.
32
+ */
33
+ export function intervalToSeconds(interval) {
34
+ if (interval === undefined) {
35
+ return INTERVAL_PRESETS["5m"];
36
+ }
37
+ if (typeof interval === "number") {
38
+ if (!Number.isFinite(interval) || interval <= 0) {
39
+ throw new ValidationError("interval (seconds) must be a positive number");
40
+ }
41
+ return Math.floor(interval);
42
+ }
43
+ const key = interval.trim().toLowerCase();
44
+ if (key in INTERVAL_PRESETS) {
45
+ return INTERVAL_PRESETS[key];
46
+ }
47
+ // Unit expression: <number><unit> where unit ∈ s/m/h/d.
48
+ const match = /^(\d+)\s*(s|m|h|d)$/.exec(key);
49
+ if (match) {
50
+ const value = parseInt(match[1], 10);
51
+ const unit = match[2];
52
+ const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
53
+ const seconds = value * multipliers[unit];
54
+ if (seconds <= 0) {
55
+ throw new ValidationError("interval must be greater than zero");
56
+ }
57
+ return seconds;
58
+ }
59
+ throw new ValidationError(`Invalid interval "${interval}". Use a preset ("5m", "15m", "1h", "daily"), ` +
60
+ `a unit expression ("30s", "10m", "2h", "1d"), or a number of seconds.`);
61
+ }
62
+ /**
63
+ * Agent Subscriptions ("Watches") Resource
64
+ *
65
+ * Manage persistent, recurring watches over commodity codes and poll for the
66
+ * events they emit.
67
+ *
68
+ * **Example:**
69
+ * ```typescript
70
+ * import { OilPriceAPI } from 'oilpriceapi';
71
+ *
72
+ * const client = new OilPriceAPI({ apiKey: 'your_key' });
73
+ *
74
+ * // Create a watch that evaluates Brent + WTI every 5 minutes
75
+ * const watch = await client.subscriptions.create({
76
+ * name: 'Crude desk',
77
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
78
+ * interval: '5m',
79
+ * });
80
+ *
81
+ * // List all watches
82
+ * const watches = await client.subscriptions.list();
83
+ *
84
+ * // Poll for new events
85
+ * let cursor = 0;
86
+ * const { events, cursor: next } = await client.subscriptions.events({ since: cursor });
87
+ * cursor = next;
88
+ *
89
+ * // Remove a watch
90
+ * await client.subscriptions.delete(watch.id);
91
+ * ```
92
+ */
93
+ export class SubscriptionsResource {
94
+ constructor(client) {
95
+ this.client = client;
96
+ }
97
+ /**
98
+ * List all subscriptions/watches for the authenticated user.
99
+ *
100
+ * @returns Array of subscriptions, newest first.
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const subscriptions = await client.subscriptions.list();
105
+ * console.log(`You have ${subscriptions.length} watches`);
106
+ * ```
107
+ */
108
+ async list() {
109
+ const response = await this.client["request"]("/v1/subscriptions", {});
110
+ return Array.isArray(response) ? response : (response.subscriptions ?? []);
111
+ }
112
+ /**
113
+ * Create a new subscription/watch.
114
+ *
115
+ * Maps the friendly `interval` ("5m" / "1h" / "daily" / seconds) to the
116
+ * API's `interval_seconds`, and forwards optional attribution as
117
+ * `X-OPA-Source` / `X-OPA-Tool` headers (source defaults to `"sdk-node"`).
118
+ *
119
+ * @param params - Watch configuration. `codes` is required.
120
+ * @returns The created subscription.
121
+ *
122
+ * @throws {ValidationError} If `codes` is empty or `interval` is invalid.
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * const watch = await client.subscriptions.create({
127
+ * name: 'Crude desk',
128
+ * codes: ['BRENT_CRUDE_USD', 'WTI_USD'],
129
+ * interval: '1h',
130
+ * tool: 'my-trading-bot',
131
+ * });
132
+ * ```
133
+ */
134
+ async create(params) {
135
+ if (!params || !Array.isArray(params.codes) || params.codes.length === 0) {
136
+ throw new ValidationError("codes is required and must be a non-empty array of commodity codes");
137
+ }
138
+ if (params.codes.some((c) => typeof c !== "string" || c.trim() === "")) {
139
+ throw new ValidationError("every code must be a non-empty string");
140
+ }
141
+ const intervalSeconds = intervalToSeconds(params.interval);
142
+ const body = {
143
+ codes: params.codes,
144
+ interval_seconds: intervalSeconds,
145
+ };
146
+ if (params.name !== undefined) {
147
+ body.name = params.name;
148
+ }
149
+ if (params.deliverWebhook !== undefined) {
150
+ body.deliver_webhook = params.deliverWebhook;
151
+ }
152
+ const headers = {
153
+ "X-OPA-Source": params.source ?? DEFAULT_SOURCE,
154
+ };
155
+ if (params.tool !== undefined) {
156
+ headers["X-OPA-Tool"] = params.tool;
157
+ }
158
+ const response = await this.client["request"]("/v1/subscriptions", {}, { method: "POST", body, headers });
159
+ return "subscription" in response ? response.subscription : response;
160
+ }
161
+ /**
162
+ * Delete a subscription/watch.
163
+ *
164
+ * @param id - The subscription ID to delete.
165
+ *
166
+ * @throws {ValidationError} If `id` is not a non-empty string.
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * await client.subscriptions.delete(watch.id);
171
+ * ```
172
+ */
173
+ async delete(id) {
174
+ if (!id || typeof id !== "string") {
175
+ throw new ValidationError("Subscription ID must be a non-empty string");
176
+ }
177
+ await this.client["request"](`/v1/subscriptions/${id}`, {}, { method: "DELETE" });
178
+ }
179
+ /**
180
+ * Poll for events emitted by your watches.
181
+ *
182
+ * Returns events with `seq` greater than the supplied cursor, ordered
183
+ * ascending. This endpoint does NOT consume the monthly request quota.
184
+ *
185
+ * @param options - Cursor (`since`), optional `watchId`, and `limit`.
186
+ * @returns The next cursor, a `has_more` flag, and the events.
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * let cursor = 0;
191
+ * while (true) {
192
+ * const { events, cursor: next, has_more } = await client.subscriptions.events({ since: cursor });
193
+ * for (const ev of events) handle(ev);
194
+ * cursor = next;
195
+ * if (!has_more) break;
196
+ * }
197
+ * ```
198
+ */
199
+ async events(options = {}) {
200
+ const params = {};
201
+ if (options.since !== undefined) {
202
+ params.since = String(options.since);
203
+ }
204
+ if (options.watchId !== undefined) {
205
+ params.watch_id = options.watchId;
206
+ }
207
+ if (options.limit !== undefined) {
208
+ params.limit = String(options.limit);
209
+ }
210
+ return this.client["request"]("/v1/subscriptions/events", params);
211
+ }
212
+ }
package/dist/version.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * - X-Client-Version header
8
8
  * - Package.json (should match)
9
9
  */
10
- export declare const SDK_VERSION = "0.9.1";
10
+ export declare const SDK_VERSION = "0.10.0";
11
11
  /**
12
12
  * SDK identifier used in User-Agent and X-Api-Client headers
13
13
  */
package/dist/version.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * - X-Client-Version header
8
8
  * - Package.json (should match)
9
9
  */
10
- export const SDK_VERSION = "0.9.1";
10
+ export const SDK_VERSION = "0.10.0";
11
11
  /**
12
12
  * SDK identifier used in User-Agent and X-Api-Client headers
13
13
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oilpriceapi",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Official Node.js SDK for Oil Price API - Real-time and historical oil & commodity prices",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",