@oxy-hq/sdk 2.0.1 → 2.3.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. */
@@ -65,6 +147,52 @@ async function getCustomerAppDebug(resolved) {
65
147
 
66
148
  //#endregion
67
149
  //#region src/customer-app/errors.ts
150
+ /**
151
+ * Error thrown by all customer-app hooks when an API call returns a
152
+ * non-2xx response. Carries the structured `code` + `hint` the server
153
+ * emits so bundle UIs can render an actionable message instead of
154
+ * "404: { ...json... }".
155
+ */
156
+ var OxyApiError = class extends Error {
157
+ constructor(opts) {
158
+ const base = opts.message || `HTTP ${opts.status}`;
159
+ const code = opts.code ? ` [${opts.code}]` : "";
160
+ const hint = opts.hint ? `\n\n${opts.hint}` : "";
161
+ super(`${base}${code}${hint}`);
162
+ this.name = "OxyApiError";
163
+ this.status = opts.status;
164
+ this.code = opts.code ?? null;
165
+ this.hint = opts.hint ?? null;
166
+ }
167
+ };
168
+ /**
169
+ * Read a non-2xx response from oxy and return an `OxyApiError`.
170
+ * Parses the JSON envelope when present; falls back to raw text
171
+ * (truncated to 240 chars so a runaway HTML error page doesn't
172
+ * dominate the bundle UI).
173
+ */
174
+ async function apiErrorFromResponse(resp) {
175
+ let body;
176
+ let raw = "";
177
+ try {
178
+ raw = await resp.text();
179
+ body = raw ? JSON.parse(raw) : void 0;
180
+ } catch {}
181
+ if (body && typeof body === "object") {
182
+ const b = body;
183
+ return new OxyApiError({
184
+ status: resp.status,
185
+ message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
186
+ code: typeof b.code === "string" ? b.code : null,
187
+ hint: typeof b.hint === "string" ? b.hint : null
188
+ });
189
+ }
190
+ const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
191
+ return new OxyApiError({
192
+ status: resp.status,
193
+ message: snippet || `HTTP ${resp.status}`
194
+ });
195
+ }
68
196
  const ARCH_DOC = "internal-docs/customer-apps.md";
69
197
  /** Interpret a thrown error as a structured report for UI display. */
70
198
  function interpretCustomerAppError(err) {
@@ -255,13 +383,174 @@ function validateManifest(raw, url) {
255
383
  name: typeof raw.name === "string" ? raw.name : void 0,
256
384
  slug: raw.slug,
257
385
  orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
258
- projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
386
+ projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
387
+ functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
259
388
  };
260
389
  }
390
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
391
+ /**
392
+ * Validate the optional `functions` map. Each key is a function name;
393
+ * each value declares how the function is invoked. Mirrors the
394
+ * server-side validation in `customer_apps_publish.rs` so a bad
395
+ * manifest fails at build, not at publish.
396
+ */
397
+ function validateFunctions(raw) {
398
+ if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
399
+ const out = {};
400
+ for (const [fnName, value] of Object.entries(raw)) {
401
+ if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
402
+ if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
403
+ const fn = {};
404
+ if (value.entry !== void 0) {
405
+ if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
406
+ fn.entry = value.entry;
407
+ }
408
+ if (value.schedule !== void 0) {
409
+ if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
410
+ fn.schedule = value.schedule;
411
+ }
412
+ if (value.timezone !== void 0) {
413
+ if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
414
+ fn.timezone = value.timezone;
415
+ }
416
+ if (value.route !== void 0) {
417
+ if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
418
+ fn.route = value.route;
419
+ }
420
+ if (value.airwayStep !== void 0) {
421
+ const step = value.airwayStep;
422
+ if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
423
+ fn.airwayStep = {
424
+ pipeline: step.pipeline,
425
+ resource: step.resource
426
+ };
427
+ }
428
+ if (value.timeoutSeconds !== void 0) {
429
+ const t = value.timeoutSeconds;
430
+ 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]`);
431
+ fn.timeoutSeconds = t;
432
+ }
433
+ if (value.cache !== void 0) {
434
+ const c = value.cache;
435
+ if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
436
+ if (c.ttlSeconds !== void 0) {
437
+ const ttl = c.ttlSeconds;
438
+ if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
439
+ fn.cache = { ttlSeconds: ttl };
440
+ }
441
+ }
442
+ if (value.retries !== void 0) {
443
+ const r = value.retries;
444
+ if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
445
+ const retries = {};
446
+ for (const key of [
447
+ "maxAttempts",
448
+ "minTimeoutMs",
449
+ "maxTimeoutMs"
450
+ ]) {
451
+ const n = r[key];
452
+ if (n !== void 0) {
453
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
454
+ retries[key] = n;
455
+ }
456
+ }
457
+ fn.retries = retries;
458
+ }
459
+ if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
460
+ const hasSchedule = fn.schedule !== void 0;
461
+ const hasAirway = fn.airwayStep !== void 0;
462
+ if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
463
+ out[fnName] = fn;
464
+ }
465
+ return out;
466
+ }
261
467
  function isRecord(v) {
262
468
  return typeof v === "object" && v !== null && !Array.isArray(v);
263
469
  }
264
470
 
471
+ //#endregion
472
+ //#region src/customer-app/function-invoke.ts
473
+ const inflight$1 = /* @__PURE__ */ new Map();
474
+ /**
475
+ * Dedup key for an invocation: function name + its (stable-serialized) body,
476
+ * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
477
+ * the separator can never collide with a name.
478
+ */
479
+ function functionInvokeKey(name, body) {
480
+ return `${name}\n${JSON.stringify(body ?? {})}`;
481
+ }
482
+ /**
483
+ * Run `run()` unless an identical invocation is already in flight, in which
484
+ * case share its promise. The entry is removed once the promise settles — so
485
+ * this dedups concurrency only, it does NOT memoize the result.
486
+ */
487
+ function sharedFunctionInvoke(key, run) {
488
+ const existing = inflight$1.get(key);
489
+ if (existing) return existing;
490
+ const p = run().finally(() => {
491
+ inflight$1.delete(key);
492
+ });
493
+ inflight$1.set(key, p);
494
+ return p;
495
+ }
496
+
497
+ //#endregion
498
+ //#region src/customer-app/function-sse.ts
499
+ /**
500
+ * Read a `text/event-stream` function response to completion. Resolves with the
501
+ * decoded result + captured logs, or rejects (with `.logs` attached) on an
502
+ * `event: error` frame / a stream that ends without a terminal event.
503
+ */
504
+ async function readFunctionSseStream(resp) {
505
+ const reader = resp.body?.getReader();
506
+ if (!reader) throw new Error("function response has no body stream");
507
+ const decoder = new TextDecoder();
508
+ let buffer = "";
509
+ let dataPayload = "";
510
+ const logs = [];
511
+ const handleFrame = (frame) => {
512
+ let event = "message";
513
+ let data = "";
514
+ for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
515
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
516
+ if (event === "log") try {
517
+ const l = JSON.parse(data);
518
+ logs.push({
519
+ level: String(l.level ?? "info"),
520
+ message: String(l.message ?? "")
521
+ });
522
+ } catch {}
523
+ else if (event === "data") dataPayload = data;
524
+ else if (event === "done") return {
525
+ done: true,
526
+ value: dataPayload ? JSON.parse(dataPayload) : null
527
+ };
528
+ else if (event === "error") {
529
+ const payload = data ? JSON.parse(data) : {};
530
+ const err = new Error(payload.message || payload.error || "function invocation failed");
531
+ err.name = payload.error || "FunctionError";
532
+ err.logs = logs;
533
+ throw err;
534
+ }
535
+ };
536
+ for (;;) {
537
+ const { done, value } = await reader.read();
538
+ if (done) break;
539
+ buffer += decoder.decode(value, { stream: true });
540
+ let sep;
541
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
542
+ const frame = buffer.slice(0, sep);
543
+ buffer = buffer.slice(sep + 2);
544
+ const result = handleFrame(frame);
545
+ if (result) return {
546
+ value: result.value,
547
+ logs
548
+ };
549
+ }
550
+ }
551
+ throw new Error("function stream ended without a terminal event");
552
+ }
553
+
265
554
  //#endregion
266
555
  //#region src/customer-app/interpolate.ts
267
556
  /**
@@ -326,6 +615,50 @@ function isSafeLinkHref(raw) {
326
615
  return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
327
616
  }
328
617
 
618
+ //#endregion
619
+ //#region src/customer-app/query-cache.ts
620
+ const SWR_TTL_MS = 3e4;
621
+ const inflight = /* @__PURE__ */ new Map();
622
+ const cache = /* @__PURE__ */ new Map();
623
+ function queryKey(projectId, db, sql) {
624
+ return `${projectId} ${db ?? ""} ${sql}`;
625
+ }
626
+ function getCached(projectId, sql, db) {
627
+ const e = cache.get(queryKey(projectId, db, sql));
628
+ return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
629
+ }
630
+ /** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
631
+ * short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
632
+ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
633
+ const key = queryKey(projectId, db, sql);
634
+ if (!opts.force) {
635
+ const fresh = getCached(projectId, sql, db);
636
+ if (fresh) return fresh;
637
+ }
638
+ const existing = inflight.get(key);
639
+ if (existing) return existing;
640
+ const body = JSON.stringify({
641
+ sql,
642
+ ...db ? { database: db } : {}
643
+ });
644
+ const p = (async () => {
645
+ const resp = await fetcher(`/api/projects/${projectId}/query`, {
646
+ method: "POST",
647
+ headers: { "content-type": "application/json" },
648
+ body
649
+ });
650
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
651
+ const data = await resp.json();
652
+ cache.set(key, {
653
+ at: Date.now(),
654
+ data
655
+ });
656
+ return data;
657
+ })().finally(() => inflight.delete(key));
658
+ inflight.set(key, p);
659
+ return p;
660
+ }
661
+
329
662
  //#endregion
330
663
  //#region src/customer-app/react.tsx
331
664
  function defaultFetcher(input, init) {
@@ -367,26 +700,55 @@ function OxyAppProvider(props) {
367
700
  }, [manifestOptions, fetcher]);
368
701
  if (state.status === "error" && state.error) {
369
702
  const err = state.error;
370
- return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, errorFallback ? errorFallback(err) : defaultErrorFallback(err));
703
+ return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
704
+ value: state,
705
+ children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
706
+ });
371
707
  }
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);
708
+ if (state.status === "loading") return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
709
+ value: state,
710
+ children: fallback ?? null
711
+ });
712
+ return /* @__PURE__ */ jsx(OxyAppContext.Provider, {
713
+ value: state,
714
+ children
715
+ });
374
716
  }
375
717
  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));
718
+ return /* @__PURE__ */ jsxs("div", {
719
+ style: {
720
+ margin: "2rem auto",
721
+ maxWidth: "640px",
722
+ padding: "1rem",
723
+ border: "1px solid #fca5a5",
724
+ background: "#fee2e2",
725
+ color: "#991b1b",
726
+ borderRadius: "8px",
727
+ fontFamily: "system-ui, -apple-system, sans-serif",
728
+ fontSize: "14px"
729
+ },
730
+ children: [
731
+ /* @__PURE__ */ jsx("div", {
732
+ style: { fontWeight: 600 },
733
+ children: err.title
734
+ }),
735
+ /* @__PURE__ */ jsx("pre", {
736
+ style: {
737
+ fontSize: "12px",
738
+ marginTop: "4px"
739
+ },
740
+ children: err.message
741
+ }),
742
+ /* @__PURE__ */ jsxs("div", {
743
+ style: { marginTop: "12px" },
744
+ children: [
745
+ /* @__PURE__ */ jsx("strong", { children: "What to try:" }),
746
+ " ",
747
+ err.hint
748
+ ]
749
+ })
750
+ ]
751
+ });
390
752
  }
391
753
  /**
392
754
  * Emit a one-time `console.warn` the first time a beta hook is
@@ -401,58 +763,6 @@ function warnBetaOnce(name) {
401
763
  if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
402
764
  }
403
765
  /**
404
- * Error thrown by all customer-app hooks when an API call returns a
405
- * non-2xx response. Carries the structured `code` + `hint` the server
406
- * emits so bundle UIs can render an actionable message instead of
407
- * "404: { ...json... }".
408
- *
409
- * The server contract is documented in
410
- * `crates/app/src/server/api/projects/agent_ask.rs` and
411
- * `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
412
- * Hooks that previously wrapped the raw text in `new Error()` now
413
- * throw this type instead.
414
- */
415
- var OxyApiError = class extends Error {
416
- constructor(opts) {
417
- const base = opts.message || `HTTP ${opts.status}`;
418
- const code = opts.code ? ` [${opts.code}]` : "";
419
- const hint = opts.hint ? `\n\n${opts.hint}` : "";
420
- super(`${base}${code}${hint}`);
421
- this.name = "OxyApiError";
422
- this.status = opts.status;
423
- this.code = opts.code ?? null;
424
- this.hint = opts.hint ?? null;
425
- }
426
- };
427
- /**
428
- * Read a non-2xx response from oxy and return an `OxyApiError`.
429
- * Parses the JSON envelope when present; falls back to raw text
430
- * (truncated to 240 chars so a runaway HTML error page doesn't
431
- * dominate the bundle UI).
432
- */
433
- async function apiErrorFromResponse(resp) {
434
- let body;
435
- let raw = "";
436
- try {
437
- raw = await resp.text();
438
- body = raw ? JSON.parse(raw) : void 0;
439
- } catch {}
440
- if (body && typeof body === "object") {
441
- const b = body;
442
- return new OxyApiError({
443
- status: resp.status,
444
- message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
445
- code: typeof b.code === "string" ? b.code : null,
446
- hint: typeof b.hint === "string" ? b.hint : null
447
- });
448
- }
449
- const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
450
- return new OxyApiError({
451
- status: resp.status,
452
- message: snippet || `HTTP ${resp.status}`
453
- });
454
- }
455
- /**
456
766
  * Read the resolved manifest from context. Throws if called outside
457
767
  * `<OxyAppProvider>` — that's a programmer error worth surfacing
458
768
  * loudly, not silently swallowing.
@@ -506,36 +816,35 @@ function useQuery(input, opts = {}) {
506
816
  } : s);
507
817
  return;
508
818
  }
509
- const ctrl = new AbortController();
510
819
  let cancelled = false;
820
+ const cached = getCached(projectId, sqlWithParams, input.database);
821
+ if (cached && nonce === 0) {
822
+ const { columns, rows } = cached;
823
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
824
+ setState({
825
+ rows: objects,
826
+ columns,
827
+ loading: false,
828
+ error: null
829
+ });
830
+ return;
831
+ }
511
832
  setState((s) => ({
512
833
  ...s,
513
834
  loading: true,
514
835
  error: null
515
836
  }));
516
- const body = JSON.stringify({
517
- sql: sqlWithParams,
518
- ...input.database ? { database: input.database } : {}
519
- });
520
- fetcher(`/api/projects/${projectId}/query`, {
521
- method: "POST",
522
- headers: { "content-type": "application/json" },
523
- body,
524
- signal: ctrl.signal
525
- }).then(async (resp) => {
526
- if (!resp.ok) throw await apiErrorFromResponse(resp);
527
- return resp.json();
528
- }).then(({ columns, rows }) => {
837
+ sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
529
838
  if (cancelled) return;
839
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
530
840
  setState({
531
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
841
+ rows: objects,
532
842
  columns,
533
843
  loading: false,
534
844
  error: null
535
845
  });
536
846
  }).catch((err) => {
537
847
  if (cancelled) return;
538
- if (err instanceof DOMException && err.name === "AbortError") return;
539
848
  setState((s) => ({
540
849
  ...s,
541
850
  loading: false,
@@ -544,7 +853,6 @@ function useQuery(input, opts = {}) {
544
853
  });
545
854
  return () => {
546
855
  cancelled = true;
547
- ctrl.abort();
548
856
  };
549
857
  }, [
550
858
  enabled,
@@ -563,6 +871,81 @@ function useQuery(input, opts = {}) {
563
871
  };
564
872
  }
565
873
  /**
874
+ * Imperative hook for invoking an Oxy Function by name.
875
+ *
876
+ * ```tsx
877
+ * const refresh = useFunction("refresh-sales");
878
+ * <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
879
+ * Refresh
880
+ * </button>
881
+ * ```
882
+ */
883
+ function useFunction(name) {
884
+ const ctx = React.useContext(OxyAppContext);
885
+ if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
886
+ const fetcher = ctx.fetcher;
887
+ const resolved = ctx.resolved;
888
+ const [state, setState] = React.useState({
889
+ data: null,
890
+ isLoading: false,
891
+ error: null,
892
+ logs: []
893
+ });
894
+ return {
895
+ invoke: React.useCallback(async (body, opts) => {
896
+ if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
897
+ const { orgSlug, appSlug, apiBaseUrl } = resolved;
898
+ const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
899
+ setState((s) => ({
900
+ ...s,
901
+ isLoading: true,
902
+ error: null
903
+ }));
904
+ try {
905
+ const headers = {
906
+ "content-type": "application/json",
907
+ accept: "text/event-stream"
908
+ };
909
+ if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
910
+ const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
911
+ const resp = await fetcher(url, {
912
+ method: "POST",
913
+ headers,
914
+ body: JSON.stringify(body ?? {})
915
+ });
916
+ if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
917
+ return readFunctionSseStream(resp);
918
+ });
919
+ setState({
920
+ data: result.value,
921
+ isLoading: false,
922
+ error: null,
923
+ logs: result.logs
924
+ });
925
+ return result.value;
926
+ } catch (err) {
927
+ const e = err instanceof Error ? err : new Error(String(err));
928
+ const logs = e.logs ?? [];
929
+ setState((s) => ({
930
+ ...s,
931
+ isLoading: false,
932
+ error: e,
933
+ logs
934
+ }));
935
+ throw e;
936
+ }
937
+ }, [
938
+ resolved,
939
+ fetcher,
940
+ name
941
+ ]),
942
+ data: state.data,
943
+ isLoading: state.isLoading,
944
+ error: state.error,
945
+ logs: state.logs
946
+ };
947
+ }
948
+ /**
566
949
  * Run a semantic-layer query against the project's `.view.yml` /
567
950
  * `.topic.yml` definitions. The server compiles to SQL and executes
568
951
  * through the same connector path as `useQuery`, so result shape
@@ -610,7 +993,8 @@ function useSemanticQuery(input, opts = {}) {
610
993
  filters: input.filters ?? [],
611
994
  ...input.limit != null ? { limit: input.limit } : {}
612
995
  });
613
- fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
996
+ const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
997
+ fetcher(url, {
614
998
  method: "POST",
615
999
  headers: { "content-type": "application/json" },
616
1000
  body,
@@ -620,8 +1004,9 @@ function useSemanticQuery(input, opts = {}) {
620
1004
  return resp.json();
621
1005
  }).then(({ columns, rows, truncated, sql }) => {
622
1006
  if (cancelled) return;
1007
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
623
1008
  setState({
624
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
1009
+ rows: objects,
625
1010
  columns,
626
1011
  truncated,
627
1012
  sql: sql ?? null,
@@ -1008,7 +1393,7 @@ function parseSseData(raw) {
1008
1393
  * uniformly. New types added upstream don't surface as artifacts
1009
1394
  * until added here — that's intentional, the renderer needs to
1010
1395
  * know how to display each. */
1011
- const SQL_EVENT_TYPES = new Set([
1396
+ const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
1012
1397
  "query_executed",
1013
1398
  "query_generated",
1014
1399
  "verified_sql",
@@ -1137,6 +1522,82 @@ function sleep(ms, signal) {
1137
1522
  });
1138
1523
  }
1139
1524
  /**
1525
+ * Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
1526
+ * `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
1527
+ * (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
1528
+ * grouped by name, with drill-down into recent occurrences.
1529
+ *
1530
+ * The handler returned by [`useTrackEvent`] is **fire-and-forget**:
1531
+ * it enqueues the event into an in-memory batch flushed every second
1532
+ * (and on `pagehide` so a navigation away doesn't drop the tail).
1533
+ * No await semantics — call it inline from a click handler without
1534
+ * awaiting it. Server-side validation errors are logged to the
1535
+ * console; the call site doesn't need to handle them.
1536
+ *
1537
+ * Example:
1538
+ * ```tsx
1539
+ * const track = useTrackEvent();
1540
+ * <button
1541
+ * onClick={() => {
1542
+ * track("export-clicked", { format: "csv", rowCount });
1543
+ * doExport();
1544
+ * }}
1545
+ * >Export</button>
1546
+ * ```
1547
+ *
1548
+ * Rate-limited at 60/min per (user, app) on the server. A burst that
1549
+ * trips the limit drops the excess events with a console warning;
1550
+ * within-limit events are unaffected.
1551
+ */
1552
+ function useTrackEvent() {
1553
+ const { projectId, fetcher } = useOxyApp();
1554
+ const queueRef = React.useRef([]);
1555
+ const flushTimerRef = React.useRef(null);
1556
+ const flush = React.useCallback(() => {
1557
+ flushTimerRef.current = null;
1558
+ if (!projectId) return;
1559
+ const batch = queueRef.current;
1560
+ if (batch.length === 0) return;
1561
+ queueRef.current = [];
1562
+ for (const evt of batch) {
1563
+ const url = `/api/customer-apps/${projectId}/events`;
1564
+ const body = JSON.stringify(evt);
1565
+ try {
1566
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1567
+ else fetcher(url, {
1568
+ method: "POST",
1569
+ headers: { "content-type": "application/json" },
1570
+ body
1571
+ }).catch((e) => {
1572
+ console.warn("[oxy] useTrackEvent flush failed:", e);
1573
+ });
1574
+ } catch (e) {
1575
+ console.warn("[oxy] useTrackEvent enqueue failed:", e);
1576
+ }
1577
+ }
1578
+ }, [projectId, fetcher]);
1579
+ React.useEffect(() => {
1580
+ if (typeof window === "undefined") return;
1581
+ const onHide = () => flush();
1582
+ window.addEventListener("pagehide", onHide);
1583
+ return () => {
1584
+ window.removeEventListener("pagehide", onHide);
1585
+ flush();
1586
+ if (flushTimerRef.current !== null) {
1587
+ clearTimeout(flushTimerRef.current);
1588
+ flushTimerRef.current = null;
1589
+ }
1590
+ };
1591
+ }, [flush]);
1592
+ return React.useCallback((name, payload) => {
1593
+ queueRef.current.push({
1594
+ event_name: name,
1595
+ payload: payload ?? {}
1596
+ });
1597
+ if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
1598
+ }, [flush]);
1599
+ }
1600
+ /**
1140
1601
  * Renders an agent run's answer + artifacts + thread link as a
1141
1602
  * single block. The default styling is intentionally neutral
1142
1603
  * (system fonts, gray surfaces) so it blends into any bundle.
@@ -1158,25 +1619,55 @@ function OxyAnswer(props) {
1158
1619
  const isRunning = state === "running";
1159
1620
  const isFailed = state === "failed";
1160
1621
  const needsClarification = state === "needs_clarification";
1161
- return /* @__PURE__ */ React.createElement("div", {
1622
+ return /* @__PURE__ */ jsxs("div", {
1162
1623
  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);
1624
+ style: styles.answerWrap,
1625
+ children: [
1626
+ isRunning && answer === null ? /* @__PURE__ */ jsxs("div", {
1627
+ style: styles.statusRow,
1628
+ children: [/* @__PURE__ */ jsx("span", {
1629
+ style: styles.spinner,
1630
+ "aria-hidden": "true"
1631
+ }), /* @__PURE__ */ jsx("span", {
1632
+ style: styles.statusText,
1633
+ children: "Thinking…"
1634
+ })]
1635
+ }) : null,
1636
+ artifacts.length > 0 ? /* @__PURE__ */ jsx("div", {
1637
+ style: styles.artifactList,
1638
+ children: artifacts.map((a) => /* @__PURE__ */ jsx(SqlArtifactBlock, {
1639
+ artifact: a,
1640
+ maxRows: maxArtifactRows
1641
+ }, a.id))
1642
+ }) : null,
1643
+ answer ? /* @__PURE__ */ jsx("div", {
1644
+ style: styles.markdown,
1645
+ children: /* @__PURE__ */ jsx(MarkdownText, { text: answer })
1646
+ }) : null,
1647
+ needsClarification && clarification ? /* @__PURE__ */ jsxs("div", {
1648
+ style: styles.clarification,
1649
+ children: [/* @__PURE__ */ jsx("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ jsx("div", {
1650
+ style: { marginTop: 4 },
1651
+ children: clarification
1652
+ })]
1653
+ }) : null,
1654
+ isFailed && error ? /* @__PURE__ */ jsx(ErrorBlock, { error }) : null,
1655
+ threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ jsxs("div", {
1656
+ style: styles.threadLinkRow,
1657
+ children: [/* @__PURE__ */ jsxs("a", {
1658
+ href: threadUrl,
1659
+ target: "_blank",
1660
+ rel: "noreferrer noopener",
1661
+ style: styles.threadLink,
1662
+ children: [threadLinkLabel, " →"]
1663
+ }), /* @__PURE__ */ jsx("span", {
1664
+ style: styles.betaBadge,
1665
+ title: "Thread linking is in beta — see docs",
1666
+ children: "beta"
1667
+ })]
1668
+ }) : null
1669
+ ]
1670
+ });
1180
1671
  }
1181
1672
  /**
1182
1673
  * Complete drop-in chat surface. One agent, one input, one answer
@@ -1200,37 +1691,48 @@ function OxyChat(props) {
1200
1691
  if (!q || run.state === "running") return;
1201
1692
  run.ask(q);
1202
1693
  }, [question, run]);
1203
- return /* @__PURE__ */ React.createElement("div", {
1694
+ return /* @__PURE__ */ jsxs("div", {
1204
1695
  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
- }));
1696
+ style: styles.chatWrap,
1697
+ children: [/* @__PURE__ */ jsxs("form", {
1698
+ onSubmit: submit,
1699
+ style: styles.chatForm,
1700
+ children: [
1701
+ /* @__PURE__ */ jsx("input", {
1702
+ type: "text",
1703
+ value: question,
1704
+ onChange: (e) => setQuestion(e.target.value),
1705
+ placeholder,
1706
+ disabled: run.state === "running",
1707
+ style: styles.chatInput,
1708
+ "aria-label": "Question"
1709
+ }),
1710
+ /* @__PURE__ */ jsx("button", {
1711
+ type: "submit",
1712
+ disabled: run.state === "running" || question.trim() === "",
1713
+ style: styles.chatSubmit,
1714
+ children: run.state === "running" ? "…" : submitLabel
1715
+ }),
1716
+ run.state === "running" ? /* @__PURE__ */ jsx("button", {
1717
+ type: "button",
1718
+ onClick: run.cancel,
1719
+ style: styles.chatCancel,
1720
+ children: "Stop"
1721
+ }) : null
1722
+ ]
1723
+ }), run.state === "idle" ? emptyState ?? /* @__PURE__ */ jsx("div", {
1724
+ style: styles.emptyState,
1725
+ children: "Ask a question to get started."
1726
+ }) : /* @__PURE__ */ jsx(OxyAnswer, {
1727
+ answer: run.answer,
1728
+ artifacts: run.artifacts,
1729
+ state: run.state,
1730
+ clarification: run.clarification,
1731
+ error: run.error,
1732
+ threadUrl: run.threadUrl,
1733
+ maxArtifactRows
1734
+ })]
1735
+ });
1234
1736
  }
1235
1737
  function SqlArtifactBlock(props) {
1236
1738
  const { artifact, maxRows } = props;
@@ -1239,17 +1741,53 @@ function SqlArtifactBlock(props) {
1239
1741
  const truncated = results ? results.rows.length > maxRows : false;
1240
1742
  const visibleRows = results ? results.rows.slice(0, maxRows) : [];
1241
1743
  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);
1744
+ return /* @__PURE__ */ jsxs("div", {
1745
+ style: styles.artifact,
1746
+ children: [/* @__PURE__ */ jsxs("button", {
1747
+ type: "button",
1748
+ onClick: () => setOpen((o) => !o),
1749
+ style: styles.artifactHeader,
1750
+ children: [
1751
+ /* @__PURE__ */ jsx("span", {
1752
+ style: styles.artifactBadge,
1753
+ children: sourceLabel
1754
+ }),
1755
+ /* @__PURE__ */ jsx("span", {
1756
+ style: styles.artifactSummary,
1757
+ children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
1758
+ }),
1759
+ /* @__PURE__ */ jsx("span", {
1760
+ style: styles.artifactToggle,
1761
+ children: open ? "Hide" : "Show"
1762
+ })
1763
+ ]
1764
+ }), open ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("pre", {
1765
+ style: styles.sqlBlock,
1766
+ children: artifact.sql
1767
+ }), artifact.error ? /* @__PURE__ */ jsx("div", {
1768
+ style: styles.error,
1769
+ children: artifact.error
1770
+ }) : results ? /* @__PURE__ */ jsxs("div", {
1771
+ style: styles.resultsWrap,
1772
+ children: [/* @__PURE__ */ jsxs("table", {
1773
+ style: styles.resultsTable,
1774
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: results.columns.map((c) => /* @__PURE__ */ jsx("th", {
1775
+ style: styles.resultsTh,
1776
+ children: c
1777
+ }, c)) }) }), /* @__PURE__ */ jsx("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", {
1778
+ style: styles.resultsTd,
1779
+ children: formatCell(cell)
1780
+ }, j)) }, i)) })]
1781
+ }), truncated ? /* @__PURE__ */ jsxs("div", {
1782
+ style: styles.truncatedNote,
1783
+ children: [
1784
+ "+",
1785
+ results.rows.length - maxRows,
1786
+ " more rows. Open the thread in Oxy to see all."
1787
+ ]
1788
+ }) : null]
1789
+ }) : null] }) : null]
1790
+ });
1253
1791
  }
1254
1792
  function formatCell(value) {
1255
1793
  if (value === null || value === void 0) return "—";
@@ -1264,16 +1802,36 @@ function formatCell(value) {
1264
1802
  */
1265
1803
  function ErrorBlock(props) {
1266
1804
  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);
1805
+ if (error instanceof OxyApiError) return /* @__PURE__ */ jsxs("div", {
1806
+ style: styles.error,
1807
+ children: [/* @__PURE__ */ jsxs("div", { children: [
1808
+ /* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
1809
+ " ",
1810
+ error.message.split("\n\n")[0]
1811
+ ] }), error.hint ? /* @__PURE__ */ jsxs("div", {
1812
+ style: {
1813
+ marginTop: 6,
1814
+ fontWeight: 400,
1815
+ whiteSpace: "pre-wrap"
1816
+ },
1817
+ children: [
1818
+ /* @__PURE__ */ jsx("strong", { children: "Hint:" }),
1819
+ " ",
1820
+ error.hint
1821
+ ]
1822
+ }) : null]
1823
+ });
1824
+ return /* @__PURE__ */ jsxs("div", {
1825
+ style: styles.error,
1826
+ children: [
1827
+ /* @__PURE__ */ jsx("strong", { children: "Run failed:" }),
1828
+ " ",
1829
+ error.message
1830
+ ]
1831
+ });
1273
1832
  }
1274
1833
  function MarkdownText(props) {
1275
- const blocks = React.useMemo(() => parseMarkdown(props.text), [props.text]);
1276
- return /* @__PURE__ */ React.createElement(React.Fragment, null, blocks);
1834
+ return /* @__PURE__ */ jsx(Fragment, { children: React.useMemo(() => parseMarkdown(props.text), [props.text]) });
1277
1835
  }
1278
1836
  function parseMarkdown(text) {
1279
1837
  const lines = text.replace(/\r\n/g, "\n").split("\n");
@@ -1343,27 +1901,23 @@ function parseMarkdown(text) {
1343
1901
  }
1344
1902
  return blocks.map((b, idx) => {
1345
1903
  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,
1904
+ case "h": return /* @__PURE__ */ jsx(`h${b.level}`, {
1905
+ style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
1906
+ children: renderInline(b.text)
1907
+ }, idx);
1908
+ case "code": return /* @__PURE__ */ jsx("pre", {
1356
1909
  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));
1910
+ "data-lang": b.lang || void 0,
1911
+ children: /* @__PURE__ */ jsx("code", { children: b.code })
1912
+ }, idx);
1913
+ case "list": return /* @__PURE__ */ jsx("ul", {
1914
+ style: styles.list,
1915
+ children: b.items.map((item, i) => /* @__PURE__ */ jsx("li", { children: renderInline(item) }, i))
1916
+ }, idx);
1917
+ case "p": return /* @__PURE__ */ jsx("p", {
1918
+ style: styles.paragraph,
1919
+ children: renderInline(b.text)
1920
+ }, idx);
1367
1921
  }
1368
1922
  });
1369
1923
  }
@@ -1379,27 +1933,31 @@ function renderInline(text) {
1379
1933
  const patterns = [
1380
1934
  {
1381
1935
  re: /`([^`]+)`/,
1382
- render: (m) => /* @__PURE__ */ React.createElement("code", { style: styles.inlineCode }, m[1])
1936
+ render: (m) => /* @__PURE__ */ jsx("code", {
1937
+ style: styles.inlineCode,
1938
+ children: m[1]
1939
+ })
1383
1940
  },
1384
1941
  {
1385
1942
  re: /\[([^\]]+)\]\(([^)]+)\)/,
1386
1943
  render: (m) => {
1387
- if (isSafeLinkHref(m[2])) return /* @__PURE__ */ React.createElement("a", {
1944
+ if (isSafeLinkHref(m[2])) return /* @__PURE__ */ jsx("a", {
1388
1945
  href: m[2],
1389
1946
  target: "_blank",
1390
1947
  rel: "noreferrer noopener",
1391
- style: styles.link
1392
- }, m[1]);
1393
- return /* @__PURE__ */ React.createElement(React.Fragment, null, m[1]);
1948
+ style: styles.link,
1949
+ children: m[1]
1950
+ });
1951
+ return /* @__PURE__ */ jsx(Fragment, { children: m[1] });
1394
1952
  }
1395
1953
  },
1396
1954
  {
1397
1955
  re: /\*\*([^*]+)\*\*/,
1398
- render: (m) => /* @__PURE__ */ React.createElement("strong", null, m[1])
1956
+ render: (m) => /* @__PURE__ */ jsx("strong", { children: m[1] })
1399
1957
  },
1400
1958
  {
1401
1959
  re: /\*([^*]+)\*/,
1402
- render: (m) => /* @__PURE__ */ React.createElement("em", null, m[1])
1960
+ render: (m) => /* @__PURE__ */ jsx("em", { children: m[1] })
1403
1961
  }
1404
1962
  ];
1405
1963
  while (remaining.length > 0) {
@@ -1417,7 +1975,7 @@ function renderInline(text) {
1417
1975
  break;
1418
1976
  }
1419
1977
  if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
1420
- segments.push(/* @__PURE__ */ React.createElement(React.Fragment, { key: key++ }, earliest.node));
1978
+ segments.push(/* @__PURE__ */ jsx(React.Fragment, { children: earliest.node }, key++));
1421
1979
  remaining = remaining.slice(earliest.idx + earliest.len);
1422
1980
  }
1423
1981
  return segments;
@@ -1647,5 +2205,133 @@ const styles = {
1647
2205
  };
1648
2206
 
1649
2207
  //#endregion
1650
- export { OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery };
2208
+ //#region src/metricTree.ts
2209
+ /**
2210
+ * Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
2211
+ * airlayer metric-tree analyses (tree introspection, sensitivity, predict,
2212
+ * explain, opportunity) over typed methods.
2213
+ *
2214
+ * Construction is internal to {@link OxyClient} — call `client.metricTree`
2215
+ * to access an instance rather than building one yourself.
2216
+ *
2217
+ * @example
2218
+ * ```typescript
2219
+ * const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
2220
+ * const tree = await client.metricTree.getTree();
2221
+ * const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
2222
+ * ```
2223
+ */
2224
+ var MetricTreeClient = class {
2225
+ constructor(config, request) {
2226
+ this.config = config;
2227
+ this.request = request;
2228
+ }
2229
+ path(suffix) {
2230
+ return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
2231
+ }
2232
+ buildQuery(extra = {}) {
2233
+ const params = { ...extra };
2234
+ if (this.config.branch) params.branch = this.config.branch;
2235
+ const qs = new URLSearchParams(params).toString();
2236
+ return qs ? `?${qs}` : "";
2237
+ }
2238
+ /**
2239
+ * Fetch the full metric tree, or the subtree rooted at `root`.
2240
+ *
2241
+ * @param root - Optional fully-qualified measure id to root the tree at.
2242
+ * @returns Nodes (measures) and edges (component / driver relationships).
2243
+ *
2244
+ * @example
2245
+ * ```typescript
2246
+ * const tree = await client.metricTree.getTree();
2247
+ * const subtree = await client.metricTree.getTree("orders.net_revenue");
2248
+ * ```
2249
+ */
2250
+ async getTree(root) {
2251
+ const query = this.buildQuery(root ? { root } : {});
2252
+ return this.request(this.path(query));
2253
+ }
2254
+ /**
2255
+ * Rank the declared drivers of a measure by influence.
2256
+ *
2257
+ * @param measureId - Fully-qualified measure id (`view.measure`).
2258
+ *
2259
+ * @example
2260
+ * ```typescript
2261
+ * const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
2262
+ * for (const driver of sensitivity.drivers) {
2263
+ * console.log(driver.measure, driver.direction, driver.strength);
2264
+ * }
2265
+ * ```
2266
+ */
2267
+ async getSensitivity(measureId) {
2268
+ const query = this.buildQuery();
2269
+ return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
2270
+ }
2271
+ /**
2272
+ * Propagate hypothetical `(measure, delta)` changes upward through the
2273
+ * tree. Returns the estimated impact on every downstream measure.
2274
+ *
2275
+ * @example
2276
+ * ```typescript
2277
+ * const result = await client.metricTree.predict([
2278
+ * { measure: "marketing_spend.total_spend", delta: 10000 },
2279
+ * ]);
2280
+ * ```
2281
+ */
2282
+ async predict(changes) {
2283
+ const query = this.buildQuery();
2284
+ return this.request(this.path(`/predict${query}`), {
2285
+ method: "POST",
2286
+ body: JSON.stringify({ changes })
2287
+ });
2288
+ }
2289
+ /**
2290
+ * Period-over-period root-cause decomposition. Recursively splits the
2291
+ * target measure by components and dimensions until the move concentrates.
2292
+ *
2293
+ * @example
2294
+ * ```typescript
2295
+ * const result = await client.metricTree.explain({
2296
+ * target: "financials.operating_profit",
2297
+ * time_dimension: "financials.month",
2298
+ * current_period: ["2025-09-01", "2025-09-30"],
2299
+ * previous_period: ["2025-08-01", "2025-08-31"],
2300
+ * });
2301
+ * ```
2302
+ */
2303
+ async explain(request) {
2304
+ const query = this.buildQuery();
2305
+ return this.request(this.path(`/explain${query}`), {
2306
+ method: "POST",
2307
+ body: JSON.stringify(request)
2308
+ });
2309
+ }
2310
+ /**
2311
+ * Size the upside opportunity for a measure by finding underperforming
2312
+ * segments. Skips high-cardinality dimensions and trims the long tail.
2313
+ *
2314
+ * @example
2315
+ * ```typescript
2316
+ * const result = await client.metricTree.findOpportunities({
2317
+ * target: "orders.net_revenue",
2318
+ * time_dimension: "orders.order_date",
2319
+ * period: ["2025-09-01", "2025-09-30"],
2320
+ * });
2321
+ * for (const dim of result.dimensions) {
2322
+ * console.log(dim.dimension, "+", dim.total_upside);
2323
+ * }
2324
+ * ```
2325
+ */
2326
+ async findOpportunities(request) {
2327
+ const query = this.buildQuery();
2328
+ return this.request(this.path(`/opportunity${query}`), {
2329
+ method: "POST",
2330
+ body: JSON.stringify(request)
2331
+ });
2332
+ }
2333
+ };
2334
+
2335
+ //#endregion
2336
+ export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, apiErrorFromResponse, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
1651
2337
  //# sourceMappingURL=index.mjs.map