@oxy-hq/sdk 2.0.1 → 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.mjs CHANGED
@@ -1,6 +1,88 @@
1
1
  // @oxy/sdk - TypeScript SDK for Oxy data platform
2
2
  import * as React from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
4
 
5
+ //#region src/anomalies.ts
6
+ /**
7
+ * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
8
+ * rather than instantiating directly — the getter wires the request helper
9
+ * so auth, timeout, and branch propagation come along for free.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
14
+ * for (const a of anomalies) {
15
+ * console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));
16
+ * }
17
+ * ```
18
+ */
19
+ var AnomaliesClient = class {
20
+ constructor(config, request) {
21
+ this.config = config;
22
+ this.request = request;
23
+ }
24
+ path(suffix) {
25
+ return `/${this.config.projectId}/semantic/anomalies${suffix}`;
26
+ }
27
+ buildQuery(extra = {}) {
28
+ const params = { ...extra };
29
+ if (this.config.branch) params.branch = this.config.branch;
30
+ const qs = new URLSearchParams(params).toString();
31
+ return qs ? `?${qs}` : "";
32
+ }
33
+ /**
34
+ * List anomalies in the inbox, newest first.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * // Open / unresolved anomalies only
39
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
40
+ * ```
41
+ */
42
+ async list(options = {}) {
43
+ const extra = {};
44
+ if (options.status) extra.status = options.status;
45
+ if (options.limit) extra.limit = String(options.limit);
46
+ return this.request(this.path(this.buildQuery(extra)));
47
+ }
48
+ /**
49
+ * Trigger a full scan. Iterates every `.monitor.yml` entry in the
50
+ * workspace, runs the detector, and upserts matching rows into the
51
+ * inbox. Returns counts of scanned / failed / persisted.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * // Scan against a known-good reference date (matches the seed dataset)
56
+ * const result = await client.anomalies.scan({ as_of: "2025-12-15" });
57
+ * console.log(`${result.anomalies_persisted} anomalies detected`);
58
+ * ```
59
+ */
60
+ async scan(options = {}) {
61
+ const extra = {};
62
+ if (options.as_of) extra.as_of = options.as_of;
63
+ return this.request(this.path(`/scan${this.buildQuery(extra)}`), { method: "POST" });
64
+ }
65
+ /**
66
+ * Update an anomaly's status (acknowledge / dismiss / re-open).
67
+ */
68
+ async updateStatus(anomalyId, status) {
69
+ const query = this.buildQuery();
70
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {
71
+ method: "POST",
72
+ body: JSON.stringify({ status })
73
+ });
74
+ }
75
+ /**
76
+ * Run the metric-tree `explain` for an anomaly and cache the result on
77
+ * the row. Subsequent calls return the cached `ExplainResult` instantly.
78
+ */
79
+ async explain(anomalyId) {
80
+ const query = this.buildQuery();
81
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
82
+ }
83
+ };
84
+
85
+ //#endregion
4
86
  //#region src/customer-app/logger.ts
5
87
  let activeLogger = createConsoleLogger();
6
88
  /** Replace the global logger. Pass `null` to silence everything. */
@@ -255,13 +337,156 @@ function validateManifest(raw, url) {
255
337
  name: typeof raw.name === "string" ? raw.name : void 0,
256
338
  slug: raw.slug,
257
339
  orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
258
- projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
340
+ projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
341
+ functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
259
342
  };
260
343
  }
344
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
345
+ /**
346
+ * Validate the optional `functions` map. Each key is a function name;
347
+ * each value declares how the function is invoked. Mirrors the
348
+ * server-side validation in `customer_apps_publish.rs` so a bad
349
+ * manifest fails at build, not at publish.
350
+ */
351
+ function validateFunctions(raw) {
352
+ if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
353
+ const out = {};
354
+ for (const [fnName, value] of Object.entries(raw)) {
355
+ if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
356
+ if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
357
+ const fn = {};
358
+ if (value.entry !== void 0) {
359
+ if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
360
+ fn.entry = value.entry;
361
+ }
362
+ if (value.schedule !== void 0) {
363
+ if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
364
+ fn.schedule = value.schedule;
365
+ }
366
+ if (value.timezone !== void 0) {
367
+ if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
368
+ fn.timezone = value.timezone;
369
+ }
370
+ if (value.route !== void 0) {
371
+ if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
372
+ fn.route = value.route;
373
+ }
374
+ if (value.airwayStep !== void 0) {
375
+ const step = value.airwayStep;
376
+ if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
377
+ fn.airwayStep = {
378
+ pipeline: step.pipeline,
379
+ resource: step.resource
380
+ };
381
+ }
382
+ if (value.timeoutSeconds !== void 0) {
383
+ const t = value.timeoutSeconds;
384
+ 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]`);
385
+ fn.timeoutSeconds = t;
386
+ }
387
+ if (value.cache !== void 0) {
388
+ const c = value.cache;
389
+ if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
390
+ if (c.ttlSeconds !== void 0) {
391
+ const ttl = c.ttlSeconds;
392
+ if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
393
+ fn.cache = { ttlSeconds: ttl };
394
+ }
395
+ }
396
+ const hasSchedule = fn.schedule !== void 0;
397
+ const hasAirway = fn.airwayStep !== void 0;
398
+ if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
399
+ out[fnName] = fn;
400
+ }
401
+ return out;
402
+ }
261
403
  function isRecord(v) {
262
404
  return typeof v === "object" && v !== null && !Array.isArray(v);
263
405
  }
264
406
 
407
+ //#endregion
408
+ //#region src/customer-app/function-invoke.ts
409
+ const inflight = /* @__PURE__ */ new Map();
410
+ /**
411
+ * Dedup key for an invocation: function name + its (stable-serialized) body,
412
+ * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
413
+ * the separator can never collide with a name.
414
+ */
415
+ function functionInvokeKey(name, body) {
416
+ return `${name}\n${JSON.stringify(body ?? {})}`;
417
+ }
418
+ /**
419
+ * Run `run()` unless an identical invocation is already in flight, in which
420
+ * case share its promise. The entry is removed once the promise settles — so
421
+ * this dedups concurrency only, it does NOT memoize the result.
422
+ */
423
+ function sharedFunctionInvoke(key, run) {
424
+ const existing = inflight.get(key);
425
+ if (existing) return existing;
426
+ const p = run().finally(() => {
427
+ inflight.delete(key);
428
+ });
429
+ inflight.set(key, p);
430
+ return p;
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/customer-app/function-sse.ts
435
+ /**
436
+ * Read a `text/event-stream` function response to completion. Resolves with the
437
+ * decoded result + captured logs, or rejects (with `.logs` attached) on an
438
+ * `event: error` frame / a stream that ends without a terminal event.
439
+ */
440
+ async function readFunctionSseStream(resp) {
441
+ const reader = resp.body?.getReader();
442
+ if (!reader) throw new Error("function response has no body stream");
443
+ const decoder = new TextDecoder();
444
+ let buffer = "";
445
+ let dataPayload = "";
446
+ const logs = [];
447
+ const handleFrame = (frame) => {
448
+ let event = "message";
449
+ let data = "";
450
+ for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
451
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
452
+ if (event === "log") try {
453
+ const l = JSON.parse(data);
454
+ logs.push({
455
+ level: String(l.level ?? "info"),
456
+ message: String(l.message ?? "")
457
+ });
458
+ } catch {}
459
+ else if (event === "data") dataPayload = data;
460
+ else if (event === "done") return {
461
+ done: true,
462
+ value: dataPayload ? JSON.parse(dataPayload) : null
463
+ };
464
+ else if (event === "error") {
465
+ const payload = data ? JSON.parse(data) : {};
466
+ const err = new Error(payload.message || payload.error || "function invocation failed");
467
+ err.name = payload.error || "FunctionError";
468
+ err.logs = logs;
469
+ throw err;
470
+ }
471
+ };
472
+ for (;;) {
473
+ const { done, value } = await reader.read();
474
+ if (done) break;
475
+ buffer += decoder.decode(value, { stream: true });
476
+ let sep;
477
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
478
+ const frame = buffer.slice(0, sep);
479
+ buffer = buffer.slice(sep + 2);
480
+ const result = handleFrame(frame);
481
+ if (result) return {
482
+ value: result.value,
483
+ logs
484
+ };
485
+ }
486
+ }
487
+ throw new Error("function stream ended without a terminal event");
488
+ }
489
+
265
490
  //#endregion
266
491
  //#region src/customer-app/interpolate.ts
267
492
  /**
@@ -367,26 +592,55 @@ function OxyAppProvider(props) {
367
592
  }, [manifestOptions, fetcher]);
368
593
  if (state.status === "error" && state.error) {
369
594
  const err = state.error;
370
- return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, errorFallback ? errorFallback(err) : defaultErrorFallback(err));
595
+ return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
596
+ value: state,
597
+ children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
598
+ });
371
599
  }
372
- if (state.status === "loading") return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, fallback ?? null);
373
- return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, children);
600
+ if (state.status === "loading") return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
601
+ value: state,
602
+ children: fallback ?? null
603
+ });
604
+ return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
605
+ value: state,
606
+ children
607
+ });
374
608
  }
375
609
  function defaultErrorFallback(err) {
376
- return /* @__PURE__ */ React.createElement("div", { style: {
377
- margin: "2rem auto",
378
- maxWidth: "640px",
379
- padding: "1rem",
380
- border: "1px solid #fca5a5",
381
- background: "#fee2e2",
382
- color: "#991b1b",
383
- borderRadius: "8px",
384
- fontFamily: "system-ui, -apple-system, sans-serif",
385
- fontSize: "14px"
386
- } }, /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 600 } }, err.title), /* @__PURE__ */ React.createElement("pre", { style: {
387
- fontSize: "12px",
388
- marginTop: "4px"
389
- } }, err.message), /* @__PURE__ */ React.createElement("div", { style: { marginTop: "12px" } }, /* @__PURE__ */ React.createElement("strong", null, "What to try:"), " ", err.hint));
610
+ return /* @__PURE__ */ jsxs("div", {
611
+ style: {
612
+ margin: "2rem auto",
613
+ maxWidth: "640px",
614
+ padding: "1rem",
615
+ border: "1px solid #fca5a5",
616
+ background: "#fee2e2",
617
+ color: "#991b1b",
618
+ borderRadius: "8px",
619
+ fontFamily: "system-ui, -apple-system, sans-serif",
620
+ fontSize: "14px"
621
+ },
622
+ children: [
623
+ /* @__PURE__ */ jsx("div", {
624
+ style: { fontWeight: 600 },
625
+ children: err.title
626
+ }),
627
+ /* @__PURE__ */ jsx("pre", {
628
+ style: {
629
+ fontSize: "12px",
630
+ marginTop: "4px"
631
+ },
632
+ children: err.message
633
+ }),
634
+ /* @__PURE__ */ jsxs("div", {
635
+ style: { marginTop: "12px" },
636
+ children: [
637
+ /* @__PURE__ */ jsx("strong", { children: "What to try:" }),
638
+ " ",
639
+ err.hint
640
+ ]
641
+ })
642
+ ]
643
+ });
390
644
  }
391
645
  /**
392
646
  * Emit a one-time `console.warn` the first time a beta hook is
@@ -563,6 +817,81 @@ function useQuery(input, opts = {}) {
563
817
  };
564
818
  }
565
819
  /**
820
+ * Imperative hook for invoking an Oxy Function by name.
821
+ *
822
+ * ```tsx
823
+ * const refresh = useFunction("refresh-sales");
824
+ * <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
825
+ * Refresh
826
+ * </button>
827
+ * ```
828
+ */
829
+ function useFunction(name) {
830
+ const ctx = React.useContext(OxyAppContext);
831
+ if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
832
+ const fetcher = ctx.fetcher;
833
+ const resolved = ctx.resolved;
834
+ const [state, setState] = React.useState({
835
+ data: null,
836
+ isLoading: false,
837
+ error: null,
838
+ logs: []
839
+ });
840
+ return {
841
+ invoke: React.useCallback(async (body, opts) => {
842
+ if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
843
+ const { orgSlug, appSlug, apiBaseUrl } = resolved;
844
+ const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
845
+ setState((s) => ({
846
+ ...s,
847
+ isLoading: true,
848
+ error: null
849
+ }));
850
+ try {
851
+ const headers = {
852
+ "content-type": "application/json",
853
+ accept: "text/event-stream"
854
+ };
855
+ if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
856
+ const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
857
+ const resp = await fetcher(url, {
858
+ method: "POST",
859
+ headers,
860
+ body: JSON.stringify(body ?? {})
861
+ });
862
+ if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
863
+ return readFunctionSseStream(resp);
864
+ });
865
+ setState({
866
+ data: result.value,
867
+ isLoading: false,
868
+ error: null,
869
+ logs: result.logs
870
+ });
871
+ return result.value;
872
+ } catch (err) {
873
+ const e = err instanceof Error ? err : new Error(String(err));
874
+ const logs = e.logs ?? [];
875
+ setState((s) => ({
876
+ ...s,
877
+ isLoading: false,
878
+ error: e,
879
+ logs
880
+ }));
881
+ throw e;
882
+ }
883
+ }, [
884
+ resolved,
885
+ fetcher,
886
+ name
887
+ ]),
888
+ data: state.data,
889
+ isLoading: state.isLoading,
890
+ error: state.error,
891
+ logs: state.logs
892
+ };
893
+ }
894
+ /**
566
895
  * Run a semantic-layer query against the project's `.view.yml` /
567
896
  * `.topic.yml` definitions. The server compiles to SQL and executes
568
897
  * through the same connector path as `useQuery`, so result shape
@@ -1008,7 +1337,7 @@ function parseSseData(raw) {
1008
1337
  * uniformly. New types added upstream don't surface as artifacts
1009
1338
  * until added here — that's intentional, the renderer needs to
1010
1339
  * know how to display each. */
1011
- const SQL_EVENT_TYPES = new Set([
1340
+ const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
1012
1341
  "query_executed",
1013
1342
  "query_generated",
1014
1343
  "verified_sql",
@@ -1137,6 +1466,82 @@ function sleep(ms, signal) {
1137
1466
  });
1138
1467
  }
1139
1468
  /**
1469
+ * Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
1470
+ * `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
1471
+ * (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
1472
+ * grouped by name, with drill-down into recent occurrences.
1473
+ *
1474
+ * The handler returned by [`useTrackEvent`] is **fire-and-forget**:
1475
+ * it enqueues the event into an in-memory batch flushed every second
1476
+ * (and on `pagehide` so a navigation away doesn't drop the tail).
1477
+ * No await semantics — call it inline from a click handler without
1478
+ * awaiting it. Server-side validation errors are logged to the
1479
+ * console; the call site doesn't need to handle them.
1480
+ *
1481
+ * Example:
1482
+ * ```tsx
1483
+ * const track = useTrackEvent();
1484
+ * <button
1485
+ * onClick={() => {
1486
+ * track("export-clicked", { format: "csv", rowCount });
1487
+ * doExport();
1488
+ * }}
1489
+ * >Export</button>
1490
+ * ```
1491
+ *
1492
+ * Rate-limited at 60/min per (user, app) on the server. A burst that
1493
+ * trips the limit drops the excess events with a console warning;
1494
+ * within-limit events are unaffected.
1495
+ */
1496
+ function useTrackEvent() {
1497
+ const { projectId, fetcher } = useOxyApp();
1498
+ const queueRef = React.useRef([]);
1499
+ const flushTimerRef = React.useRef(null);
1500
+ const flush = React.useCallback(() => {
1501
+ flushTimerRef.current = null;
1502
+ if (!projectId) return;
1503
+ const batch = queueRef.current;
1504
+ if (batch.length === 0) return;
1505
+ queueRef.current = [];
1506
+ for (const evt of batch) {
1507
+ const url = `/api/customer-apps/${projectId}/events`;
1508
+ const body = JSON.stringify(evt);
1509
+ try {
1510
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1511
+ else fetcher(url, {
1512
+ method: "POST",
1513
+ headers: { "content-type": "application/json" },
1514
+ body
1515
+ }).catch((e) => {
1516
+ console.warn("[oxy] useTrackEvent flush failed:", e);
1517
+ });
1518
+ } catch (e) {
1519
+ console.warn("[oxy] useTrackEvent enqueue failed:", e);
1520
+ }
1521
+ }
1522
+ }, [projectId, fetcher]);
1523
+ React.useEffect(() => {
1524
+ if (typeof window === "undefined") return;
1525
+ const onHide = () => flush();
1526
+ window.addEventListener("pagehide", onHide);
1527
+ return () => {
1528
+ window.removeEventListener("pagehide", onHide);
1529
+ flush();
1530
+ if (flushTimerRef.current !== null) {
1531
+ clearTimeout(flushTimerRef.current);
1532
+ flushTimerRef.current = null;
1533
+ }
1534
+ };
1535
+ }, [flush]);
1536
+ return React.useCallback((name, payload) => {
1537
+ queueRef.current.push({
1538
+ event_name: name,
1539
+ payload: payload ?? {}
1540
+ });
1541
+ if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
1542
+ }, [flush]);
1543
+ }
1544
+ /**
1140
1545
  * Renders an agent run's answer + artifacts + thread link as a
1141
1546
  * single block. The default styling is intentionally neutral
1142
1547
  * (system fonts, gray surfaces) so it blends into any bundle.
@@ -1158,25 +1563,55 @@ function OxyAnswer(props) {
1158
1563
  const isRunning = state === "running";
1159
1564
  const isFailed = state === "failed";
1160
1565
  const needsClarification = state === "needs_clarification";
1161
- return /* @__PURE__ */ React.createElement("div", {
1566
+ return /* @__PURE__ */ jsxs("div", {
1162
1567
  className,
1163
- style: styles.answerWrap
1164
- }, isRunning && answer === null ? /* @__PURE__ */ React.createElement("div", { style: styles.statusRow }, /* @__PURE__ */ React.createElement("span", {
1165
- style: styles.spinner,
1166
- "aria-hidden": "true"
1167
- }), /* @__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, {
1168
- key: a.id,
1169
- artifact: a,
1170
- maxRows: maxArtifactRows
1171
- }))) : 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", {
1172
- href: threadUrl,
1173
- target: "_blank",
1174
- rel: "noreferrer noopener",
1175
- style: styles.threadLink
1176
- }, threadLinkLabel, " →"), /* @__PURE__ */ React.createElement("span", {
1177
- style: styles.betaBadge,
1178
- title: "Thread linking is in beta — see docs"
1179
- }, "beta")) : null);
1568
+ style: styles.answerWrap,
1569
+ children: [
1570
+ isRunning && answer === null ? /* @__PURE__ */ jsxs("div", {
1571
+ style: styles.statusRow,
1572
+ children: [/* @__PURE__ */ jsx("span", {
1573
+ style: styles.spinner,
1574
+ "aria-hidden": "true"
1575
+ }), /* @__PURE__ */ jsx("span", {
1576
+ style: styles.statusText,
1577
+ children: "Thinking…"
1578
+ })]
1579
+ }) : null,
1580
+ artifacts.length > 0 ? /* @__PURE__ */ jsx("div", {
1581
+ style: styles.artifactList,
1582
+ children: artifacts.map((a) => /* @__PURE__ */ jsx(SqlArtifactBlock, {
1583
+ artifact: a,
1584
+ maxRows: maxArtifactRows
1585
+ }, a.id))
1586
+ }) : null,
1587
+ answer ? /* @__PURE__ */ jsx("div", {
1588
+ style: styles.markdown,
1589
+ children: /* @__PURE__ */ jsx(MarkdownText, { text: answer })
1590
+ }) : null,
1591
+ needsClarification && clarification ? /* @__PURE__ */ jsxs("div", {
1592
+ style: styles.clarification,
1593
+ children: [/* @__PURE__ */ jsx("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ jsx("div", {
1594
+ style: { marginTop: 4 },
1595
+ children: clarification
1596
+ })]
1597
+ }) : null,
1598
+ isFailed && error ? /* @__PURE__ */ jsx(ErrorBlock, { error }) : null,
1599
+ threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ jsxs("div", {
1600
+ style: styles.threadLinkRow,
1601
+ children: [/* @__PURE__ */ jsxs("a", {
1602
+ href: threadUrl,
1603
+ target: "_blank",
1604
+ rel: "noreferrer noopener",
1605
+ style: styles.threadLink,
1606
+ children: [threadLinkLabel, " →"]
1607
+ }), /* @__PURE__ */ jsx("span", {
1608
+ style: styles.betaBadge,
1609
+ title: "Thread linking is in beta — see docs",
1610
+ children: "beta"
1611
+ })]
1612
+ }) : null
1613
+ ]
1614
+ });
1180
1615
  }
1181
1616
  /**
1182
1617
  * Complete drop-in chat surface. One agent, one input, one answer
@@ -1200,37 +1635,48 @@ function OxyChat(props) {
1200
1635
  if (!q || run.state === "running") return;
1201
1636
  run.ask(q);
1202
1637
  }, [question, run]);
1203
- return /* @__PURE__ */ React.createElement("div", {
1638
+ return /* @__PURE__ */ jsxs("div", {
1204
1639
  className,
1205
- style: styles.chatWrap
1206
- }, /* @__PURE__ */ React.createElement("form", {
1207
- onSubmit: submit,
1208
- style: styles.chatForm
1209
- }, /* @__PURE__ */ React.createElement("input", {
1210
- type: "text",
1211
- value: question,
1212
- onChange: (e) => setQuestion(e.target.value),
1213
- placeholder,
1214
- disabled: run.state === "running",
1215
- style: styles.chatInput,
1216
- "aria-label": "Question"
1217
- }), /* @__PURE__ */ React.createElement("button", {
1218
- type: "submit",
1219
- disabled: run.state === "running" || question.trim() === "",
1220
- style: styles.chatSubmit
1221
- }, run.state === "running" ? "…" : submitLabel), run.state === "running" ? /* @__PURE__ */ React.createElement("button", {
1222
- type: "button",
1223
- onClick: run.cancel,
1224
- style: styles.chatCancel
1225
- }, "Stop") : null), run.state === "idle" ? emptyState ?? /* @__PURE__ */ React.createElement("div", { style: styles.emptyState }, "Ask a question to get started.") : /* @__PURE__ */ React.createElement(OxyAnswer, {
1226
- answer: run.answer,
1227
- artifacts: run.artifacts,
1228
- state: run.state,
1229
- clarification: run.clarification,
1230
- error: run.error,
1231
- threadUrl: run.threadUrl,
1232
- maxArtifactRows
1233
- }));
1640
+ style: styles.chatWrap,
1641
+ children: [/* @__PURE__ */ jsxs("form", {
1642
+ onSubmit: submit,
1643
+ style: styles.chatForm,
1644
+ children: [
1645
+ /* @__PURE__ */ jsx("input", {
1646
+ type: "text",
1647
+ value: question,
1648
+ onChange: (e) => setQuestion(e.target.value),
1649
+ placeholder,
1650
+ disabled: run.state === "running",
1651
+ style: styles.chatInput,
1652
+ "aria-label": "Question"
1653
+ }),
1654
+ /* @__PURE__ */ jsx("button", {
1655
+ type: "submit",
1656
+ disabled: run.state === "running" || question.trim() === "",
1657
+ style: styles.chatSubmit,
1658
+ children: run.state === "running" ? "…" : submitLabel
1659
+ }),
1660
+ run.state === "running" ? /* @__PURE__ */ jsx("button", {
1661
+ type: "button",
1662
+ onClick: run.cancel,
1663
+ style: styles.chatCancel,
1664
+ children: "Stop"
1665
+ }) : null
1666
+ ]
1667
+ }), run.state === "idle" ? emptyState ?? /* @__PURE__ */ jsx("div", {
1668
+ style: styles.emptyState,
1669
+ children: "Ask a question to get started."
1670
+ }) : /* @__PURE__ */ jsx(OxyAnswer, {
1671
+ answer: run.answer,
1672
+ artifacts: run.artifacts,
1673
+ state: run.state,
1674
+ clarification: run.clarification,
1675
+ error: run.error,
1676
+ threadUrl: run.threadUrl,
1677
+ maxArtifactRows
1678
+ })]
1679
+ });
1234
1680
  }
1235
1681
  function SqlArtifactBlock(props) {
1236
1682
  const { artifact, maxRows } = props;
@@ -1239,17 +1685,53 @@ function SqlArtifactBlock(props) {
1239
1685
  const truncated = results ? results.rows.length > maxRows : false;
1240
1686
  const visibleRows = results ? results.rows.slice(0, maxRows) : [];
1241
1687
  const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
1242
- return /* @__PURE__ */ React.createElement("div", { style: styles.artifact }, /* @__PURE__ */ React.createElement("button", {
1243
- type: "button",
1244
- onClick: () => setOpen((o) => !o),
1245
- style: styles.artifactHeader
1246
- }, /* @__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", {
1247
- key: c,
1248
- style: styles.resultsTh
1249
- }, c)))), /* @__PURE__ */ React.createElement("tbody", null, visibleRows.map((row, i) => /* @__PURE__ */ React.createElement("tr", { key: i }, row.map((cell, j) => /* @__PURE__ */ React.createElement("td", {
1250
- key: j,
1251
- style: styles.resultsTd
1252
- }, 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);
1688
+ return /* @__PURE__ */ jsxs("div", {
1689
+ style: styles.artifact,
1690
+ children: [/* @__PURE__ */ jsxs("button", {
1691
+ type: "button",
1692
+ onClick: () => setOpen((o) => !o),
1693
+ style: styles.artifactHeader,
1694
+ children: [
1695
+ /* @__PURE__ */ jsx("span", {
1696
+ style: styles.artifactBadge,
1697
+ children: sourceLabel
1698
+ }),
1699
+ /* @__PURE__ */ jsx("span", {
1700
+ style: styles.artifactSummary,
1701
+ children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
1702
+ }),
1703
+ /* @__PURE__ */ jsx("span", {
1704
+ style: styles.artifactToggle,
1705
+ children: open ? "Hide" : "Show"
1706
+ })
1707
+ ]
1708
+ }), open ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("pre", {
1709
+ style: styles.sqlBlock,
1710
+ children: artifact.sql
1711
+ }), artifact.error ? /* @__PURE__ */ jsx("div", {
1712
+ style: styles.error,
1713
+ children: artifact.error
1714
+ }) : results ? /* @__PURE__ */ jsxs("div", {
1715
+ style: styles.resultsWrap,
1716
+ children: [/* @__PURE__ */ jsxs("table", {
1717
+ style: styles.resultsTable,
1718
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: results.columns.map((c) => /* @__PURE__ */ jsx("th", {
1719
+ style: styles.resultsTh,
1720
+ children: c
1721
+ }, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", {
1722
+ style: styles.resultsTd,
1723
+ children: formatCell(cell)
1724
+ }, j)) }, i)) })]
1725
+ }), truncated ? /* @__PURE__ */ jsxs("div", {
1726
+ style: styles.truncatedNote,
1727
+ children: [
1728
+ "+",
1729
+ results.rows.length - maxRows,
1730
+ " more rows. Open the thread in Oxy to see all."
1731
+ ]
1732
+ }) : null]
1733
+ }) : null] }) : null]
1734
+ });
1253
1735
  }
1254
1736
  function formatCell(value) {
1255
1737
  if (value === null || value === void 0) return "—";
@@ -1264,16 +1746,36 @@ function formatCell(value) {
1264
1746
  */
1265
1747
  function ErrorBlock(props) {
1266
1748
  const { error } = props;
1267
- 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: {
1268
- marginTop: 6,
1269
- fontWeight: 400,
1270
- whiteSpace: "pre-wrap"
1271
- } }, /* @__PURE__ */ React.createElement("strong", null, "Hint:"), " ", error.hint) : null);
1272
- return /* @__PURE__ */ React.createElement("div", { style: styles.error }, /* @__PURE__ */ React.createElement("strong", null, "Run failed:"), " ", error.message);
1749
+ if (error instanceof OxyApiError) return /* @__PURE__ */ jsxs("div", {
1750
+ style: styles.error,
1751
+ children: [/* @__PURE__ */ jsxs("div", { children: [
1752
+ /* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
1753
+ " ",
1754
+ error.message.split("\n\n")[0]
1755
+ ] }), error.hint ? /* @__PURE__ */ jsxs("div", {
1756
+ style: {
1757
+ marginTop: 6,
1758
+ fontWeight: 400,
1759
+ whiteSpace: "pre-wrap"
1760
+ },
1761
+ children: [
1762
+ /* @__PURE__ */ jsx("strong", { children: "Hint:" }),
1763
+ " ",
1764
+ error.hint
1765
+ ]
1766
+ }) : null]
1767
+ });
1768
+ return /* @__PURE__ */ jsxs("div", {
1769
+ style: styles.error,
1770
+ children: [
1771
+ /* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
1772
+ " ",
1773
+ error.message
1774
+ ]
1775
+ });
1273
1776
  }
1274
1777
  function MarkdownText(props) {
1275
- const blocks = React.useMemo(() => parseMarkdown(props.text), [props.text]);
1276
- return /* @__PURE__ */ React.createElement(React.Fragment, null, blocks);
1778
+ return /* @__PURE__ */ jsx(Fragment, { children: React.useMemo(() => parseMarkdown(props.text), [props.text]) });
1277
1779
  }
1278
1780
  function parseMarkdown(text) {
1279
1781
  const lines = text.replace(/\r\n/g, "\n").split("\n");
@@ -1343,27 +1845,23 @@ function parseMarkdown(text) {
1343
1845
  }
1344
1846
  return blocks.map((b, idx) => {
1345
1847
  switch (b.kind) {
1346
- case "h": {
1347
- const Tag = `h${b.level}`;
1348
- const headingStyle = b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3;
1349
- return /* @__PURE__ */ React.createElement(Tag, {
1350
- key: idx,
1351
- style: headingStyle
1352
- }, renderInline(b.text));
1353
- }
1354
- case "code": return /* @__PURE__ */ React.createElement("pre", {
1355
- key: idx,
1848
+ case "h": return /* @__PURE__ */ jsx(`h${b.level}`, {
1849
+ style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
1850
+ children: renderInline(b.text)
1851
+ }, idx);
1852
+ case "code": return /* @__PURE__ */ jsx("pre", {
1356
1853
  style: styles.codeBlock,
1357
- "data-lang": b.lang || void 0
1358
- }, /* @__PURE__ */ React.createElement("code", null, b.code));
1359
- case "list": return /* @__PURE__ */ React.createElement("ul", {
1360
- key: idx,
1361
- style: styles.list
1362
- }, b.items.map((item, i) => /* @__PURE__ */ React.createElement("li", { key: i }, renderInline(item))));
1363
- case "p": return /* @__PURE__ */ React.createElement("p", {
1364
- key: idx,
1365
- style: styles.paragraph
1366
- }, renderInline(b.text));
1854
+ "data-lang": b.lang || void 0,
1855
+ children: /* @__PURE__ */ jsx("code", { children: b.code })
1856
+ }, idx);
1857
+ case "list": return /* @__PURE__ */ jsx("ul", {
1858
+ style: styles.list,
1859
+ children: b.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInline(item) }, i))
1860
+ }, idx);
1861
+ case "p": return /* @__PURE__ */ jsx("p", {
1862
+ style: styles.paragraph,
1863
+ children: renderInline(b.text)
1864
+ }, idx);
1367
1865
  }
1368
1866
  });
1369
1867
  }
@@ -1379,27 +1877,31 @@ function renderInline(text) {
1379
1877
  const patterns = [
1380
1878
  {
1381
1879
  re: /`([^`]+)`/,
1382
- render: (m) => /* @__PURE__ */ React.createElement("code", { style: styles.inlineCode }, m[1])
1880
+ render: (m) => /* @__PURE__ */ jsx("code", {
1881
+ style: styles.inlineCode,
1882
+ children: m[1]
1883
+ })
1383
1884
  },
1384
1885
  {
1385
1886
  re: /\[([^\]]+)\]\(([^)]+)\)/,
1386
1887
  render: (m) => {
1387
- if (isSafeLinkHref(m[2])) return /* @__PURE__ */ React.createElement("a", {
1888
+ if (isSafeLinkHref(m[2])) return /* @__PURE__ */ jsx("a", {
1388
1889
  href: m[2],
1389
1890
  target: "_blank",
1390
1891
  rel: "noreferrer noopener",
1391
- style: styles.link
1392
- }, m[1]);
1393
- return /* @__PURE__ */ React.createElement(React.Fragment, null, m[1]);
1892
+ style: styles.link,
1893
+ children: m[1]
1894
+ });
1895
+ return /* @__PURE__ */ jsx(Fragment, { children: m[1] });
1394
1896
  }
1395
1897
  },
1396
1898
  {
1397
1899
  re: /\*\*([^*]+)\*\*/,
1398
- render: (m) => /* @__PURE__ */ React.createElement("strong", null, m[1])
1900
+ render: (m) => /* @__PURE__ */ jsx("strong", { children: m[1] })
1399
1901
  },
1400
1902
  {
1401
1903
  re: /\*([^*]+)\*/,
1402
- render: (m) => /* @__PURE__ */ React.createElement("em", null, m[1])
1904
+ render: (m) => /* @__PURE__ */ jsx("em", { children: m[1] })
1403
1905
  }
1404
1906
  ];
1405
1907
  while (remaining.length > 0) {
@@ -1417,7 +1919,7 @@ function renderInline(text) {
1417
1919
  break;
1418
1920
  }
1419
1921
  if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
1420
- segments.push(/* @__PURE__ */ React.createElement(React.Fragment, { key: key++ }, earliest.node));
1922
+ segments.push(/* @__PURE__ */ jsx(React.Fragment, { children: earliest.node }, key++));
1421
1923
  remaining = remaining.slice(earliest.idx + earliest.len);
1422
1924
  }
1423
1925
  return segments;
@@ -1647,5 +2149,133 @@ const styles = {
1647
2149
  };
1648
2150
 
1649
2151
  //#endregion
1650
- export { OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery };
2152
+ //#region src/metricTree.ts
2153
+ /**
2154
+ * Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
2155
+ * airlayer metric-tree analyses (tree introspection, sensitivity, predict,
2156
+ * explain, opportunity) over typed methods.
2157
+ *
2158
+ * Construction is internal to {@link OxyClient} — call `client.metricTree`
2159
+ * to access an instance rather than building one yourself.
2160
+ *
2161
+ * @example
2162
+ * ```typescript
2163
+ * const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
2164
+ * const tree = await client.metricTree.getTree();
2165
+ * const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
2166
+ * ```
2167
+ */
2168
+ var MetricTreeClient = class {
2169
+ constructor(config, request) {
2170
+ this.config = config;
2171
+ this.request = request;
2172
+ }
2173
+ path(suffix) {
2174
+ return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
2175
+ }
2176
+ buildQuery(extra = {}) {
2177
+ const params = { ...extra };
2178
+ if (this.config.branch) params.branch = this.config.branch;
2179
+ const qs = new URLSearchParams(params).toString();
2180
+ return qs ? `?${qs}` : "";
2181
+ }
2182
+ /**
2183
+ * Fetch the full metric tree, or the subtree rooted at `root`.
2184
+ *
2185
+ * @param root - Optional fully-qualified measure id to root the tree at.
2186
+ * @returns Nodes (measures) and edges (component / driver relationships).
2187
+ *
2188
+ * @example
2189
+ * ```typescript
2190
+ * const tree = await client.metricTree.getTree();
2191
+ * const subtree = await client.metricTree.getTree("orders.net_revenue");
2192
+ * ```
2193
+ */
2194
+ async getTree(root) {
2195
+ const query = this.buildQuery(root ? { root } : {});
2196
+ return this.request(this.path(query));
2197
+ }
2198
+ /**
2199
+ * Rank the declared drivers of a measure by influence.
2200
+ *
2201
+ * @param measureId - Fully-qualified measure id (`view.measure`).
2202
+ *
2203
+ * @example
2204
+ * ```typescript
2205
+ * const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
2206
+ * for (const driver of sensitivity.drivers) {
2207
+ * console.log(driver.measure, driver.direction, driver.strength);
2208
+ * }
2209
+ * ```
2210
+ */
2211
+ async getSensitivity(measureId) {
2212
+ const query = this.buildQuery();
2213
+ return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
2214
+ }
2215
+ /**
2216
+ * Propagate hypothetical `(measure, delta)` changes upward through the
2217
+ * tree. Returns the estimated impact on every downstream measure.
2218
+ *
2219
+ * @example
2220
+ * ```typescript
2221
+ * const result = await client.metricTree.predict([
2222
+ * { measure: "marketing_spend.total_spend", delta: 10000 },
2223
+ * ]);
2224
+ * ```
2225
+ */
2226
+ async predict(changes) {
2227
+ const query = this.buildQuery();
2228
+ return this.request(this.path(`/predict${query}`), {
2229
+ method: "POST",
2230
+ body: JSON.stringify({ changes })
2231
+ });
2232
+ }
2233
+ /**
2234
+ * Period-over-period root-cause decomposition. Recursively splits the
2235
+ * target measure by components and dimensions until the move concentrates.
2236
+ *
2237
+ * @example
2238
+ * ```typescript
2239
+ * const result = await client.metricTree.explain({
2240
+ * target: "financials.operating_profit",
2241
+ * time_dimension: "financials.month",
2242
+ * current_period: ["2025-09-01", "2025-09-30"],
2243
+ * previous_period: ["2025-08-01", "2025-08-31"],
2244
+ * });
2245
+ * ```
2246
+ */
2247
+ async explain(request) {
2248
+ const query = this.buildQuery();
2249
+ return this.request(this.path(`/explain${query}`), {
2250
+ method: "POST",
2251
+ body: JSON.stringify(request)
2252
+ });
2253
+ }
2254
+ /**
2255
+ * Size the upside opportunity for a measure by finding underperforming
2256
+ * segments. Skips high-cardinality dimensions and trims the long tail.
2257
+ *
2258
+ * @example
2259
+ * ```typescript
2260
+ * const result = await client.metricTree.findOpportunities({
2261
+ * target: "orders.net_revenue",
2262
+ * time_dimension: "orders.order_date",
2263
+ * period: ["2025-09-01", "2025-09-30"],
2264
+ * });
2265
+ * for (const dim of result.dimensions) {
2266
+ * console.log(dim.dimension, "+", dim.total_upside);
2267
+ * }
2268
+ * ```
2269
+ */
2270
+ async findOpportunities(request) {
2271
+ const query = this.buildQuery();
2272
+ return this.request(this.path(`/opportunity${query}`), {
2273
+ method: "POST",
2274
+ body: JSON.stringify(request)
2275
+ });
2276
+ }
2277
+ };
2278
+
2279
+ //#endregion
2280
+ export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
1651
2281
  //# sourceMappingURL=index.mjs.map