@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.cjs CHANGED
@@ -28,8 +28,90 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
 
29
29
  //#endregion
30
30
  let react = require("react");
31
- react = __toESM(react);
31
+ react = __toESM(react, 1);
32
+ let react_jsx_runtime = require("react/jsx-runtime");
32
33
 
34
+ //#region src/anomalies.ts
35
+ /**
36
+ * Client for `/semantic/anomalies*`. Construct via `OxyClient.anomalies`
37
+ * rather than instantiating directly — the getter wires the request helper
38
+ * so auth, timeout, and branch propagation come along for free.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
43
+ * for (const a of anomalies) {
44
+ * console.log(a.label ?? a.measure, a.severity, a.z_score.toFixed(2));
45
+ * }
46
+ * ```
47
+ */
48
+ var AnomaliesClient = class {
49
+ constructor(config, request) {
50
+ this.config = config;
51
+ this.request = request;
52
+ }
53
+ path(suffix) {
54
+ return `/${this.config.projectId}/semantic/anomalies${suffix}`;
55
+ }
56
+ buildQuery(extra = {}) {
57
+ const params = { ...extra };
58
+ if (this.config.branch) params.branch = this.config.branch;
59
+ const qs = new URLSearchParams(params).toString();
60
+ return qs ? `?${qs}` : "";
61
+ }
62
+ /**
63
+ * List anomalies in the inbox, newest first.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * // Open / unresolved anomalies only
68
+ * const { anomalies } = await client.anomalies.list({ status: "new" });
69
+ * ```
70
+ */
71
+ async list(options = {}) {
72
+ const extra = {};
73
+ if (options.status) extra.status = options.status;
74
+ if (options.limit) extra.limit = String(options.limit);
75
+ return this.request(this.path(this.buildQuery(extra)));
76
+ }
77
+ /**
78
+ * Trigger a full scan. Iterates every `.monitor.yml` entry in the
79
+ * workspace, runs the detector, and upserts matching rows into the
80
+ * inbox. Returns counts of scanned / failed / persisted.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * // Scan against a known-good reference date (matches the seed dataset)
85
+ * const result = await client.anomalies.scan({ as_of: "2025-12-15" });
86
+ * console.log(`${result.anomalies_persisted} anomalies detected`);
87
+ * ```
88
+ */
89
+ async scan(options = {}) {
90
+ const extra = {};
91
+ if (options.as_of) extra.as_of = options.as_of;
92
+ return this.request(this.path(`/scan${this.buildQuery(extra)}`), { method: "POST" });
93
+ }
94
+ /**
95
+ * Update an anomaly's status (acknowledge / dismiss / re-open).
96
+ */
97
+ async updateStatus(anomalyId, status) {
98
+ const query = this.buildQuery();
99
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/status${query}`), {
100
+ method: "POST",
101
+ body: JSON.stringify({ status })
102
+ });
103
+ }
104
+ /**
105
+ * Run the metric-tree `explain` for an anomaly and cache the result on
106
+ * the row. Subsequent calls return the cached `ExplainResult` instantly.
107
+ */
108
+ async explain(anomalyId) {
109
+ const query = this.buildQuery();
110
+ return this.request(this.path(`/${encodeURIComponent(anomalyId)}/explain${query}`), { method: "POST" });
111
+ }
112
+ };
113
+
114
+ //#endregion
33
115
  //#region src/customer-app/logger.ts
34
116
  let activeLogger = createConsoleLogger();
35
117
  /** Replace the global logger. Pass `null` to silence everything. */
@@ -94,6 +176,52 @@ async function getCustomerAppDebug(resolved) {
94
176
 
95
177
  //#endregion
96
178
  //#region src/customer-app/errors.ts
179
+ /**
180
+ * Error thrown by all customer-app hooks when an API call returns a
181
+ * non-2xx response. Carries the structured `code` + `hint` the server
182
+ * emits so bundle UIs can render an actionable message instead of
183
+ * "404: { ...json... }".
184
+ */
185
+ var OxyApiError = class extends Error {
186
+ constructor(opts) {
187
+ const base = opts.message || `HTTP ${opts.status}`;
188
+ const code = opts.code ? ` [${opts.code}]` : "";
189
+ const hint = opts.hint ? `\n\n${opts.hint}` : "";
190
+ super(`${base}${code}${hint}`);
191
+ this.name = "OxyApiError";
192
+ this.status = opts.status;
193
+ this.code = opts.code ?? null;
194
+ this.hint = opts.hint ?? null;
195
+ }
196
+ };
197
+ /**
198
+ * Read a non-2xx response from oxy and return an `OxyApiError`.
199
+ * Parses the JSON envelope when present; falls back to raw text
200
+ * (truncated to 240 chars so a runaway HTML error page doesn't
201
+ * dominate the bundle UI).
202
+ */
203
+ async function apiErrorFromResponse(resp) {
204
+ let body;
205
+ let raw = "";
206
+ try {
207
+ raw = await resp.text();
208
+ body = raw ? JSON.parse(raw) : void 0;
209
+ } catch {}
210
+ if (body && typeof body === "object") {
211
+ const b = body;
212
+ return new OxyApiError({
213
+ status: resp.status,
214
+ message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
215
+ code: typeof b.code === "string" ? b.code : null,
216
+ hint: typeof b.hint === "string" ? b.hint : null
217
+ });
218
+ }
219
+ const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
220
+ return new OxyApiError({
221
+ status: resp.status,
222
+ message: snippet || `HTTP ${resp.status}`
223
+ });
224
+ }
97
225
  const ARCH_DOC = "internal-docs/customer-apps.md";
98
226
  /** Interpret a thrown error as a structured report for UI display. */
99
227
  function interpretCustomerAppError(err) {
@@ -284,13 +412,174 @@ function validateManifest(raw, url) {
284
412
  name: typeof raw.name === "string" ? raw.name : void 0,
285
413
  slug: raw.slug,
286
414
  orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
287
- projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
415
+ projectId: typeof raw.projectId === "string" ? raw.projectId : void 0,
416
+ functions: raw.functions !== void 0 ? validateFunctions(raw.functions) : void 0
288
417
  };
289
418
  }
419
+ const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
420
+ /**
421
+ * Validate the optional `functions` map. Each key is a function name;
422
+ * each value declares how the function is invoked. Mirrors the
423
+ * server-side validation in `customer_apps_publish.rs` so a bad
424
+ * manifest fails at build, not at publish.
425
+ */
426
+ function validateFunctions(raw) {
427
+ if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
428
+ const out = {};
429
+ for (const [fnName, value] of Object.entries(raw)) {
430
+ if (!FUNCTION_NAME_RE.test(fnName)) throw new Error(`oxy-app.json: function name "${fnName}" must match ^[a-z][a-z0-9-]{0,63}$`);
431
+ if (!isRecord(value)) throw new Error(`oxy-app.json: function "${fnName}" must be an object`);
432
+ const fn = {};
433
+ if (value.entry !== void 0) {
434
+ if (typeof value.entry !== "string" || !value.entry.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`entry\` must be a non-empty string`);
435
+ fn.entry = value.entry;
436
+ }
437
+ if (value.schedule !== void 0) {
438
+ if (typeof value.schedule !== "string" || !value.schedule.trim()) throw new Error(`oxy-app.json: function "${fnName}" \`schedule\` must be a cron string`);
439
+ fn.schedule = value.schedule;
440
+ }
441
+ if (value.timezone !== void 0) {
442
+ if (typeof value.timezone !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`timezone\` must be a string`);
443
+ fn.timezone = value.timezone;
444
+ }
445
+ if (value.route !== void 0) {
446
+ if (typeof value.route !== "boolean") throw new Error(`oxy-app.json: function "${fnName}" \`route\` must be a boolean`);
447
+ fn.route = value.route;
448
+ }
449
+ if (value.airwayStep !== void 0) {
450
+ const step = value.airwayStep;
451
+ if (!isRecord(step) || typeof step.pipeline !== "string" || typeof step.resource !== "string") throw new Error(`oxy-app.json: function "${fnName}" \`airwayStep\` must be { pipeline, resource }`);
452
+ fn.airwayStep = {
453
+ pipeline: step.pipeline,
454
+ resource: step.resource
455
+ };
456
+ }
457
+ if (value.timeoutSeconds !== void 0) {
458
+ const t = value.timeoutSeconds;
459
+ 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]`);
460
+ fn.timeoutSeconds = t;
461
+ }
462
+ if (value.cache !== void 0) {
463
+ const c = value.cache;
464
+ if (!isRecord(c)) throw new Error(`oxy-app.json: function "${fnName}" \`cache\` must be an object`);
465
+ if (c.ttlSeconds !== void 0) {
466
+ const ttl = c.ttlSeconds;
467
+ if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < 1) throw new Error(`oxy-app.json: function "${fnName}" \`cache.ttlSeconds\` must be a positive integer`);
468
+ fn.cache = { ttlSeconds: ttl };
469
+ }
470
+ }
471
+ if (value.retries !== void 0) {
472
+ const r = value.retries;
473
+ if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
474
+ const retries = {};
475
+ for (const key of [
476
+ "maxAttempts",
477
+ "minTimeoutMs",
478
+ "maxTimeoutMs"
479
+ ]) {
480
+ const n = r[key];
481
+ if (n !== void 0) {
482
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
483
+ retries[key] = n;
484
+ }
485
+ }
486
+ fn.retries = retries;
487
+ }
488
+ if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
489
+ const hasSchedule = fn.schedule !== void 0;
490
+ const hasAirway = fn.airwayStep !== void 0;
491
+ if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
492
+ out[fnName] = fn;
493
+ }
494
+ return out;
495
+ }
290
496
  function isRecord(v) {
291
497
  return typeof v === "object" && v !== null && !Array.isArray(v);
292
498
  }
293
499
 
500
+ //#endregion
501
+ //#region src/customer-app/function-invoke.ts
502
+ const inflight$1 = /* @__PURE__ */ new Map();
503
+ /**
504
+ * Dedup key for an invocation: function name + its (stable-serialized) body,
505
+ * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
506
+ * the separator can never collide with a name.
507
+ */
508
+ function functionInvokeKey(name, body) {
509
+ return `${name}\n${JSON.stringify(body ?? {})}`;
510
+ }
511
+ /**
512
+ * Run `run()` unless an identical invocation is already in flight, in which
513
+ * case share its promise. The entry is removed once the promise settles — so
514
+ * this dedups concurrency only, it does NOT memoize the result.
515
+ */
516
+ function sharedFunctionInvoke(key, run) {
517
+ const existing = inflight$1.get(key);
518
+ if (existing) return existing;
519
+ const p = run().finally(() => {
520
+ inflight$1.delete(key);
521
+ });
522
+ inflight$1.set(key, p);
523
+ return p;
524
+ }
525
+
526
+ //#endregion
527
+ //#region src/customer-app/function-sse.ts
528
+ /**
529
+ * Read a `text/event-stream` function response to completion. Resolves with the
530
+ * decoded result + captured logs, or rejects (with `.logs` attached) on an
531
+ * `event: error` frame / a stream that ends without a terminal event.
532
+ */
533
+ async function readFunctionSseStream(resp) {
534
+ const reader = resp.body?.getReader();
535
+ if (!reader) throw new Error("function response has no body stream");
536
+ const decoder = new TextDecoder();
537
+ let buffer = "";
538
+ let dataPayload = "";
539
+ const logs = [];
540
+ const handleFrame = (frame) => {
541
+ let event = "message";
542
+ let data = "";
543
+ for (const line of frame.split("\n")) if (line.startsWith("event:")) event = line.slice(6).trim();
544
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
545
+ if (event === "log") try {
546
+ const l = JSON.parse(data);
547
+ logs.push({
548
+ level: String(l.level ?? "info"),
549
+ message: String(l.message ?? "")
550
+ });
551
+ } catch {}
552
+ else if (event === "data") dataPayload = data;
553
+ else if (event === "done") return {
554
+ done: true,
555
+ value: dataPayload ? JSON.parse(dataPayload) : null
556
+ };
557
+ else if (event === "error") {
558
+ const payload = data ? JSON.parse(data) : {};
559
+ const err = new Error(payload.message || payload.error || "function invocation failed");
560
+ err.name = payload.error || "FunctionError";
561
+ err.logs = logs;
562
+ throw err;
563
+ }
564
+ };
565
+ for (;;) {
566
+ const { done, value } = await reader.read();
567
+ if (done) break;
568
+ buffer += decoder.decode(value, { stream: true });
569
+ let sep;
570
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
571
+ const frame = buffer.slice(0, sep);
572
+ buffer = buffer.slice(sep + 2);
573
+ const result = handleFrame(frame);
574
+ if (result) return {
575
+ value: result.value,
576
+ logs
577
+ };
578
+ }
579
+ }
580
+ throw new Error("function stream ended without a terminal event");
581
+ }
582
+
294
583
  //#endregion
295
584
  //#region src/customer-app/interpolate.ts
296
585
  /**
@@ -355,6 +644,50 @@ function isSafeLinkHref(raw) {
355
644
  return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
356
645
  }
357
646
 
647
+ //#endregion
648
+ //#region src/customer-app/query-cache.ts
649
+ const SWR_TTL_MS = 3e4;
650
+ const inflight = /* @__PURE__ */ new Map();
651
+ const cache = /* @__PURE__ */ new Map();
652
+ function queryKey(projectId, db, sql) {
653
+ return `${projectId} ${db ?? ""} ${sql}`;
654
+ }
655
+ function getCached(projectId, sql, db) {
656
+ const e = cache.get(queryKey(projectId, db, sql));
657
+ return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
658
+ }
659
+ /** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
660
+ * short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
661
+ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
662
+ const key = queryKey(projectId, db, sql);
663
+ if (!opts.force) {
664
+ const fresh = getCached(projectId, sql, db);
665
+ if (fresh) return fresh;
666
+ }
667
+ const existing = inflight.get(key);
668
+ if (existing) return existing;
669
+ const body = JSON.stringify({
670
+ sql,
671
+ ...db ? { database: db } : {}
672
+ });
673
+ const p = (async () => {
674
+ const resp = await fetcher(`/api/projects/${projectId}/query`, {
675
+ method: "POST",
676
+ headers: { "content-type": "application/json" },
677
+ body
678
+ });
679
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
680
+ const data = await resp.json();
681
+ cache.set(key, {
682
+ at: Date.now(),
683
+ data
684
+ });
685
+ return data;
686
+ })().finally(() => inflight.delete(key));
687
+ inflight.set(key, p);
688
+ return p;
689
+ }
690
+
358
691
  //#endregion
359
692
  //#region src/customer-app/react.tsx
360
693
  function defaultFetcher(input, init) {
@@ -396,26 +729,55 @@ function OxyAppProvider(props) {
396
729
  }, [manifestOptions, fetcher]);
397
730
  if (state.status === "error" && state.error) {
398
731
  const err = state.error;
399
- return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, errorFallback ? errorFallback(err) : defaultErrorFallback(err));
732
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
733
+ value: state,
734
+ children: errorFallback ? errorFallback(err) : defaultErrorFallback(err)
735
+ });
400
736
  }
401
- if (state.status === "loading") return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, fallback ?? null);
402
- return /* @__PURE__ */ react.createElement(OxyAppContext.Provider, { value: state }, children);
737
+ if (state.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
738
+ value: state,
739
+ children: fallback ?? null
740
+ });
741
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAppContext.Provider, {
742
+ value: state,
743
+ children
744
+ });
403
745
  }
404
746
  function defaultErrorFallback(err) {
405
- return /* @__PURE__ */ react.createElement("div", { style: {
406
- margin: "2rem auto",
407
- maxWidth: "640px",
408
- padding: "1rem",
409
- border: "1px solid #fca5a5",
410
- background: "#fee2e2",
411
- color: "#991b1b",
412
- borderRadius: "8px",
413
- fontFamily: "system-ui, -apple-system, sans-serif",
414
- fontSize: "14px"
415
- } }, /* @__PURE__ */ react.createElement("div", { style: { fontWeight: 600 } }, err.title), /* @__PURE__ */ react.createElement("pre", { style: {
416
- fontSize: "12px",
417
- marginTop: "4px"
418
- } }, err.message), /* @__PURE__ */ react.createElement("div", { style: { marginTop: "12px" } }, /* @__PURE__ */ react.createElement("strong", null, "What to try:"), " ", err.hint));
747
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
748
+ style: {
749
+ margin: "2rem auto",
750
+ maxWidth: "640px",
751
+ padding: "1rem",
752
+ border: "1px solid #fca5a5",
753
+ background: "#fee2e2",
754
+ color: "#991b1b",
755
+ borderRadius: "8px",
756
+ fontFamily: "system-ui, -apple-system, sans-serif",
757
+ fontSize: "14px"
758
+ },
759
+ children: [
760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
761
+ style: { fontWeight: 600 },
762
+ children: err.title
763
+ }),
764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
765
+ style: {
766
+ fontSize: "12px",
767
+ marginTop: "4px"
768
+ },
769
+ children: err.message
770
+ }),
771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
772
+ style: { marginTop: "12px" },
773
+ children: [
774
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "What to try:" }),
775
+ " ",
776
+ err.hint
777
+ ]
778
+ })
779
+ ]
780
+ });
419
781
  }
420
782
  /**
421
783
  * Emit a one-time `console.warn` the first time a beta hook is
@@ -430,58 +792,6 @@ function warnBetaOnce(name) {
430
792
  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.`);
431
793
  }
432
794
  /**
433
- * Error thrown by all customer-app hooks when an API call returns a
434
- * non-2xx response. Carries the structured `code` + `hint` the server
435
- * emits so bundle UIs can render an actionable message instead of
436
- * "404: { ...json... }".
437
- *
438
- * The server contract is documented in
439
- * `crates/app/src/server/api/projects/agent_ask.rs` and
440
- * `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
441
- * Hooks that previously wrapped the raw text in `new Error()` now
442
- * throw this type instead.
443
- */
444
- var OxyApiError = class extends Error {
445
- constructor(opts) {
446
- const base = opts.message || `HTTP ${opts.status}`;
447
- const code = opts.code ? ` [${opts.code}]` : "";
448
- const hint = opts.hint ? `\n\n${opts.hint}` : "";
449
- super(`${base}${code}${hint}`);
450
- this.name = "OxyApiError";
451
- this.status = opts.status;
452
- this.code = opts.code ?? null;
453
- this.hint = opts.hint ?? null;
454
- }
455
- };
456
- /**
457
- * Read a non-2xx response from oxy and return an `OxyApiError`.
458
- * Parses the JSON envelope when present; falls back to raw text
459
- * (truncated to 240 chars so a runaway HTML error page doesn't
460
- * dominate the bundle UI).
461
- */
462
- async function apiErrorFromResponse(resp) {
463
- let body;
464
- let raw = "";
465
- try {
466
- raw = await resp.text();
467
- body = raw ? JSON.parse(raw) : void 0;
468
- } catch {}
469
- if (body && typeof body === "object") {
470
- const b = body;
471
- return new OxyApiError({
472
- status: resp.status,
473
- message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
474
- code: typeof b.code === "string" ? b.code : null,
475
- hint: typeof b.hint === "string" ? b.hint : null
476
- });
477
- }
478
- const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
479
- return new OxyApiError({
480
- status: resp.status,
481
- message: snippet || `HTTP ${resp.status}`
482
- });
483
- }
484
- /**
485
795
  * Read the resolved manifest from context. Throws if called outside
486
796
  * `<OxyAppProvider>` — that's a programmer error worth surfacing
487
797
  * loudly, not silently swallowing.
@@ -535,36 +845,35 @@ function useQuery(input, opts = {}) {
535
845
  } : s);
536
846
  return;
537
847
  }
538
- const ctrl = new AbortController();
539
848
  let cancelled = false;
849
+ const cached = getCached(projectId, sqlWithParams, input.database);
850
+ if (cached && nonce === 0) {
851
+ const { columns, rows } = cached;
852
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
853
+ setState({
854
+ rows: objects,
855
+ columns,
856
+ loading: false,
857
+ error: null
858
+ });
859
+ return;
860
+ }
540
861
  setState((s) => ({
541
862
  ...s,
542
863
  loading: true,
543
864
  error: null
544
865
  }));
545
- const body = JSON.stringify({
546
- sql: sqlWithParams,
547
- ...input.database ? { database: input.database } : {}
548
- });
549
- fetcher(`/api/projects/${projectId}/query`, {
550
- method: "POST",
551
- headers: { "content-type": "application/json" },
552
- body,
553
- signal: ctrl.signal
554
- }).then(async (resp) => {
555
- if (!resp.ok) throw await apiErrorFromResponse(resp);
556
- return resp.json();
557
- }).then(({ columns, rows }) => {
866
+ sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
558
867
  if (cancelled) return;
868
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
559
869
  setState({
560
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
870
+ rows: objects,
561
871
  columns,
562
872
  loading: false,
563
873
  error: null
564
874
  });
565
875
  }).catch((err) => {
566
876
  if (cancelled) return;
567
- if (err instanceof DOMException && err.name === "AbortError") return;
568
877
  setState((s) => ({
569
878
  ...s,
570
879
  loading: false,
@@ -573,7 +882,6 @@ function useQuery(input, opts = {}) {
573
882
  });
574
883
  return () => {
575
884
  cancelled = true;
576
- ctrl.abort();
577
885
  };
578
886
  }, [
579
887
  enabled,
@@ -592,6 +900,81 @@ function useQuery(input, opts = {}) {
592
900
  };
593
901
  }
594
902
  /**
903
+ * Imperative hook for invoking an Oxy Function by name.
904
+ *
905
+ * ```tsx
906
+ * const refresh = useFunction("refresh-sales");
907
+ * <button disabled={refresh.isLoading} onClick={() => refresh.invoke({ full: true })}>
908
+ * Refresh
909
+ * </button>
910
+ * ```
911
+ */
912
+ function useFunction(name) {
913
+ const ctx = react.useContext(OxyAppContext);
914
+ if (!ctx) throw new Error("useFunction must be called inside <OxyAppProvider>");
915
+ const fetcher = ctx.fetcher;
916
+ const resolved = ctx.resolved;
917
+ const [state, setState] = react.useState({
918
+ data: null,
919
+ isLoading: false,
920
+ error: null,
921
+ logs: []
922
+ });
923
+ return {
924
+ invoke: react.useCallback(async (body, opts) => {
925
+ if (!resolved) throw new Error("useFunction.invoke called before the manifest finished loading. Render behind the provider's `fallback` until ready.");
926
+ const { orgSlug, appSlug, apiBaseUrl } = resolved;
927
+ const url = `${apiBaseUrl || ""}/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/fn/${encodeURIComponent(name)}`;
928
+ setState((s) => ({
929
+ ...s,
930
+ isLoading: true,
931
+ error: null
932
+ }));
933
+ try {
934
+ const headers = {
935
+ "content-type": "application/json",
936
+ accept: "text/event-stream"
937
+ };
938
+ if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
939
+ const result = await sharedFunctionInvoke(functionInvokeKey(name, body), async () => {
940
+ const resp = await fetcher(url, {
941
+ method: "POST",
942
+ headers,
943
+ body: JSON.stringify(body ?? {})
944
+ });
945
+ if (!resp.ok && resp.status !== 200) throw await apiErrorFromResponse(resp);
946
+ return readFunctionSseStream(resp);
947
+ });
948
+ setState({
949
+ data: result.value,
950
+ isLoading: false,
951
+ error: null,
952
+ logs: result.logs
953
+ });
954
+ return result.value;
955
+ } catch (err) {
956
+ const e = err instanceof Error ? err : new Error(String(err));
957
+ const logs = e.logs ?? [];
958
+ setState((s) => ({
959
+ ...s,
960
+ isLoading: false,
961
+ error: e,
962
+ logs
963
+ }));
964
+ throw e;
965
+ }
966
+ }, [
967
+ resolved,
968
+ fetcher,
969
+ name
970
+ ]),
971
+ data: state.data,
972
+ isLoading: state.isLoading,
973
+ error: state.error,
974
+ logs: state.logs
975
+ };
976
+ }
977
+ /**
595
978
  * Run a semantic-layer query against the project's `.view.yml` /
596
979
  * `.topic.yml` definitions. The server compiles to SQL and executes
597
980
  * through the same connector path as `useQuery`, so result shape
@@ -639,7 +1022,8 @@ function useSemanticQuery(input, opts = {}) {
639
1022
  filters: input.filters ?? [],
640
1023
  ...input.limit != null ? { limit: input.limit } : {}
641
1024
  });
642
- fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
1025
+ const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
1026
+ fetcher(url, {
643
1027
  method: "POST",
644
1028
  headers: { "content-type": "application/json" },
645
1029
  body,
@@ -649,8 +1033,9 @@ function useSemanticQuery(input, opts = {}) {
649
1033
  return resp.json();
650
1034
  }).then(({ columns, rows, truncated, sql }) => {
651
1035
  if (cancelled) return;
1036
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
652
1037
  setState({
653
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
1038
+ rows: objects,
654
1039
  columns,
655
1040
  truncated,
656
1041
  sql: sql ?? null,
@@ -1037,7 +1422,7 @@ function parseSseData(raw) {
1037
1422
  * uniformly. New types added upstream don't surface as artifacts
1038
1423
  * until added here — that's intentional, the renderer needs to
1039
1424
  * know how to display each. */
1040
- const SQL_EVENT_TYPES = new Set([
1425
+ const SQL_EVENT_TYPES = /* @__PURE__ */ new Set([
1041
1426
  "query_executed",
1042
1427
  "query_generated",
1043
1428
  "verified_sql",
@@ -1166,6 +1551,82 @@ function sleep(ms, signal) {
1166
1551
  });
1167
1552
  }
1168
1553
  /**
1554
+ * Engineer-tagged usage event. Free-form `event_name` (≤ 64 chars,
1555
+ * `[a-z][a-z0-9-]*` validated server-side) + optional JSON `payload`
1556
+ * (object, ≤ 4 KiB serialized). Surfaces in the admin Activity tab
1557
+ * grouped by name, with drill-down into recent occurrences.
1558
+ *
1559
+ * The handler returned by [`useTrackEvent`] is **fire-and-forget**:
1560
+ * it enqueues the event into an in-memory batch flushed every second
1561
+ * (and on `pagehide` so a navigation away doesn't drop the tail).
1562
+ * No await semantics — call it inline from a click handler without
1563
+ * awaiting it. Server-side validation errors are logged to the
1564
+ * console; the call site doesn't need to handle them.
1565
+ *
1566
+ * Example:
1567
+ * ```tsx
1568
+ * const track = useTrackEvent();
1569
+ * <button
1570
+ * onClick={() => {
1571
+ * track("export-clicked", { format: "csv", rowCount });
1572
+ * doExport();
1573
+ * }}
1574
+ * >Export</button>
1575
+ * ```
1576
+ *
1577
+ * Rate-limited at 60/min per (user, app) on the server. A burst that
1578
+ * trips the limit drops the excess events with a console warning;
1579
+ * within-limit events are unaffected.
1580
+ */
1581
+ function useTrackEvent() {
1582
+ const { projectId, fetcher } = useOxyApp();
1583
+ const queueRef = react.useRef([]);
1584
+ const flushTimerRef = react.useRef(null);
1585
+ const flush = react.useCallback(() => {
1586
+ flushTimerRef.current = null;
1587
+ if (!projectId) return;
1588
+ const batch = queueRef.current;
1589
+ if (batch.length === 0) return;
1590
+ queueRef.current = [];
1591
+ for (const evt of batch) {
1592
+ const url = `/api/customer-apps/${projectId}/events`;
1593
+ const body = JSON.stringify(evt);
1594
+ try {
1595
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1596
+ else fetcher(url, {
1597
+ method: "POST",
1598
+ headers: { "content-type": "application/json" },
1599
+ body
1600
+ }).catch((e) => {
1601
+ console.warn("[oxy] useTrackEvent flush failed:", e);
1602
+ });
1603
+ } catch (e) {
1604
+ console.warn("[oxy] useTrackEvent enqueue failed:", e);
1605
+ }
1606
+ }
1607
+ }, [projectId, fetcher]);
1608
+ react.useEffect(() => {
1609
+ if (typeof window === "undefined") return;
1610
+ const onHide = () => flush();
1611
+ window.addEventListener("pagehide", onHide);
1612
+ return () => {
1613
+ window.removeEventListener("pagehide", onHide);
1614
+ flush();
1615
+ if (flushTimerRef.current !== null) {
1616
+ clearTimeout(flushTimerRef.current);
1617
+ flushTimerRef.current = null;
1618
+ }
1619
+ };
1620
+ }, [flush]);
1621
+ return react.useCallback((name, payload) => {
1622
+ queueRef.current.push({
1623
+ event_name: name,
1624
+ payload: payload ?? {}
1625
+ });
1626
+ if (flushTimerRef.current === null) flushTimerRef.current = setTimeout(flush, 1e3);
1627
+ }, [flush]);
1628
+ }
1629
+ /**
1169
1630
  * Renders an agent run's answer + artifacts + thread link as a
1170
1631
  * single block. The default styling is intentionally neutral
1171
1632
  * (system fonts, gray surfaces) so it blends into any bundle.
@@ -1187,25 +1648,55 @@ function OxyAnswer(props) {
1187
1648
  const isRunning = state === "running";
1188
1649
  const isFailed = state === "failed";
1189
1650
  const needsClarification = state === "needs_clarification";
1190
- return /* @__PURE__ */ react.createElement("div", {
1651
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1191
1652
  className,
1192
- style: styles.answerWrap
1193
- }, isRunning && answer === null ? /* @__PURE__ */ react.createElement("div", { style: styles.statusRow }, /* @__PURE__ */ react.createElement("span", {
1194
- style: styles.spinner,
1195
- "aria-hidden": "true"
1196
- }), /* @__PURE__ */ react.createElement("span", { style: styles.statusText }, "Thinking…")) : null, artifacts.length > 0 ? /* @__PURE__ */ react.createElement("div", { style: styles.artifactList }, artifacts.map((a) => /* @__PURE__ */ react.createElement(SqlArtifactBlock, {
1197
- key: a.id,
1198
- artifact: a,
1199
- maxRows: maxArtifactRows
1200
- }))) : null, answer ? /* @__PURE__ */ react.createElement("div", { style: styles.markdown }, /* @__PURE__ */ react.createElement(MarkdownText, { text: answer })) : null, needsClarification && clarification ? /* @__PURE__ */ react.createElement("div", { style: styles.clarification }, /* @__PURE__ */ react.createElement("strong", null, "Agent needs clarification:"), /* @__PURE__ */ react.createElement("div", { style: { marginTop: 4 } }, clarification)) : null, isFailed && error ? /* @__PURE__ */ react.createElement(ErrorBlock, { error }) : null, threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ react.createElement("div", { style: styles.threadLinkRow }, /* @__PURE__ */ react.createElement("a", {
1201
- href: threadUrl,
1202
- target: "_blank",
1203
- rel: "noreferrer noopener",
1204
- style: styles.threadLink
1205
- }, threadLinkLabel, " →"), /* @__PURE__ */ react.createElement("span", {
1206
- style: styles.betaBadge,
1207
- title: "Thread linking is in beta — see docs"
1208
- }, "beta")) : null);
1653
+ style: styles.answerWrap,
1654
+ children: [
1655
+ isRunning && answer === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1656
+ style: styles.statusRow,
1657
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1658
+ style: styles.spinner,
1659
+ "aria-hidden": "true"
1660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1661
+ style: styles.statusText,
1662
+ children: "Thinking…"
1663
+ })]
1664
+ }) : null,
1665
+ artifacts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1666
+ style: styles.artifactList,
1667
+ children: artifacts.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SqlArtifactBlock, {
1668
+ artifact: a,
1669
+ maxRows: maxArtifactRows
1670
+ }, a.id))
1671
+ }) : null,
1672
+ answer ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1673
+ style: styles.markdown,
1674
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarkdownText, { text: answer })
1675
+ }) : null,
1676
+ needsClarification && clarification ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1677
+ style: styles.clarification,
1678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Agent needs clarification:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1679
+ style: { marginTop: 4 },
1680
+ children: clarification
1681
+ })]
1682
+ }) : null,
1683
+ isFailed && error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBlock, { error }) : null,
1684
+ threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1685
+ style: styles.threadLinkRow,
1686
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
1687
+ href: threadUrl,
1688
+ target: "_blank",
1689
+ rel: "noreferrer noopener",
1690
+ style: styles.threadLink,
1691
+ children: [threadLinkLabel, " →"]
1692
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1693
+ style: styles.betaBadge,
1694
+ title: "Thread linking is in beta — see docs",
1695
+ children: "beta"
1696
+ })]
1697
+ }) : null
1698
+ ]
1699
+ });
1209
1700
  }
1210
1701
  /**
1211
1702
  * Complete drop-in chat surface. One agent, one input, one answer
@@ -1229,37 +1720,48 @@ function OxyChat(props) {
1229
1720
  if (!q || run.state === "running") return;
1230
1721
  run.ask(q);
1231
1722
  }, [question, run]);
1232
- return /* @__PURE__ */ react.createElement("div", {
1723
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
1724
  className,
1234
- style: styles.chatWrap
1235
- }, /* @__PURE__ */ react.createElement("form", {
1236
- onSubmit: submit,
1237
- style: styles.chatForm
1238
- }, /* @__PURE__ */ react.createElement("input", {
1239
- type: "text",
1240
- value: question,
1241
- onChange: (e) => setQuestion(e.target.value),
1242
- placeholder,
1243
- disabled: run.state === "running",
1244
- style: styles.chatInput,
1245
- "aria-label": "Question"
1246
- }), /* @__PURE__ */ react.createElement("button", {
1247
- type: "submit",
1248
- disabled: run.state === "running" || question.trim() === "",
1249
- style: styles.chatSubmit
1250
- }, run.state === "running" ? "…" : submitLabel), run.state === "running" ? /* @__PURE__ */ react.createElement("button", {
1251
- type: "button",
1252
- onClick: run.cancel,
1253
- style: styles.chatCancel
1254
- }, "Stop") : null), run.state === "idle" ? emptyState ?? /* @__PURE__ */ react.createElement("div", { style: styles.emptyState }, "Ask a question to get started.") : /* @__PURE__ */ react.createElement(OxyAnswer, {
1255
- answer: run.answer,
1256
- artifacts: run.artifacts,
1257
- state: run.state,
1258
- clarification: run.clarification,
1259
- error: run.error,
1260
- threadUrl: run.threadUrl,
1261
- maxArtifactRows
1262
- }));
1725
+ style: styles.chatWrap,
1726
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1727
+ onSubmit: submit,
1728
+ style: styles.chatForm,
1729
+ children: [
1730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1731
+ type: "text",
1732
+ value: question,
1733
+ onChange: (e) => setQuestion(e.target.value),
1734
+ placeholder,
1735
+ disabled: run.state === "running",
1736
+ style: styles.chatInput,
1737
+ "aria-label": "Question"
1738
+ }),
1739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1740
+ type: "submit",
1741
+ disabled: run.state === "running" || question.trim() === "",
1742
+ style: styles.chatSubmit,
1743
+ children: run.state === "running" ? "…" : submitLabel
1744
+ }),
1745
+ run.state === "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1746
+ type: "button",
1747
+ onClick: run.cancel,
1748
+ style: styles.chatCancel,
1749
+ children: "Stop"
1750
+ }) : null
1751
+ ]
1752
+ }), run.state === "idle" ? emptyState ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1753
+ style: styles.emptyState,
1754
+ children: "Ask a question to get started."
1755
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OxyAnswer, {
1756
+ answer: run.answer,
1757
+ artifacts: run.artifacts,
1758
+ state: run.state,
1759
+ clarification: run.clarification,
1760
+ error: run.error,
1761
+ threadUrl: run.threadUrl,
1762
+ maxArtifactRows
1763
+ })]
1764
+ });
1263
1765
  }
1264
1766
  function SqlArtifactBlock(props) {
1265
1767
  const { artifact, maxRows } = props;
@@ -1268,17 +1770,53 @@ function SqlArtifactBlock(props) {
1268
1770
  const truncated = results ? results.rows.length > maxRows : false;
1269
1771
  const visibleRows = results ? results.rows.slice(0, maxRows) : [];
1270
1772
  const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
1271
- return /* @__PURE__ */ react.createElement("div", { style: styles.artifact }, /* @__PURE__ */ react.createElement("button", {
1272
- type: "button",
1273
- onClick: () => setOpen((o) => !o),
1274
- style: styles.artifactHeader
1275
- }, /* @__PURE__ */ react.createElement("span", { style: styles.artifactBadge }, sourceLabel), /* @__PURE__ */ react.createElement("span", { style: styles.artifactSummary }, results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"), /* @__PURE__ */ react.createElement("span", { style: styles.artifactToggle }, open ? "Hide" : "Show")), open ? /* @__PURE__ */ react.createElement("div", null, /* @__PURE__ */ react.createElement("pre", { style: styles.sqlBlock }, artifact.sql), artifact.error ? /* @__PURE__ */ react.createElement("div", { style: styles.error }, artifact.error) : results ? /* @__PURE__ */ react.createElement("div", { style: styles.resultsWrap }, /* @__PURE__ */ react.createElement("table", { style: styles.resultsTable }, /* @__PURE__ */ react.createElement("thead", null, /* @__PURE__ */ react.createElement("tr", null, results.columns.map((c) => /* @__PURE__ */ react.createElement("th", {
1276
- key: c,
1277
- style: styles.resultsTh
1278
- }, c)))), /* @__PURE__ */ react.createElement("tbody", null, visibleRows.map((row, i) => /* @__PURE__ */ react.createElement("tr", { key: i }, row.map((cell, j) => /* @__PURE__ */ react.createElement("td", {
1279
- key: j,
1280
- style: styles.resultsTd
1281
- }, formatCell(cell))))))), truncated ? /* @__PURE__ */ react.createElement("div", { style: styles.truncatedNote }, "+", results.rows.length - maxRows, " more rows. Open the thread in Oxy to see all.") : null) : null) : null);
1773
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1774
+ style: styles.artifact,
1775
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1776
+ type: "button",
1777
+ onClick: () => setOpen((o) => !o),
1778
+ style: styles.artifactHeader,
1779
+ children: [
1780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1781
+ style: styles.artifactBadge,
1782
+ children: sourceLabel
1783
+ }),
1784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1785
+ style: styles.artifactSummary,
1786
+ children: results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"
1787
+ }),
1788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1789
+ style: styles.artifactToggle,
1790
+ children: open ? "Hide" : "Show"
1791
+ })
1792
+ ]
1793
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1794
+ style: styles.sqlBlock,
1795
+ children: artifact.sql
1796
+ }), artifact.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1797
+ style: styles.error,
1798
+ children: artifact.error
1799
+ }) : results ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1800
+ style: styles.resultsWrap,
1801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
1802
+ style: styles.resultsTable,
1803
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: results.columns.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1804
+ style: styles.resultsTh,
1805
+ children: c
1806
+ }, c)) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: visibleRows.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: row.map((cell, j) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1807
+ style: styles.resultsTd,
1808
+ children: formatCell(cell)
1809
+ }, j)) }, i)) })]
1810
+ }), truncated ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1811
+ style: styles.truncatedNote,
1812
+ children: [
1813
+ "+",
1814
+ results.rows.length - maxRows,
1815
+ " more rows. Open the thread in Oxy to see all."
1816
+ ]
1817
+ }) : null]
1818
+ }) : null] }) : null]
1819
+ });
1282
1820
  }
1283
1821
  function formatCell(value) {
1284
1822
  if (value === null || value === void 0) return "—";
@@ -1293,16 +1831,36 @@ function formatCell(value) {
1293
1831
  */
1294
1832
  function ErrorBlock(props) {
1295
1833
  const { error } = props;
1296
- if (error instanceof OxyApiError) return /* @__PURE__ */ react.createElement("div", { style: styles.error }, /* @__PURE__ */ react.createElement("div", null, /* @__PURE__ */ react.createElement("strong", null, "Run failed:"), " ", error.message.split("\n\n")[0]), error.hint ? /* @__PURE__ */ react.createElement("div", { style: {
1297
- marginTop: 6,
1298
- fontWeight: 400,
1299
- whiteSpace: "pre-wrap"
1300
- } }, /* @__PURE__ */ react.createElement("strong", null, "Hint:"), " ", error.hint) : null);
1301
- return /* @__PURE__ */ react.createElement("div", { style: styles.error }, /* @__PURE__ */ react.createElement("strong", null, "Run failed:"), " ", error.message);
1834
+ if (error instanceof OxyApiError) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1835
+ style: styles.error,
1836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
1838
+ " ",
1839
+ error.message.split("\n\n")[0]
1840
+ ] }), error.hint ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1841
+ style: {
1842
+ marginTop: 6,
1843
+ fontWeight: 400,
1844
+ whiteSpace: "pre-wrap"
1845
+ },
1846
+ children: [
1847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Hint:" }),
1848
+ " ",
1849
+ error.hint
1850
+ ]
1851
+ }) : null]
1852
+ });
1853
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1854
+ style: styles.error,
1855
+ children: [
1856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Run failed:" }),
1857
+ " ",
1858
+ error.message
1859
+ ]
1860
+ });
1302
1861
  }
1303
1862
  function MarkdownText(props) {
1304
- const blocks = react.useMemo(() => parseMarkdown(props.text), [props.text]);
1305
- return /* @__PURE__ */ react.createElement(react.Fragment, null, blocks);
1863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: react.useMemo(() => parseMarkdown(props.text), [props.text]) });
1306
1864
  }
1307
1865
  function parseMarkdown(text) {
1308
1866
  const lines = text.replace(/\r\n/g, "\n").split("\n");
@@ -1372,27 +1930,23 @@ function parseMarkdown(text) {
1372
1930
  }
1373
1931
  return blocks.map((b, idx) => {
1374
1932
  switch (b.kind) {
1375
- case "h": {
1376
- const Tag = `h${b.level}`;
1377
- const headingStyle = b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3;
1378
- return /* @__PURE__ */ react.createElement(Tag, {
1379
- key: idx,
1380
- style: headingStyle
1381
- }, renderInline(b.text));
1382
- }
1383
- case "code": return /* @__PURE__ */ react.createElement("pre", {
1384
- key: idx,
1933
+ case "h": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(`h${b.level}`, {
1934
+ style: b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3,
1935
+ children: renderInline(b.text)
1936
+ }, idx);
1937
+ case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1385
1938
  style: styles.codeBlock,
1386
- "data-lang": b.lang || void 0
1387
- }, /* @__PURE__ */ react.createElement("code", null, b.code));
1388
- case "list": return /* @__PURE__ */ react.createElement("ul", {
1389
- key: idx,
1390
- style: styles.list
1391
- }, b.items.map((item, i) => /* @__PURE__ */ react.createElement("li", { key: i }, renderInline(item))));
1392
- case "p": return /* @__PURE__ */ react.createElement("p", {
1393
- key: idx,
1394
- style: styles.paragraph
1395
- }, renderInline(b.text));
1939
+ "data-lang": b.lang || void 0,
1940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: b.code })
1941
+ }, idx);
1942
+ case "list": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1943
+ style: styles.list,
1944
+ children: b.items.map((item, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: renderInline(item) }, i))
1945
+ }, idx);
1946
+ case "p": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1947
+ style: styles.paragraph,
1948
+ children: renderInline(b.text)
1949
+ }, idx);
1396
1950
  }
1397
1951
  });
1398
1952
  }
@@ -1408,27 +1962,31 @@ function renderInline(text) {
1408
1962
  const patterns = [
1409
1963
  {
1410
1964
  re: /`([^`]+)`/,
1411
- render: (m) => /* @__PURE__ */ react.createElement("code", { style: styles.inlineCode }, m[1])
1965
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1966
+ style: styles.inlineCode,
1967
+ children: m[1]
1968
+ })
1412
1969
  },
1413
1970
  {
1414
1971
  re: /\[([^\]]+)\]\(([^)]+)\)/,
1415
1972
  render: (m) => {
1416
- if (isSafeLinkHref(m[2])) return /* @__PURE__ */ react.createElement("a", {
1973
+ if (isSafeLinkHref(m[2])) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1417
1974
  href: m[2],
1418
1975
  target: "_blank",
1419
1976
  rel: "noreferrer noopener",
1420
- style: styles.link
1421
- }, m[1]);
1422
- return /* @__PURE__ */ react.createElement(react.Fragment, null, m[1]);
1977
+ style: styles.link,
1978
+ children: m[1]
1979
+ });
1980
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: m[1] });
1423
1981
  }
1424
1982
  },
1425
1983
  {
1426
1984
  re: /\*\*([^*]+)\*\*/,
1427
- render: (m) => /* @__PURE__ */ react.createElement("strong", null, m[1])
1985
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: m[1] })
1428
1986
  },
1429
1987
  {
1430
1988
  re: /\*([^*]+)\*/,
1431
- render: (m) => /* @__PURE__ */ react.createElement("em", null, m[1])
1989
+ render: (m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: m[1] })
1432
1990
  }
1433
1991
  ];
1434
1992
  while (remaining.length > 0) {
@@ -1446,7 +2004,7 @@ function renderInline(text) {
1446
2004
  break;
1447
2005
  }
1448
2006
  if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
1449
- segments.push(/* @__PURE__ */ react.createElement(react.Fragment, { key: key++ }, earliest.node));
2007
+ segments.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: earliest.node }, key++));
1450
2008
  remaining = remaining.slice(earliest.idx + earliest.len);
1451
2009
  }
1452
2010
  return segments;
@@ -1676,11 +2234,142 @@ const styles = {
1676
2234
  };
1677
2235
 
1678
2236
  //#endregion
2237
+ //#region src/metricTree.ts
2238
+ /**
2239
+ * Client for the `/semantic/metric-tree*` endpoints. Surfaces the four
2240
+ * airlayer metric-tree analyses (tree introspection, sensitivity, predict,
2241
+ * explain, opportunity) over typed methods.
2242
+ *
2243
+ * Construction is internal to {@link OxyClient} — call `client.metricTree`
2244
+ * to access an instance rather than building one yourself.
2245
+ *
2246
+ * @example
2247
+ * ```typescript
2248
+ * const client = await OxyClient.create({ projectId: "...", apiKey: "..." });
2249
+ * const tree = await client.metricTree.getTree();
2250
+ * const drivers = await client.metricTree.getSensitivity("orders.net_revenue");
2251
+ * ```
2252
+ */
2253
+ var MetricTreeClient = class {
2254
+ constructor(config, request) {
2255
+ this.config = config;
2256
+ this.request = request;
2257
+ }
2258
+ path(suffix) {
2259
+ return `/${this.config.projectId}/semantic/metric-tree${suffix}`;
2260
+ }
2261
+ buildQuery(extra = {}) {
2262
+ const params = { ...extra };
2263
+ if (this.config.branch) params.branch = this.config.branch;
2264
+ const qs = new URLSearchParams(params).toString();
2265
+ return qs ? `?${qs}` : "";
2266
+ }
2267
+ /**
2268
+ * Fetch the full metric tree, or the subtree rooted at `root`.
2269
+ *
2270
+ * @param root - Optional fully-qualified measure id to root the tree at.
2271
+ * @returns Nodes (measures) and edges (component / driver relationships).
2272
+ *
2273
+ * @example
2274
+ * ```typescript
2275
+ * const tree = await client.metricTree.getTree();
2276
+ * const subtree = await client.metricTree.getTree("orders.net_revenue");
2277
+ * ```
2278
+ */
2279
+ async getTree(root) {
2280
+ const query = this.buildQuery(root ? { root } : {});
2281
+ return this.request(this.path(query));
2282
+ }
2283
+ /**
2284
+ * Rank the declared drivers of a measure by influence.
2285
+ *
2286
+ * @param measureId - Fully-qualified measure id (`view.measure`).
2287
+ *
2288
+ * @example
2289
+ * ```typescript
2290
+ * const sensitivity = await client.metricTree.getSensitivity("orders.net_revenue");
2291
+ * for (const driver of sensitivity.drivers) {
2292
+ * console.log(driver.measure, driver.direction, driver.strength);
2293
+ * }
2294
+ * ```
2295
+ */
2296
+ async getSensitivity(measureId) {
2297
+ const query = this.buildQuery();
2298
+ return this.request(this.path(`/${encodeURIComponent(measureId)}/sensitivity${query}`));
2299
+ }
2300
+ /**
2301
+ * Propagate hypothetical `(measure, delta)` changes upward through the
2302
+ * tree. Returns the estimated impact on every downstream measure.
2303
+ *
2304
+ * @example
2305
+ * ```typescript
2306
+ * const result = await client.metricTree.predict([
2307
+ * { measure: "marketing_spend.total_spend", delta: 10000 },
2308
+ * ]);
2309
+ * ```
2310
+ */
2311
+ async predict(changes) {
2312
+ const query = this.buildQuery();
2313
+ return this.request(this.path(`/predict${query}`), {
2314
+ method: "POST",
2315
+ body: JSON.stringify({ changes })
2316
+ });
2317
+ }
2318
+ /**
2319
+ * Period-over-period root-cause decomposition. Recursively splits the
2320
+ * target measure by components and dimensions until the move concentrates.
2321
+ *
2322
+ * @example
2323
+ * ```typescript
2324
+ * const result = await client.metricTree.explain({
2325
+ * target: "financials.operating_profit",
2326
+ * time_dimension: "financials.month",
2327
+ * current_period: ["2025-09-01", "2025-09-30"],
2328
+ * previous_period: ["2025-08-01", "2025-08-31"],
2329
+ * });
2330
+ * ```
2331
+ */
2332
+ async explain(request) {
2333
+ const query = this.buildQuery();
2334
+ return this.request(this.path(`/explain${query}`), {
2335
+ method: "POST",
2336
+ body: JSON.stringify(request)
2337
+ });
2338
+ }
2339
+ /**
2340
+ * Size the upside opportunity for a measure by finding underperforming
2341
+ * segments. Skips high-cardinality dimensions and trims the long tail.
2342
+ *
2343
+ * @example
2344
+ * ```typescript
2345
+ * const result = await client.metricTree.findOpportunities({
2346
+ * target: "orders.net_revenue",
2347
+ * time_dimension: "orders.order_date",
2348
+ * period: ["2025-09-01", "2025-09-30"],
2349
+ * });
2350
+ * for (const dim of result.dimensions) {
2351
+ * console.log(dim.dimension, "+", dim.total_upside);
2352
+ * }
2353
+ * ```
2354
+ */
2355
+ async findOpportunities(request) {
2356
+ const query = this.buildQuery();
2357
+ return this.request(this.path(`/opportunity${query}`), {
2358
+ method: "POST",
2359
+ body: JSON.stringify(request)
2360
+ });
2361
+ }
2362
+ };
2363
+
2364
+ //#endregion
2365
+ exports.AnomaliesClient = AnomaliesClient;
2366
+ exports.MetricTreeClient = MetricTreeClient;
1679
2367
  exports.OxyAnswer = OxyAnswer;
1680
2368
  exports.OxyApiError = OxyApiError;
1681
2369
  exports.OxyAppProvider = OxyAppProvider;
1682
2370
  exports.OxyChat = OxyChat;
1683
2371
  exports._resetCustomerAppManifestCacheForTest = _resetCustomerAppManifestCacheForTest;
2372
+ exports.apiErrorFromResponse = apiErrorFromResponse;
1684
2373
  exports.getCustomerAppDebug = getCustomerAppDebug;
1685
2374
  exports.getOxyAppLogger = getOxyAppLogger;
1686
2375
  exports.interpretCustomerAppError = interpretCustomerAppError;
@@ -1688,8 +2377,10 @@ exports.loadCustomerAppManifest = loadCustomerAppManifest;
1688
2377
  exports.readInjectedAppConfig = readInjectedAppConfig;
1689
2378
  exports.setOxyAppLogger = setOxyAppLogger;
1690
2379
  exports.useAgentRun = useAgentRun;
2380
+ exports.useFunction = useFunction;
1691
2381
  exports.useProcedureRun = useProcedureRun;
1692
2382
  exports.useQuery = useQuery;
1693
2383
  exports.useResolvedManifest = useResolvedManifest;
1694
2384
  exports.useSemanticQuery = useSemanticQuery;
2385
+ exports.useTrackEvent = useTrackEvent;
1695
2386
  //# sourceMappingURL=index.cjs.map