@oxy-hq/sdk 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,8 +28,90 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
 
29
29
  //#endregion
30
30
  let react = require("react");
31
- react = __toESM(react);
31
+ react = __toESM(react, 1);
32
+ let react_jsx_runtime = require("react/jsx-runtime");
32
33
 
34
+ //#region src/anomalies.ts
35
+ /**
36
+ * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
37
+ * rather than instantiating directly — the getter wires the request helper
38
+ * so auth, timeout, and branch propagation come along for free.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
43
+ * for (const a of anomalies) {
44
+ * console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));
45
+ * }
46
+ * ```
47
+ */
48
+ var AnomaliesClient = class {
49
+ constructor(config, request) {
50
+ this.config = config;
51
+ this.request = request;
52
+ }
53
+ path(suffix) {
54
+ return `/${this.config.projectId}/semantic/anomalies${suffix}`;
55
+ }
56
+ buildQuery(extra = {}) {
57
+ const params = { ...extra };
58
+ if (this.config.branch) params.branch = this.config.branch;
59
+ const qs = new URLSearchParams(params).toString();
60
+ return qs ? `?${qs}` : "";
61
+ }
62
+ /**
63
+ * List anomalies in the inbox, newest first.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * // Open / unresolved anomalies only
68
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
69
+ * ```
70
+ */
71
+ async list(options = {}) {
72
+ const extra = {};
73
+ if (options.status) extra.status = options.status;
74
+ if (options.limit) extra.limit = String(options.limit);
75
+ return this.request(this.path(this.buildQuery(extra)));
76
+ }
77
+ /**
78
+ * Trigger a full scan. Iterates every `.monitor.yml` entry in the
79
+ * workspace, runs the detector, and upserts matching rows into the
80
+ * inbox. Returns counts of scanned / failed / persisted.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * // Scan against a known-good reference date (matches the seed dataset)
85
+ * const result = await client.anomalies.scan({ as_of: "2025-12-15" });
86
+ * console.log(`${result.anomalies_persisted} anomalies detected`);
87
+ * ```
88
+ */
89
+ async scan(options = {}) {
90
+ const extra = {};
91
+ if (options.as_of) extra.as_of = options.as_of;
92
+ return this.request(this.path(`/scan${this.buildQuery(extra)}`), { method: "POST" });
93
+ }
94
+ /**
95
+ * Update an anomaly's status (acknowledge / dismiss / re-open).
96
+ */
97
+ async updateStatus(anomalyId, status) {
98
+ const query = this.buildQuery();
99
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {
100
+ method: "POST",
101
+ body: JSON.stringify({ status })
102
+ });
103
+ }
104
+ /**
105
+ * Run the metric-tree `explain` for an anomaly and cache the result on
106
+ * the row. Subsequent calls return the cached `ExplainResult` instantly.
107
+ */
108
+ async explain(anomalyId) {
109
+ const query = this.buildQuery();
110
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
111
+ }
112
+ };
113
+
114
+ //#endregion
33
115
  //#region src/customer-app/logger.ts
34
116
  let activeLogger = createConsoleLogger();
35
117
  /** Replace the global logger. Pass `null` to silence everything. */
@@ -284,13 +366,156 @@ function validateManifest(raw, url) {
284
366
  name: typeof raw.name === "string" ? raw.name : void 0,
285
367
  slug: raw.slug,
286
368
  orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
287
- projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
369
+ projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
370
+ functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
288
371
  };
289
372
  }
373
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
374
+ /**
375
+ * Validate the optional `functions` map. Each key is a function name;
376
+ * each value declares how the function is invoked. Mirrors the
377
+ * server-side validation in `customer_apps_publish.rs` so a bad
378
+ * manifest fails at build, not at publish.
379
+ */
380
+ function validateFunctions(raw) {
381
+ if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
382
+ const out = {};
383
+ for (const [fnName, value] of Object.entries(raw)) {
384
+ if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
385
+ if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
386
+ const fn = {};
387
+ if (value.entry !== void 0) {
388
+ if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
389
+ fn.entry = value.entry;
390
+ }
391
+ if (value.schedule !== void 0) {
392
+ if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
393
+ fn.schedule = value.schedule;
394
+ }
395
+ if (value.timezone !== void 0) {
396
+ if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
397
+ fn.timezone = value.timezone;
398
+ }
399
+ if (value.route !== void 0) {
400
+ if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
401
+ fn.route = value.route;
402
+ }
403
+ if (value.airwayStep !== void 0) {
404
+ const step = value.airwayStep;
405
+ if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
406
+ fn.airwayStep = {
407
+ pipeline: step.pipeline,
408
+ resource: step.resource
409
+ };
410
+ }
411
+ if (value.timeoutSeconds !== void 0) {
412
+ const t = value.timeoutSeconds;
413
+ if (typeof t !== "number" || !Number.isInteger(t) || t < 1 || t > 300) throw new Error(`oxy-app.json: function "${fnName}" \`timeoutSeconds\` must be an integer in [1, 300]`);
414
+ fn.timeoutSeconds = t;
415
+ }
416
+ if (value.cache !== void 0) {
417
+ const c = value.cache;
418
+ if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
419
+ if (c.ttlSeconds !== void 0) {
420
+ const ttl = c.ttlSeconds;
421
+ if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
422
+ fn.cache = { ttlSeconds: ttl };
423
+ }
424
+ }
425
+ const hasSchedule = fn.schedule !== void 0;
426
+ const hasAirway = fn.airwayStep !== void 0;
427
+ if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
428
+ out[fnName] = fn;
429
+ }
430
+ return out;
431
+ }
290
432
  function isRecord(v) {
291
433
  return typeof v === "object" && v !== null && !Array.isArray(v);
292
434
  }
293
435
 
436
+ //#endregion
437
+ //#region src/customer-app/function-invoke.ts
438
+ const inflight = /* @__PURE__ */ new Map();
439
+ /**
440
+ * Dedup key for an invocation: function name + its (stable-serialized) body,
441
+ * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
442
+ * the separator can never collide with a name.
443
+ */
444
+ function functionInvokeKey(name, body) {
445
+ return `${name}\n${JSON.stringify(body ?? {})}`;
446
+ }
447
+ /**
448
+ * Run `run()` unless an identical invocation is already in flight, in which
449
+ * case share its promise. The entry is removed once the promise settles — so
450
+ * this dedups concurrency only, it does NOT memoize the result.
451
+ */
452
+ function sharedFunctionInvoke(key, run) {
453
+ const existing = inflight.get(key);
454
+ if (existing) return existing;
455
+ const p = run().finally(() => {
456
+ inflight.delete(key);
457
+ });
458
+ inflight.set(key, p);
459
+ return p;
460
+ }
461
+
462
+ //#endregion
463
+ //#region src/customer-app/function-sse.ts
464
+ /**
465
+ * Read a `text/event-stream` function response to completion. Resolves with the
466
+ * decoded result + captured logs, or rejects (with `.logs` attached) on an
467
+ * `event: error` frame / a stream that ends without a terminal event.
468
+ */
469
+ async function readFunctionSseStream(resp) {
470
+ const reader = resp.body?.getReader();
471
+ if (!reader) throw new Error("function response has no body stream");
472
+ const decoder = new TextDecoder();
473
+ let buffer = "";
474
+ let dataPayload = "";
475
+ const logs = [];
476
+ const handleFrame = (frame) => {
477
+ let event = "message";
478
+ let data = "";
479
+ for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
480
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
481
+ if (event === "log") try {
482
+ const l = JSON.parse(data);
483
+ logs.push({
484
+ level: String(l.level ?? "info"),
485
+ message: String(l.message ?? "")
486
+ });
487
+ } catch {}
488
+ else if (event === "data") dataPayload = data;
489
+ else if (event === "done") return {
490
+ done: true,
491
+ value: dataPayload ? JSON.parse(dataPayload) : null
492
+ };
493
+ else if (event === "error") {
494
+ const payload = data ? JSON.parse(data) : {};
495
+ const err = new Error(payload.message || payload.error || "function invocation failed");
496
+ err.name = payload.error || "FunctionError";
497
+ err.logs = logs;
498
+ throw err;
499
+ }
500
+ };
501
+ for (;;) {
502
+ const { done, value } = await reader.read();
503
+ if (done) break;
504
+ buffer += decoder.decode(value, { stream: true });
505
+ let sep;
506
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
507
+ const frame = buffer.slice(0, sep);
508
+ buffer = buffer.slice(sep + 2);
509
+ const result = handleFrame(frame);
510
+ if (result) return {
511
+ value: result.value,
512
+ logs
513
+ };
514
+ }
515
+ }
516
+ throw new Error("function stream ended without a terminal event");
517
+ }
518
+
294
519
  //#endregion
295
520
  //#region src/customer-app/interpolate.ts
296
521
  /**
@@ -396,26 +621,55 @@ function OxyAppProvider(props) {
396
621
  }, [manifestOptions, fetcher]);
397
622
  if (state.status === "error" && state.error) {
398
623
  const err = state.error;
399
- return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, errorFallback ? errorFallback(err) : defaultErrorFallback(err));
624
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
625
+ value: state,
626
+ children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
627
+ });
400
628
  }
401
- if (state.status === "loading") return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, fallback ?? null);
402
- return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, children);
629
+ if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
630
+ value: state,
631
+ children: fallback ?? null
632
+ });
633
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
634
+ value: state,
635
+ children
636
+ });
403
637
  }
404
638
  function defaultErrorFallback(err) {
405
- return /* @__PURE__ */ react.createElement("div", { style: {
406
- margin: "2rem auto",
407
- maxWidth: "640px",
408
- padding: "1rem",
409
- border: "1px solid #fca5a5",
410
- background: "#fee2e2",
411
- color: "#991b1b",
412
- borderRadius: "8px",
413
- fontFamily: "system-ui, -apple-system, sans-serif",
414
- fontSize: "14px"
415
- } }, /* @__PURE__ */ react.createElement("div", { style: { fontWeight: 600 } }, err.title), /* @__PURE__ */ react.createElement("pre", { style: {
416
- fontSize: "12px",
417
- marginTop: "4px"
418
- } }, err.message), /* @__PURE__ */ react.createElement("div", { style: { marginTop: "12px" } }, /* @__PURE__ */ react.createElement("strong", null, "What to try:"), " ", err.hint));
639
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
640
+ style: {
641
+ margin: "2rem auto",
642
+ maxWidth: "640px",
643
+ padding: "1rem",
644
+ border: "1px solid #fca5a5",
645
+ background: "#fee2e2",
646
+ color: "#991b1b",
647
+ borderRadius: "8px",
648
+ fontFamily: "system-ui, -apple-system, sans-serif",
649
+ fontSize: "14px"
650
+ },
651
+ children: [
652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
653
+ style: { fontWeight: 600 },
654
+ children: err.title
655
+ }),
656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
657
+ style: {
658
+ fontSize: "12px",
659
+ marginTop: "4px"
660
+ },
661
+ children: err.message
662
+ }),
663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
664
+ style: { marginTop: "12px" },
665
+ children: [
666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "What to try:" }),
667
+ " ",
668
+ err.hint
669
+ ]
670
+ })
671
+ ]
672
+ });
419
673
  }
420
674
  /**
421
675
  * Emit a one-time `console.warn` the first time a beta hook is
@@ -592,6 +846,81 @@ function useQuery(input, opts = {}) {
592
846
  };
593
847
  }
594
848
  /**
849
+ * Imperative hook for invoking an Oxy Function by name.
850
+ *
851
+ * ```tsx
852
+ * const refresh = useFunction("refresh-sales");
853
+ * <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
854
+ * Refresh
855
+ * </button>
856
+ * ```
857
+ */
858
+ function useFunction(name) {
859
+ const ctx = react.useContext(OxyAppContext);
860
+ if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
861
+ const fetcher = ctx.fetcher;
862
+ const resolved = ctx.resolved;
863
+ const [state, setState] = react.useState({
864
+ data: null,
865
+ isLoading: false,
866
+ error: null,
867
+ logs: []
868
+ });
869
+ return {
870
+ invoke: react.useCallback(async (body, opts) => {
871
+ if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
872
+ const { orgSlug, appSlug, apiBaseUrl } = resolved;
873
+ const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
874
+ setState((s) => ({
875
+ ...s,
876
+ isLoading: true,
877
+ error: null
878
+ }));
879
+ try {
880
+ const headers = {
881
+ "content-type": "application/json",
882
+ accept: "text/event-stream"
883
+ };
884
+ if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
885
+ const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
886
+ const resp = await fetcher(url, {
887
+ method: "POST",
888
+ headers,
889
+ body: JSON.stringify(body ?? {})
890
+ });
891
+ if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
892
+ return readFunctionSseStream(resp);
893
+ });
894
+ setState({
895
+ data: result.value,
896
+ isLoading: false,
897
+ error: null,
898
+ logs: result.logs
899
+ });
900
+ return result.value;
901
+ } catch (err) {
902
+ const e = err instanceof Error ? err : new Error(String(err));
903
+ const logs = e.logs ?? [];
904
+ setState((s) => ({
905
+ ...s,
906
+ isLoading: false,
907
+ error: e,
908
+ logs
909
+ }));
910
+ throw e;
911
+ }
912
+ }, [
913
+ resolved,
914
+ fetcher,
915
+ name
916
+ ]),
917
+ data: state.data,
918
+ isLoading: state.isLoading,
919
+ error: state.error,
920
+ logs: state.logs
921
+ };
922
+ }
923
+ /**
595
924
  * Run a semantic-layer query against the project's `.view.yml` /
596
925
  * `.topic.yml` definitions. The server compiles to SQL and executes
597
926
  * through the same connector path as `useQuery`, so result shape
@@ -1037,7 +1366,7 @@ function parseSseData(raw) {
1037
1366
  * uniformly. New types added upstream don't surface as artifacts
1038
1367
  * until added here — that's intentional, the renderer needs to
1039
1368
  * know how to display each. */
1040
- const SQL_EVENT_TYPES = new Set([
1369
+ const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
1041
1370
  "query_executed",
1042
1371
  "query_generated",
1043
1372
  "verified_sql",
@@ -1166,6 +1495,82 @@ function sleep(ms, signal) {
1166
1495
  });
1167
1496
  }
1168
1497
  /**
1498
+ * Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
1499
+ * `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
1500
+ * (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
1501
+ * grouped by name, with drill-down into recent occurrences.
1502
+ *
1503
+ * The handler returned by [`useTrackEvent`] is **fire-and-forget**:
1504
+ * it enqueues the event into an in-memory batch flushed every second
1505
+ * (and on `pagehide` so a navigation away doesn't drop the tail).
1506
+ * No await semantics — call it inline from a click handler without
1507
+ * awaiting it. Server-side validation errors are logged to the
1508
+ * console; the call site doesn't need to handle them.
1509
+ *
1510
+ * Example:
1511
+ * ```tsx
1512
+ * const track = useTrackEvent();
1513
+ * <button
1514
+ * onClick={() => {
1515
+ * track("export-clicked", { format: "csv", rowCount });
1516
+ * doExport();
1517
+ * }}
1518
+ * >Export</button>
1519
+ * ```
1520
+ *
1521
+ * Rate-limited at 60/min per (user, app) on the server. A burst that
1522
+ * trips the limit drops the excess events with a console warning;
1523
+ * within-limit events are unaffected.
1524
+ */
1525
+ function useTrackEvent() {
1526
+ const { projectId, fetcher } = useOxyApp();
1527
+ const queueRef = react.useRef([]);
1528
+ const flushTimerRef = react.useRef(null);
1529
+ const flush = react.useCallback(() => {
1530
+ flushTimerRef.current = null;
1531
+ if (!projectId) return;
1532
+ const batch = queueRef.current;
1533
+ if (batch.length === 0) return;
1534
+ queueRef.current = [];
1535
+ for (const evt of batch) {
1536
+ const url = `/api/customer-apps/${projectId}/events`;
1537
+ const body = JSON.stringify(evt);
1538
+ try {
1539
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1540
+ else fetcher(url, {
1541
+ method: "POST",
1542
+ headers: { "content-type": "application/json" },
1543
+ body
1544
+ }).catch((e) => {
1545
+ console.warn("[oxy] useTrackEvent flush failed:", e);
1546
+ });
1547
+ } catch (e) {
1548
+ console.warn("[oxy] useTrackEvent enqueue failed:", e);
1549
+ }
1550
+ }
1551
+ }, [projectId, fetcher]);
1552
+ react.useEffect(() => {
1553
+ if (typeof window === "undefined") return;
1554
+ const onHide = () => flush();
1555
+ window.addEventListener("pagehide", onHide);
1556
+ return () => {
1557
+ window.removeEventListener("pagehide", onHide);
1558
+ flush();
1559
+ if (flushTimerRef.current !== null) {
1560
+ clearTimeout(flushTimerRef.current);
1561
+ flushTimerRef.current = null;
1562
+ }
1563
+ };
1564
+ }, [flush]);
1565
+ return react.useCallback((name, payload) => {
1566
+ queueRef.current.push({
1567
+ event_name: name,
1568
+ payload: payload ?? {}
1569
+ });
1570
+ if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
1571
+ }, [flush]);
1572
+ }
1573
+ /**
1169
1574
  * Renders an agent run's answer + artifacts + thread link as a
1170
1575
  * single block. The default styling is intentionally neutral
1171
1576
  * (system fonts, gray surfaces) so it blends into any bundle.
@@ -1187,25 +1592,55 @@ function OxyAnswer(props) {
1187
1592
  const isRunning = state === "running";
1188
1593
  const isFailed = state === "failed";
1189
1594
  const needsClarification = state === "needs_clarification";
1190
- return /* @__PURE__ */ react.createElement("div", {
1595
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1191
1596
  className,
1192
- style: styles.answerWrap
1193
- }, isRunning && answer === null ? /* @__PURE__ */ react.createElement("div", { style: styles.statusRow }, /* @__PURE__ */ react.createElement("span", {
1194
- style: styles.spinner,
1195
- "aria-hidden": "true"
1196
- }), /* @__PURE__ */ react.createElement("span", { style: styles.statusText }, "Thinking…")) : null, artifacts.length > 0 ? /* @__PURE__ */ react.createElement("div", { style: styles.artifactList }, artifacts.map((a) => /* @__PURE__ */ react.createElement(SqlArtifactBlock, {
1197
- key: a.id,
1198
- artifact: a,
1199
- maxRows: maxArtifactRows
1200
- }))) : null, answer ? /* @__PURE__ */ react.createElement("div", { style: styles.markdown }, /* @__PURE__ */ react.createElement(MarkdownText, { text: answer })) : null, needsClarification && clarification ? /* @__PURE__ */ react.createElement("div", { style: styles.clarification }, /* @__PURE__ */ react.createElement("strong", null, "Agent needs clarification:"), /* @__PURE__ */ react.createElement("div", { style: { marginTop: 4 } }, clarification)) : null, isFailed && error ? /* @__PURE__ */ react.createElement(ErrorBlock, { error }) : null, threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ react.createElement("div", { style: styles.threadLinkRow }, /* @__PURE__ */ react.createElement("a", {
1201
- href: threadUrl,
1202
- target: "_blank",
1203
- rel: "noreferrer noopener",
1204
- style: styles.threadLink
1205
- }, threadLinkLabel, " →"), /* @__PURE__ */ react.createElement("span", {
1206
- style: styles.betaBadge,
1207
- title: "Thread linking is in beta — see docs"
1208
- }, "beta")) : null);
1597
+ style: styles.answerWrap,
1598
+ children: [
1599
+ isRunning && answer === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1600
+ style: styles.statusRow,
1601
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1602
+ style: styles.spinner,
1603
+ "aria-hidden": "true"
1604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1605
+ style: styles.statusText,
1606
+ children: "Thinking…"
1607
+ })]
1608
+ }) : null,
1609
+ artifacts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1610
+ style: styles.artifactList,
1611
+ children: artifacts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SqlArtifactBlock, {
1612
+ artifact: a,
1613
+ maxRows: maxArtifactRows
1614
+ }, a.id))
1615
+ }) : null,
1616
+ answer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1617
+ style: styles.markdown,
1618
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownText, { text: answer })
1619
+ }) : null,
1620
+ needsClarification && clarification ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1621
+ style: styles.clarification,
1622
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1623
+ style: { marginTop: 4 },
1624
+ children: clarification
1625
+ })]
1626
+ }) : null,
1627
+ isFailed && error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBlock, { error }) : null,
1628
+ threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1629
+ style: styles.threadLinkRow,
1630
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
1631
+ href: threadUrl,
1632
+ target: "_blank",
1633
+ rel: "noreferrer noopener",
1634
+ style: styles.threadLink,
1635
+ children: [threadLinkLabel, " →"]
1636
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
+ style: styles.betaBadge,
1638
+ title: "Thread linking is in beta — see docs",
1639
+ children: "beta"
1640
+ })]
1641
+ }) : null
1642
+ ]
1643
+ });
1209
1644
  }
1210
1645
  /**
1211
1646
  * Complete drop-in chat surface. One agent, one input, one answer
@@ -1229,37 +1664,48 @@ function OxyChat(props) {
1229
1664
  if (!q || run.state === "running") return;
1230
1665
  run.ask(q);
1231
1666
  }, [question, run]);
1232
- return /* @__PURE__ */ react.createElement("div", {
1667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
1668
  className,
1234
- style: styles.chatWrap
1235
- }, /* @__PURE__ */ react.createElement("form", {
1236
- onSubmit: submit,
1237
- style: styles.chatForm
1238
- }, /* @__PURE__ */ react.createElement("input", {
1239
- type: "text",
1240
- value: question,
1241
- onChange: (e) => setQuestion(e.target.value),
1242
- placeholder,
1243
- disabled: run.state === "running",
1244
- style: styles.chatInput,
1245
- "aria-label": "Question"
1246
- }), /* @__PURE__ */ react.createElement("button", {
1247
- type: "submit",
1248
- disabled: run.state === "running" || question.trim() === "",
1249
- style: styles.chatSubmit
1250
- }, run.state === "running" ? "…" : submitLabel), run.state === "running" ? /* @__PURE__ */ react.createElement("button", {
1251
- type: "button",
1252
- onClick: run.cancel,
1253
- style: styles.chatCancel
1254
- }, "Stop") : null), run.state === "idle" ? emptyState ?? /* @__PURE__ */ react.createElement("div", { style: styles.emptyState }, "Ask a question to get started.") : /* @__PURE__ */ react.createElement(OxyAnswer, {
1255
- answer: run.answer,
1256
- artifacts: run.artifacts,
1257
- state: run.state,
1258
- clarification: run.clarification,
1259
- error: run.error,
1260
- threadUrl: run.threadUrl,
1261
- maxArtifactRows
1262
- }));
1669
+ style: styles.chatWrap,
1670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1671
+ onSubmit: submit,
1672
+ style: styles.chatForm,
1673
+ children: [
1674
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1675
+ type: "text",
1676
+ value: question,
1677
+ onChange: (e) => setQuestion(e.target.value),
1678
+ placeholder,
1679
+ disabled: run.state === "running",
1680
+ style: styles.chatInput,
1681
+ "aria-label": "Question"
1682
+ }),
1683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1684
+ type: "submit",
1685
+ disabled: run.state === "running" || question.trim() === "",
1686
+ style: styles.chatSubmit,
1687
+ children: run.state === "running" ? "…" : submitLabel
1688
+ }),
1689
+ run.state === "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1690
+ type: "button",
1691
+ onClick: run.cancel,
1692
+ style: styles.chatCancel,
1693
+ children: "Stop"
1694
+ }) : null
1695
+ ]
1696
+ }), run.state === "idle" ? emptyState ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1697
+ style: styles.emptyState,
1698
+ children: "Ask a question to get started."
1699
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAnswer, {
1700
+ answer: run.answer,
1701
+ artifacts: run.artifacts,
1702
+ state: run.state,
1703
+ clarification: run.clarification,
1704
+ error: run.error,
1705
+ threadUrl: run.threadUrl,
1706
+ maxArtifactRows
1707
+ })]
1708
+ });
1263
1709
  }
1264
1710
  function SqlArtifactBlock(props) {
1265
1711
  const { artifact, maxRows } = props;
@@ -1268,17 +1714,53 @@ function SqlArtifactBlock(props) {
1268
1714
  const truncated = results ? results.rows.length > maxRows : false;
1269
1715
  const visibleRows = results ? results.rows.slice(0, maxRows) : [];
1270
1716
  const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
1271
- return /* @__PURE__ */ react.createElement("div", { style: styles.artifact }, /* @__PURE__ */ react.createElement("button", {
1272
- type: "button",
1273
- onClick: () => setOpen((o) => !o),
1274
- style: styles.artifactHeader
1275
- }, /* @__PURE__ */ react.createElement("span", { style: styles.artifactBadge }, sourceLabel), /* @__PURE__ */ react.createElement("span", { style: styles.artifactSummary }, results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"), /* @__PURE__ */ react.createElement("span", { style: styles.artifactToggle }, open ? "Hide" : "Show")), open ? /* @__PURE__ */ react.createElement("div", null, /* @__PURE__ */ react.createElement("pre", { style: styles.sqlBlock }, artifact.sql), artifact.error ? /* @__PURE__ */ react.createElement("div", { style: styles.error }, artifact.error) : results ? /* @__PURE__ */ react.createElement("div", { style: styles.resultsWrap }, /* @__PURE__ */ react.createElement("table", { style: styles.resultsTable }, /* @__PURE__ */ react.createElement("thead", null, /* @__PURE__ */ react.createElement("tr", null, results.columns.map((c) => /* @__PURE__ */ react.createElement("th", {
1276
- key: c,
1277
- style: styles.resultsTh
1278
- }, c)))), /* @__PURE__ */ react.createElement("tbody", null, visibleRows.map((row, i) => /* @__PURE__ */ react.createElement("tr", { key: i }, row.map((cell, j) => /* @__PURE__ */ react.createElement("td", {
1279
- key: j,
1280
- style: styles.resultsTd
1281
- }, formatCell(cell))))))), truncated ? /* @__PURE__ */ react.createElement("div", { style: styles.truncatedNote }, "+", results.rows.length - maxRows, " more rows. Open the thread in Oxy to see all.") : null) : null) : null);
1717
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1718
+ style: styles.artifact,
1719
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1720
+ type: "button",
1721
+ onClick: () => setOpen((o) => !o),
1722
+ style: styles.artifactHeader,
1723
+ children: [
1724
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1725
+ style: styles.artifactBadge,
1726
+ children: sourceLabel
1727
+ }),
1728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1729
+ style: styles.artifactSummary,
1730
+ children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
1731
+ }),
1732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1733
+ style: styles.artifactToggle,
1734
+ children: open ? "Hide" : "Show"
1735
+ })
1736
+ ]
1737
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1738
+ style: styles.sqlBlock,
1739
+ children: artifact.sql
1740
+ }), artifact.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1741
+ style: styles.error,
1742
+ children: artifact.error
1743
+ }) : results ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1744
+ style: styles.resultsWrap,
1745
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1746
+ style: styles.resultsTable,
1747
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: results.columns.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1748
+ style: styles.resultsTh,
1749
+ children: c
1750
+ }, c)) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: row.map((cell, j) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1751
+ style: styles.resultsTd,
1752
+ children: formatCell(cell)
1753
+ }, j)) }, i)) })]
1754
+ }), truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1755
+ style: styles.truncatedNote,
1756
+ children: [
1757
+ "+",
1758
+ results.rows.length - maxRows,
1759
+ " more rows. Open the thread in Oxy to see all."
1760
+ ]
1761
+ }) : null]
1762
+ }) : null] }) : null]
1763
+ });
1282
1764
  }
1283
1765
  function formatCell(value) {
1284
1766
  if (value === null || value === void 0) return "—";
@@ -1293,16 +1775,36 @@ function formatCell(value) {
1293
1775
  */
1294
1776
  function ErrorBlock(props) {
1295
1777
  const { error } = props;
1296
- if (error instanceof OxyApiError) return /* @__PURE__ */ react.createElement("div", { style: styles.error }, /* @__PURE__ */ react.createElement("div", null, /* @__PURE__ */ react.createElement("strong", null, "Run failed:"), " ", error.message.split("\n\n")[0]), error.hint ? /* @__PURE__ */ react.createElement("div", { style: {
1297
- marginTop: 6,
1298
- fontWeight: 400,
1299
- whiteSpace: "pre-wrap"
1300
- } }, /* @__PURE__ */ react.createElement("strong", null, "Hint:"), " ", error.hint) : null);
1301
- return /* @__PURE__ */ react.createElement("div", { style: styles.error }, /* @__PURE__ */ react.createElement("strong", null, "Run failed:"), " ", error.message);
1778
+ if (error instanceof OxyApiError) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1779
+ style: styles.error,
1780
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
1782
+ " ",
1783
+ error.message.split("\n\n")[0]
1784
+ ] }), error.hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1785
+ style: {
1786
+ marginTop: 6,
1787
+ fontWeight: 400,
1788
+ whiteSpace: "pre-wrap"
1789
+ },
1790
+ children: [
1791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Hint:" }),
1792
+ " ",
1793
+ error.hint
1794
+ ]
1795
+ }) : null]
1796
+ });
1797
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1798
+ style: styles.error,
1799
+ children: [
1800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
1801
+ " ",
1802
+ error.message
1803
+ ]
1804
+ });
1302
1805
  }
1303
1806
  function MarkdownText(props) {
1304
- const blocks = react.useMemo(() => parseMarkdown(props.text), [props.text]);
1305
- return /* @__PURE__ */ react.createElement(react.Fragment, null, blocks);
1807
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: react.useMemo(() => parseMarkdown(props.text), [props.text]) });
1306
1808
  }
1307
1809
  function parseMarkdown(text) {
1308
1810
  const lines = text.replace(/\r\n/g, "\n").split("\n");
@@ -1372,27 +1874,23 @@ function parseMarkdown(text) {
1372
1874
  }
1373
1875
  return blocks.map((b, idx) => {
1374
1876
  switch (b.kind) {
1375
- case "h": {
1376
- const Tag = `h${b.level}`;
1377
- const headingStyle = b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3;
1378
- return /* @__PURE__ */ react.createElement(Tag, {
1379
- key: idx,
1380
- style: headingStyle
1381
- }, renderInline(b.text));
1382
- }
1383
- case "code": return /* @__PURE__ */ react.createElement("pre", {
1384
- key: idx,
1877
+ case "h": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(`h${b.level}`, {
1878
+ style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
1879
+ children: renderInline(b.text)
1880
+ }, idx);
1881
+ case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1385
1882
  style: styles.codeBlock,
1386
- "data-lang": b.lang || void 0
1387
- }, /* @__PURE__ */ react.createElement("code", null, b.code));
1388
- case "list": return /* @__PURE__ */ react.createElement("ul", {
1389
- key: idx,
1390
- style: styles.list
1391
- }, b.items.map((item, i) => /* @__PURE__ */ react.createElement("li", { key: i }, renderInline(item))));
1392
- case "p": return /* @__PURE__ */ react.createElement("p", {
1393
- key: idx,
1394
- style: styles.paragraph
1395
- }, renderInline(b.text));
1883
+ "data-lang": b.lang || void 0,
1884
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: b.code })
1885
+ }, idx);
1886
+ case "list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1887
+ style: styles.list,
1888
+ children: b.items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: renderInline(item) }, i))
1889
+ }, idx);
1890
+ case "p": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1891
+ style: styles.paragraph,
1892
+ children: renderInline(b.text)
1893
+ }, idx);
1396
1894
  }
1397
1895
  });
1398
1896
  }
@@ -1408,27 +1906,31 @@ function renderInline(text) {
1408
1906
  const patterns = [
1409
1907
  {
1410
1908
  re: /`([^`]+)`/,
1411
- render: (m) => /* @__PURE__ */ react.createElement("code", { style: styles.inlineCode }, m[1])
1909
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1910
+ style: styles.inlineCode,
1911
+ children: m[1]
1912
+ })
1412
1913
  },
1413
1914
  {
1414
1915
  re: /\[([^\]]+)\]\(([^)]+)\)/,
1415
1916
  render: (m) => {
1416
- if (isSafeLinkHref(m[2])) return /* @__PURE__ */ react.createElement("a", {
1917
+ if (isSafeLinkHref(m[2])) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1417
1918
  href: m[2],
1418
1919
  target: "_blank",
1419
1920
  rel: "noreferrer noopener",
1420
- style: styles.link
1421
- }, m[1]);
1422
- return /* @__PURE__ */ react.createElement(react.Fragment, null, m[1]);
1921
+ style: styles.link,
1922
+ children: m[1]
1923
+ });
1924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: m[1] });
1423
1925
  }
1424
1926
  },
1425
1927
  {
1426
1928
  re: /\*\*([^*]+)\*\*/,
1427
- render: (m) => /* @__PURE__ */ react.createElement("strong", null, m[1])
1929
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: m[1] })
1428
1930
  },
1429
1931
  {
1430
1932
  re: /\*([^*]+)\*/,
1431
- render: (m) => /* @__PURE__ */ react.createElement("em", null, m[1])
1933
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: m[1] })
1432
1934
  }
1433
1935
  ];
1434
1936
  while (remaining.length > 0) {
@@ -1446,7 +1948,7 @@ function renderInline(text) {
1446
1948
  break;
1447
1949
  }
1448
1950
  if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
1449
- segments.push(/* @__PURE__ */ react.createElement(react.Fragment, { key: key++ }, earliest.node));
1951
+ segments.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: earliest.node }, key++));
1450
1952
  remaining = remaining.slice(earliest.idx + earliest.len);
1451
1953
  }
1452
1954
  return segments;
@@ -1676,6 +2178,136 @@ const styles = {
1676
2178
  };
1677
2179
 
1678
2180
  //#endregion
2181
+ //#region src/metricTree.ts
2182
+ /**
2183
+ * Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
2184
+ * airlayer metric-tree analyses (tree introspection, sensitivity, predict,
2185
+ * explain, opportunity) over typed methods.
2186
+ *
2187
+ * Construction is internal to {@link OxyClient} — call `client.metricTree`
2188
+ * to access an instance rather than building one yourself.
2189
+ *
2190
+ * @example
2191
+ * ```typescript
2192
+ * const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
2193
+ * const tree = await client.metricTree.getTree();
2194
+ * const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
2195
+ * ```
2196
+ */
2197
+ var MetricTreeClient = class {
2198
+ constructor(config, request) {
2199
+ this.config = config;
2200
+ this.request = request;
2201
+ }
2202
+ path(suffix) {
2203
+ return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
2204
+ }
2205
+ buildQuery(extra = {}) {
2206
+ const params = { ...extra };
2207
+ if (this.config.branch) params.branch = this.config.branch;
2208
+ const qs = new URLSearchParams(params).toString();
2209
+ return qs ? `?${qs}` : "";
2210
+ }
2211
+ /**
2212
+ * Fetch the full metric tree, or the subtree rooted at `root`.
2213
+ *
2214
+ * @param root - Optional fully-qualified measure id to root the tree at.
2215
+ * @returns Nodes (measures) and edges (component / driver relationships).
2216
+ *
2217
+ * @example
2218
+ * ```typescript
2219
+ * const tree = await client.metricTree.getTree();
2220
+ * const subtree = await client.metricTree.getTree("orders.net_revenue");
2221
+ * ```
2222
+ */
2223
+ async getTree(root) {
2224
+ const query = this.buildQuery(root ? { root } : {});
2225
+ return this.request(this.path(query));
2226
+ }
2227
+ /**
2228
+ * Rank the declared drivers of a measure by influence.
2229
+ *
2230
+ * @param measureId - Fully-qualified measure id (`view.measure`).
2231
+ *
2232
+ * @example
2233
+ * ```typescript
2234
+ * const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
2235
+ * for (const driver of sensitivity.drivers) {
2236
+ * console.log(driver.measure, driver.direction, driver.strength);
2237
+ * }
2238
+ * ```
2239
+ */
2240
+ async getSensitivity(measureId) {
2241
+ const query = this.buildQuery();
2242
+ return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
2243
+ }
2244
+ /**
2245
+ * Propagate hypothetical `(measure, delta)` changes upward through the
2246
+ * tree. Returns the estimated impact on every downstream measure.
2247
+ *
2248
+ * @example
2249
+ * ```typescript
2250
+ * const result = await client.metricTree.predict([
2251
+ * { measure: "marketing_spend.total_spend", delta: 10000 },
2252
+ * ]);
2253
+ * ```
2254
+ */
2255
+ async predict(changes) {
2256
+ const query = this.buildQuery();
2257
+ return this.request(this.path(`/predict${query}`), {
2258
+ method: "POST",
2259
+ body: JSON.stringify({ changes })
2260
+ });
2261
+ }
2262
+ /**
2263
+ * Period-over-period root-cause decomposition. Recursively splits the
2264
+ * target measure by components and dimensions until the move concentrates.
2265
+ *
2266
+ * @example
2267
+ * ```typescript
2268
+ * const result = await client.metricTree.explain({
2269
+ * target: "financials.operating_profit",
2270
+ * time_dimension: "financials.month",
2271
+ * current_period: ["2025-09-01", "2025-09-30"],
2272
+ * previous_period: ["2025-08-01", "2025-08-31"],
2273
+ * });
2274
+ * ```
2275
+ */
2276
+ async explain(request) {
2277
+ const query = this.buildQuery();
2278
+ return this.request(this.path(`/explain${query}`), {
2279
+ method: "POST",
2280
+ body: JSON.stringify(request)
2281
+ });
2282
+ }
2283
+ /**
2284
+ * Size the upside opportunity for a measure by finding underperforming
2285
+ * segments. Skips high-cardinality dimensions and trims the long tail.
2286
+ *
2287
+ * @example
2288
+ * ```typescript
2289
+ * const result = await client.metricTree.findOpportunities({
2290
+ * target: "orders.net_revenue",
2291
+ * time_dimension: "orders.order_date",
2292
+ * period: ["2025-09-01", "2025-09-30"],
2293
+ * });
2294
+ * for (const dim of result.dimensions) {
2295
+ * console.log(dim.dimension, "+", dim.total_upside);
2296
+ * }
2297
+ * ```
2298
+ */
2299
+ async findOpportunities(request) {
2300
+ const query = this.buildQuery();
2301
+ return this.request(this.path(`/opportunity${query}`), {
2302
+ method: "POST",
2303
+ body: JSON.stringify(request)
2304
+ });
2305
+ }
2306
+ };
2307
+
2308
+ //#endregion
2309
+ exports.AnomaliesClient = AnomaliesClient;
2310
+ exports.MetricTreeClient = MetricTreeClient;
1679
2311
  exports.OxyAnswer = OxyAnswer;
1680
2312
  exports.OxyApiError = OxyApiError;
1681
2313
  exports.OxyAppProvider = OxyAppProvider;
@@ -1688,8 +2320,10 @@ exports.loadCustomerAppManifest = loadCustomerAppManifest;
1688
2320
  exports.readInjectedAppConfig = readInjectedAppConfig;
1689
2321
  exports.setOxyAppLogger = setOxyAppLogger;
1690
2322
  exports.useAgentRun = useAgentRun;
2323
+ exports.useFunction = useFunction;
1691
2324
  exports.useProcedureRun = useProcedureRun;
1692
2325
  exports.useQuery = useQuery;
1693
2326
  exports.useResolvedManifest = useResolvedManifest;
1694
2327
  exports.useSemanticQuery = useSemanticQuery;
2328
+ exports.useTrackEvent = useTrackEvent;
1695
2329
  //# sourceMappingURL=index.cjs.map