@qoniai/qoni 0.6.0 → 0.7.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
@@ -538,6 +538,87 @@ var ActionKinds = {
538
538
  };
539
539
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
540
540
 
541
+ // src/scopes.ts
542
+ var QoniScopes = {
543
+ GUMEM_SESSION_CREATE: "gumem.session:create",
544
+ GUMEM_MESSAGE_WRITE: "gumem.message:write",
545
+ GUMEM_MEMORY_READ: "gumem.memory:read",
546
+ GUMEM_MEMORY_WRITE: "gumem.memory:write",
547
+ GUMEM_MEMORY_DELETE: "gumem.memory:delete",
548
+ GUMEM_SEARCH_RUN: "gumem.search:run",
549
+ GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
550
+ GUMEM_RESOURCE_WRITE: "gumem.resource:write",
551
+ GUMEM_ACTION_WRITE: "gumem.action:write",
552
+ GUMEM_ACTION_READ: "gumem.action:read",
553
+ GUMEM_PROFILE_READ: "gumem.profile:read",
554
+ // WebAgent products: exactly two verbs per product — `read` (observe) and
555
+ // `manage` (anything that changes execution state). See
556
+ // wa/docs/qoni-sdk-public-surface.md §3.
557
+ DO_ANYTHING_READ: "webagent.do_anything:read",
558
+ DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
559
+ WEB_SEARCH_READ: "webagent.web_search:read",
560
+ WEB_SEARCH_MANAGE: "webagent.web_search:manage",
561
+ DEEP_RESEARCH_READ: "webagent.deep_research:read",
562
+ DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
563
+ TRACK_READ: "webagent.track:read",
564
+ TRACK_MANAGE: "webagent.track:manage",
565
+ // Interactive site login (`InteractionHandle.openLogin()` on a `site_login`
566
+ // interaction). Not part of any product bundle: the AccessKey policy must
567
+ // allow it, and the delegation must request it explicitly.
568
+ SITE_LOGIN_REQUEST: "webagent.site_login:request",
569
+ // Persist that login into the browser profile (`OpenLoginResult.confirm()`).
570
+ SITE_LOGIN_CONFIRM: "webagent.site_login:confirm"
571
+ };
572
+ var QoniProductScopes = {
573
+ doAnything: [
574
+ QoniScopes.DO_ANYTHING_READ,
575
+ QoniScopes.DO_ANYTHING_MANAGE
576
+ ],
577
+ webSearch: [
578
+ QoniScopes.WEB_SEARCH_READ,
579
+ QoniScopes.WEB_SEARCH_MANAGE
580
+ ],
581
+ deepResearch: [
582
+ QoniScopes.DEEP_RESEARCH_READ,
583
+ QoniScopes.DEEP_RESEARCH_MANAGE
584
+ ],
585
+ track: [
586
+ QoniScopes.TRACK_READ,
587
+ QoniScopes.TRACK_MANAGE
588
+ ]
589
+ };
590
+ var KNOWN_SCOPES = Object.values(QoniScopes);
591
+ var QoniScopeBundles = {
592
+ GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
593
+ GUMEM_SESSION_RECALL: [
594
+ QoniScopes.GUMEM_MEMORY_READ,
595
+ QoniScopes.GUMEM_MEMORY_WRITE,
596
+ QoniScopes.GUMEM_MESSAGE_WRITE
597
+ ],
598
+ GUMEM_WRITE: [
599
+ QoniScopes.GUMEM_SESSION_CREATE,
600
+ QoniScopes.GUMEM_MESSAGE_WRITE,
601
+ QoniScopes.GUMEM_ACTION_WRITE
602
+ ],
603
+ // One bundle per WebAgent product = [read, manage].
604
+ DO_ANYTHING: [
605
+ QoniScopes.DO_ANYTHING_READ,
606
+ QoniScopes.DO_ANYTHING_MANAGE
607
+ ],
608
+ WEB_SEARCH: [
609
+ QoniScopes.WEB_SEARCH_READ,
610
+ QoniScopes.WEB_SEARCH_MANAGE
611
+ ],
612
+ DEEP_RESEARCH: [
613
+ QoniScopes.DEEP_RESEARCH_READ,
614
+ QoniScopes.DEEP_RESEARCH_MANAGE
615
+ ],
616
+ TRACK: [
617
+ QoniScopes.TRACK_READ,
618
+ QoniScopes.TRACK_MANAGE
619
+ ]
620
+ };
621
+
541
622
  // src/interactions.ts
542
623
  function asString2(value) {
543
624
  return typeof value === "string" && value ? value : void 0;
@@ -636,6 +717,9 @@ function normalizeInteraction(payload) {
636
717
  if (evidence) interaction.evidence = evidence;
637
718
  return interaction;
638
719
  }
720
+ function interactionActionScopes(action, productManageScope) {
721
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
722
+ }
639
723
  var InteractionHandle = class {
640
724
  constructor(interaction, post) {
641
725
  this.post = post;
@@ -660,48 +744,122 @@ var InteractionHandle = class {
660
744
  return this.interaction.actions.some((a) => a.kind === kind);
661
745
  }
662
746
  /** Answer a clarification ask. */
663
- answer(text) {
664
- return this.invoke(ActionKinds.answer, { response: text });
747
+ async answer(text) {
748
+ await this.invoke(ActionKinds.answer, { response: text });
665
749
  }
666
750
  /** Skip / dismiss the interaction (proceed without acting on it). */
667
- skip() {
668
- return this.invoke(ActionKinds.skip);
751
+ async skip() {
752
+ await this.invoke(ActionKinds.skip);
669
753
  }
670
754
  /** Confirm a pending confirmation (approve the plan / destructive op). */
671
- confirm() {
672
- return this.invoke(ActionKinds.confirm);
755
+ async confirm() {
756
+ await this.invoke(ActionKinds.confirm);
673
757
  }
674
758
  /** Reject a pending confirmation. */
675
- reject() {
676
- return this.invoke(ActionKinds.reject);
759
+ async reject() {
760
+ await this.invoke(ActionKinds.reject);
677
761
  }
678
- /** Open the login surface for a site_login interaction. */
679
- openLogin() {
680
- return this.invoke(ActionKinds.openLogin);
762
+ /**
763
+ * Start an interactive login session for a `site_login` interaction. The
764
+ * backend needs the site to sign in to: pass `siteId`, or omit it when the
765
+ * interaction lists exactly one site. Show `liveUrl` to the user; when they
766
+ * finish, call `confirm()` on the result to save the login, and act on its
767
+ * outcome (see {@link OpenLoginResult.confirm}) before {@link confirmSignedIn}.
768
+ * The delegation must include `QoniScopes.SITE_LOGIN_REQUEST` and
769
+ * `QoniScopes.SITE_LOGIN_CONFIRM`; the AccessKey policy must allow both.
770
+ */
771
+ async openLogin(options = {}) {
772
+ this.#declaredAction(ActionKinds.openLogin);
773
+ const body = { site_id: this.#loginSiteId(options.siteId) };
774
+ if (options.profileId !== void 0) {
775
+ if (typeof options.profileId !== "string" || !options.profileId) {
776
+ throw new QoniValidationError("openLogin profileId must be a non-empty string");
777
+ }
778
+ body.profile_id = options.profileId;
779
+ }
780
+ const response = await this.invoke(ActionKinds.openLogin, body);
781
+ const raw = isJsonObject(response) ? response : {};
782
+ const fields = camelizeRecord(raw);
783
+ const profileId = asString2(fields.profileId) ?? "";
784
+ const siteId = asString2(fields.siteId) ?? String(body.site_id);
785
+ return {
786
+ profileId,
787
+ siteId,
788
+ sessionId: asString2(fields.sessionId) ?? "",
789
+ liveUrl: asString2(fields.liveUrl) ?? "",
790
+ raw,
791
+ confirm: async (confirmOptions = {}) => {
792
+ if (!profileId) {
793
+ throw new QoniValidationError("openLogin response did not include a profileId to confirm");
794
+ }
795
+ const confirmBody = { site_id: siteId };
796
+ if (confirmOptions.displayLabel !== void 0) confirmBody.display_label = confirmOptions.displayLabel;
797
+ const response2 = await this.post(
798
+ {
799
+ kind: ActionKinds.openLogin,
800
+ label: "Confirm login",
801
+ method: "POST",
802
+ endpoint: `/profiles/${encodeURIComponent(profileId)}/confirm`
803
+ },
804
+ confirmBody,
805
+ { requiredScopes: [QoniScopes.SITE_LOGIN_CONFIRM] }
806
+ );
807
+ const raw2 = isJsonObject(response2) ? response2 : {};
808
+ const fields2 = camelizeRecord(raw2);
809
+ const state = asString2(fields2.state) ?? "";
810
+ return {
811
+ profileId: asString2(fields2.profileId) ?? profileId,
812
+ active: state === "active",
813
+ state,
814
+ probeResult: asString2(fields2.probeResult) ?? "",
815
+ raw: raw2
816
+ };
817
+ }
818
+ };
681
819
  }
682
820
  /** Tell the agent the user has finished signing in. */
683
- confirmSignedIn() {
684
- return this.invoke(ActionKinds.confirmSignedIn);
821
+ async confirmSignedIn() {
822
+ await this.invoke(ActionKinds.confirmSignedIn);
685
823
  }
686
824
  /** Connect to the live browser for a take_control interaction. */
687
- connectControl() {
688
- return this.invoke(ActionKinds.connectControl);
825
+ async connectControl() {
826
+ await this.invoke(ActionKinds.connectControl);
689
827
  }
690
828
  /** Refresh the live control surface / live url. */
691
- refreshControl() {
692
- return this.invoke(ActionKinds.refreshControl);
829
+ async refreshControl() {
830
+ await this.invoke(ActionKinds.refreshControl);
693
831
  }
694
832
  /** Hand control back to the agent. */
695
- releaseControl() {
696
- return this.invoke(ActionKinds.releaseControl);
833
+ async releaseControl() {
834
+ await this.invoke(ActionKinds.releaseControl);
697
835
  }
698
836
  /** Retry a wait interaction now. */
699
- retry() {
700
- return this.invoke(ActionKinds.retry);
837
+ async retry() {
838
+ await this.invoke(ActionKinds.retry);
701
839
  }
702
840
  /** Switch the profile a wait/login interaction is bound to. */
703
- switchProfile() {
704
- return this.invoke(ActionKinds.switchProfile);
841
+ async switchProfile() {
842
+ await this.invoke(ActionKinds.switchProfile);
843
+ }
844
+ /** Resolve the site for `openLogin`: explicit, or the only listed site. */
845
+ #loginSiteId(requested) {
846
+ const payload = this.interaction.payload;
847
+ const listed = Array.isArray(payload?.sites) ? payload.sites.map((site) => site.siteId).filter((id) => typeof id === "string" && id.length > 0) : [];
848
+ if (requested !== void 0) {
849
+ if (typeof requested !== "string" || !requested) {
850
+ throw new QoniValidationError("openLogin siteId must be a non-empty string");
851
+ }
852
+ if (listed.length > 0 && !listed.includes(requested)) {
853
+ throw new QoniValidationError(
854
+ `openLogin siteId "${requested}" is not one of the sites this interaction asks for: ${listed.join(", ")}`
855
+ );
856
+ }
857
+ return requested;
858
+ }
859
+ if (listed.length === 1) return listed[0];
860
+ throw new QoniValidationError(
861
+ listed.length === 0 ? "openLogin needs a siteId: this interaction does not list the site to sign in to" : `openLogin needs a siteId: choose one of ${listed.join(", ")}`
862
+ );
705
863
  }
706
864
  /**
707
865
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -709,6 +867,10 @@ var InteractionHandle = class {
709
867
  * method contract uniformly promise-based.
710
868
  */
711
869
  async invoke(kind, body) {
870
+ return this.post(this.#declaredAction(kind), body);
871
+ }
872
+ /** The declared action of `kind`; throws when the backend did not offer it. */
873
+ #declaredAction(kind) {
712
874
  const action = this.interaction.actions.find((a) => a.kind === kind);
713
875
  if (!action) {
714
876
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -716,7 +878,7 @@ var InteractionHandle = class {
716
878
  `interaction "${this.interaction.id}" (${this.interaction.type}) does not offer the "${kind}" action \u2014 the backend declared: ${offered}. Only call methods whose action the interaction currently offers.`
717
879
  );
718
880
  }
719
- await this.post(action, body);
881
+ return action;
720
882
  }
721
883
  };
722
884
 
@@ -786,7 +948,7 @@ var RunHandle = class {
786
948
  break settled;
787
949
  }
788
950
  }
789
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
951
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
790
952
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
791
953
  terminalDetail = detail2;
792
954
  break;
@@ -800,10 +962,10 @@ var RunHandle = class {
800
962
  sawCleanClose = true;
801
963
  eventIdAtLastClose = lastEventId;
802
964
  }
803
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
965
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
804
966
  this.#adoptSessionRef(detail);
805
967
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
806
- result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
968
+ result.artifacts = this.ops.listArtifacts ? await settleRead(() => this.ops.listArtifacts(signal), signal) : [];
807
969
  return result;
808
970
  } catch (err) {
809
971
  if (timedOut() && !opts.signal?.aborted) {
@@ -826,7 +988,7 @@ var RunHandle = class {
826
988
  interactionHandle(interaction) {
827
989
  return new InteractionHandle(
828
990
  interaction,
829
- (action, body) => this.ops.postAction(action, body)
991
+ (action, body, options) => this.ops.postAction(action, body, options)
830
992
  );
831
993
  }
832
994
  /**
@@ -920,6 +1082,35 @@ function withTimeout(signal, timeoutMs) {
920
1082
  }
921
1083
  return { signal: controller.signal, timedOut: () => fired, dispose };
922
1084
  }
1085
+ var SETTLE_READ_BACKOFF_MS = [250, 1e3];
1086
+ async function settleRead(read, signal) {
1087
+ for (let attempt = 0; ; attempt++) {
1088
+ try {
1089
+ return await waitWithSignal(read, signal);
1090
+ } catch (err) {
1091
+ const delay = SETTLE_READ_BACKOFF_MS[attempt];
1092
+ if (delay === void 0 || signal?.aborted || !(err instanceof QoniError) || !err.retryable) throw err;
1093
+ await sleepWithSignal(delay, signal);
1094
+ }
1095
+ }
1096
+ }
1097
+ function sleepWithSignal(ms, signal) {
1098
+ return new Promise((resolve, reject) => {
1099
+ if (signal?.aborted) {
1100
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1101
+ return;
1102
+ }
1103
+ const onAbort = () => {
1104
+ clearTimeout(timer);
1105
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1106
+ };
1107
+ const timer = setTimeout(() => {
1108
+ signal?.removeEventListener("abort", onAbort);
1109
+ resolve();
1110
+ }, ms);
1111
+ signal?.addEventListener("abort", onAbort, { once: true });
1112
+ });
1113
+ }
923
1114
  function waitWithSignal(operation, signal) {
924
1115
  if (!signal) return Promise.resolve().then(operation);
925
1116
  return new Promise((resolve, reject) => {
@@ -1017,17 +1208,15 @@ function createDeepResearchNamespace(transport) {
1017
1208
  signal: opts.signal,
1018
1209
  onReconnect: opts.onReconnect
1019
1210
  }),
1020
- postAction: async (action, body) => {
1021
- await transport.webAgentJson(
1022
- action.method || "POST",
1023
- action.endpoint,
1024
- token,
1025
- {
1026
- ...body !== void 0 ? { body } : {},
1027
- requiredScopes: ["webagent.deep_research:manage"]
1028
- }
1029
- );
1030
- },
1211
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1212
+ action.method || "POST",
1213
+ action.endpoint,
1214
+ token,
1215
+ {
1216
+ ...body !== void 0 ? { body } : {},
1217
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.deep_research:manage")
1218
+ }
1219
+ )).data,
1031
1220
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
1032
1221
  listArtifacts: async (signal) => {
1033
1222
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1292,17 +1481,15 @@ function createDoAnythingNamespace(transport) {
1292
1481
  // Skip screenshot rows in backfill when the caller did not opt in.
1293
1482
  includeScreenshots: capture?.screenshots ? void 0 : false
1294
1483
  }),
1295
- postAction: async (action, body) => {
1296
- await transport.webAgentJson(
1297
- action.method || "POST",
1298
- action.endpoint,
1299
- token,
1300
- {
1301
- ...body !== void 0 ? { body } : {},
1302
- requiredScopes: ["webagent.do_anything:manage"]
1303
- }
1304
- );
1305
- },
1484
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1485
+ action.method || "POST",
1486
+ action.endpoint,
1487
+ token,
1488
+ {
1489
+ ...body !== void 0 ? { body } : {},
1490
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.do_anything:manage")
1491
+ }
1492
+ )).data,
1306
1493
  cancel: async (reason) => asRecord2(
1307
1494
  (await api.cancel({
1308
1495
  token,
@@ -1423,81 +1610,6 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1423
1610
  };
1424
1611
  }
1425
1612
 
1426
- // src/scopes.ts
1427
- var QoniScopes = {
1428
- GUMEM_SESSION_CREATE: "gumem.session:create",
1429
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1430
- GUMEM_MEMORY_READ: "gumem.memory:read",
1431
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1432
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1433
- GUMEM_SEARCH_RUN: "gumem.search:run",
1434
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1435
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1436
- GUMEM_ACTION_WRITE: "gumem.action:write",
1437
- GUMEM_ACTION_READ: "gumem.action:read",
1438
- GUMEM_PROFILE_READ: "gumem.profile:read",
1439
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1440
- // `manage` (anything that changes execution state). See
1441
- // wa/docs/qoni-sdk-public-surface.md §3.
1442
- DO_ANYTHING_READ: "webagent.do_anything:read",
1443
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1444
- WEB_SEARCH_READ: "webagent.web_search:read",
1445
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1446
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1447
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1448
- TRACK_READ: "webagent.track:read",
1449
- TRACK_MANAGE: "webagent.track:manage"
1450
- };
1451
- var QoniProductScopes = {
1452
- doAnything: [
1453
- QoniScopes.DO_ANYTHING_READ,
1454
- QoniScopes.DO_ANYTHING_MANAGE
1455
- ],
1456
- webSearch: [
1457
- QoniScopes.WEB_SEARCH_READ,
1458
- QoniScopes.WEB_SEARCH_MANAGE
1459
- ],
1460
- deepResearch: [
1461
- QoniScopes.DEEP_RESEARCH_READ,
1462
- QoniScopes.DEEP_RESEARCH_MANAGE
1463
- ],
1464
- track: [
1465
- QoniScopes.TRACK_READ,
1466
- QoniScopes.TRACK_MANAGE
1467
- ]
1468
- };
1469
- var KNOWN_SCOPES = Object.values(QoniScopes);
1470
- var QoniScopeBundles = {
1471
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1472
- GUMEM_SESSION_RECALL: [
1473
- QoniScopes.GUMEM_MEMORY_READ,
1474
- QoniScopes.GUMEM_MEMORY_WRITE,
1475
- QoniScopes.GUMEM_MESSAGE_WRITE
1476
- ],
1477
- GUMEM_WRITE: [
1478
- QoniScopes.GUMEM_SESSION_CREATE,
1479
- QoniScopes.GUMEM_MESSAGE_WRITE,
1480
- QoniScopes.GUMEM_ACTION_WRITE
1481
- ],
1482
- // One bundle per WebAgent product = [read, manage].
1483
- DO_ANYTHING: [
1484
- QoniScopes.DO_ANYTHING_READ,
1485
- QoniScopes.DO_ANYTHING_MANAGE
1486
- ],
1487
- WEB_SEARCH: [
1488
- QoniScopes.WEB_SEARCH_READ,
1489
- QoniScopes.WEB_SEARCH_MANAGE
1490
- ],
1491
- DEEP_RESEARCH: [
1492
- QoniScopes.DEEP_RESEARCH_READ,
1493
- QoniScopes.DEEP_RESEARCH_MANAGE
1494
- ],
1495
- TRACK: [
1496
- QoniScopes.TRACK_READ,
1497
- QoniScopes.TRACK_MANAGE
1498
- ]
1499
- };
1500
-
1501
1613
  // src/genauth.ts
1502
1614
  function createGenAuthNamespace(transport, options = {}) {
1503
1615
  return {
@@ -1696,7 +1808,7 @@ var MonitorHandle = class {
1696
1808
  id;
1697
1809
  /** Refresh and return the monitor (normalized camelCase fields). */
1698
1810
  async get() {
1699
- return camelizeRecord(await this.ops.get());
1811
+ return normalizeMonitorRecord(await this.ops.get());
1700
1812
  }
1701
1813
  /**
1702
1814
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1704,11 +1816,11 @@ var MonitorHandle = class {
1704
1816
  * deleting.
1705
1817
  */
1706
1818
  async pause() {
1707
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1819
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1708
1820
  }
1709
1821
  /** Resume a paused monitor. */
1710
1822
  async resume() {
1711
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1823
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1712
1824
  }
1713
1825
  /**
1714
1826
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1726,11 +1838,11 @@ var MonitorHandle = class {
1726
1838
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1727
1839
  );
1728
1840
  }
1729
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1841
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1730
1842
  }
1731
1843
  /** Trigger an immediate tick outside the schedule. */
1732
1844
  async runNow() {
1733
- return camelizeRecord(await this.ops.runNow());
1845
+ return normalizeMonitorRecord(await this.ops.runNow());
1734
1846
  }
1735
1847
  /**
1736
1848
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1751,7 +1863,7 @@ var MonitorHandle = class {
1751
1863
  interactionHandle(interaction) {
1752
1864
  return new InteractionHandle(
1753
1865
  interaction,
1754
- (action, body) => this.ops.postAction(action, body)
1866
+ (action, body, options) => this.ops.postAction(action, body, options)
1755
1867
  );
1756
1868
  }
1757
1869
  /**
@@ -1856,17 +1968,15 @@ function createTrackNamespace(transport) {
1856
1968
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1857
1969
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1858
1970
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1859
- postAction: async (action, body) => {
1860
- await transport.webAgentJson(
1861
- action.method || "POST",
1862
- action.endpoint,
1863
- token,
1864
- {
1865
- ...body !== void 0 ? { body } : {},
1866
- requiredScopes: ["webagent.track:manage"]
1867
- }
1868
- );
1869
- },
1971
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1972
+ action.method || "POST",
1973
+ action.endpoint,
1974
+ token,
1975
+ {
1976
+ ...body !== void 0 ? { body } : {},
1977
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.track:manage")
1978
+ }
1979
+ )).data,
1870
1980
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1871
1981
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1872
1982
  delete: async () => {
@@ -1881,10 +1991,12 @@ function createTrackNamespace(transport) {
1881
1991
  if (!prompt || typeof prompt !== "string") {
1882
1992
  throw new QoniValidationError("track.create requires a prompt string");
1883
1993
  }
1994
+ const body = snakeifyKeys(rest);
1995
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1884
1996
  const created = await api.createMonitor({
1885
1997
  token,
1886
1998
  intent: prompt,
1887
- ...snakeifyKeys(rest)
1999
+ ...body
1888
2000
  });
1889
2001
  const monitorId = created.data.id || created.data.monitor_id;
1890
2002
  if (!monitorId) {
@@ -1902,7 +2014,7 @@ function createTrackNamespace(transport) {
1902
2014
  };
1903
2015
  }
1904
2016
  function normalizeMonitorRun(item) {
1905
- const normalized = camelizeRecord(item);
2017
+ const normalized = normalizeMonitorRecord(item);
1906
2018
  if (typeof normalized.id !== "string" || !normalized.id) {
1907
2019
  const id = normalized.runId ?? normalized.snapshotId;
1908
2020
  if (typeof id === "string" && id) normalized.id = id;
@@ -1912,11 +2024,44 @@ function normalizeMonitorRun(item) {
1912
2024
  function normalizeTrackInterveneInput(input) {
1913
2025
  return renameKeys4(input, { requestId: "request_id" });
1914
2026
  }
2027
+ var USER_KEYED_FIELDS = /* @__PURE__ */ new Set(["extraction_schema", "baseline_extracted", "extracted"]);
2028
+ var USER_KEYED_DIFF_FIELDS = /* @__PURE__ */ new Set(["before", "after", "added", "removed", "sample_unchanged"]);
2029
+ function normalizeMonitorRecord(value) {
2030
+ const out = {};
2031
+ for (const [key, item] of Object.entries(value)) {
2032
+ if (USER_KEYED_FIELDS.has(key)) out[camelKey2(key)] = item;
2033
+ else if (key === "diffs" && Array.isArray(item)) out.diffs = item.map(normalizeDiff);
2034
+ else out[camelKey2(key)] = camelizeKeys(item);
2035
+ }
2036
+ return out;
2037
+ }
2038
+ function normalizeDiff(entry) {
2039
+ if (!isJsonObject(entry)) return camelizeKeys(entry);
2040
+ const out = {};
2041
+ for (const [key, item] of Object.entries(entry)) {
2042
+ out[camelKey2(key)] = USER_KEYED_DIFF_FIELDS.has(key) ? item : camelizeKeys(item);
2043
+ }
2044
+ return out;
2045
+ }
2046
+ function camelKey2(key) {
2047
+ return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
2048
+ }
1915
2049
  function normalizeSchedule(schedule) {
2050
+ if (!isJsonObject(schedule)) return schedule;
1916
2051
  if (schedule.kind === "interval") {
1917
- return { kind: "interval", interval_seconds: schedule.intervalSeconds };
2052
+ const { intervalSeconds, interval_seconds, ...rest } = schedule;
2053
+ const seconds = intervalSeconds ?? interval_seconds;
2054
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
2055
+ throw new QoniValidationError(
2056
+ 'track interval schedule needs a positive number: { kind: "interval", intervalSeconds }'
2057
+ );
2058
+ }
2059
+ return { ...rest, interval_seconds: seconds };
2060
+ }
2061
+ if (schedule.kind === "cron" && (typeof schedule.expr !== "string" || !schedule.expr.trim())) {
2062
+ throw new QoniValidationError('track cron schedule needs an expression: { kind: "cron", expr }');
1918
2063
  }
1919
- return { kind: "cron", expr: schedule.expr };
2064
+ return schedule;
1920
2065
  }
1921
2066
  function snakeifyKeys(value) {
1922
2067
  const out = {};