@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.js CHANGED
@@ -490,6 +490,87 @@ var ActionKinds = {
490
490
  };
491
491
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
492
492
 
493
+ // src/scopes.ts
494
+ var QoniScopes = {
495
+ GUMEM_SESSION_CREATE: "gumem.session:create",
496
+ GUMEM_MESSAGE_WRITE: "gumem.message:write",
497
+ GUMEM_MEMORY_READ: "gumem.memory:read",
498
+ GUMEM_MEMORY_WRITE: "gumem.memory:write",
499
+ GUMEM_MEMORY_DELETE: "gumem.memory:delete",
500
+ GUMEM_SEARCH_RUN: "gumem.search:run",
501
+ GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
502
+ GUMEM_RESOURCE_WRITE: "gumem.resource:write",
503
+ GUMEM_ACTION_WRITE: "gumem.action:write",
504
+ GUMEM_ACTION_READ: "gumem.action:read",
505
+ GUMEM_PROFILE_READ: "gumem.profile:read",
506
+ // WebAgent products: exactly two verbs per product — `read` (observe) and
507
+ // `manage` (anything that changes execution state). See
508
+ // wa/docs/qoni-sdk-public-surface.md §3.
509
+ DO_ANYTHING_READ: "webagent.do_anything:read",
510
+ DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
511
+ WEB_SEARCH_READ: "webagent.web_search:read",
512
+ WEB_SEARCH_MANAGE: "webagent.web_search:manage",
513
+ DEEP_RESEARCH_READ: "webagent.deep_research:read",
514
+ DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
515
+ TRACK_READ: "webagent.track:read",
516
+ TRACK_MANAGE: "webagent.track:manage",
517
+ // Interactive site login (`InteractionHandle.openLogin()` on a `site_login`
518
+ // interaction). Not part of any product bundle: the AccessKey policy must
519
+ // allow it, and the delegation must request it explicitly.
520
+ SITE_LOGIN_REQUEST: "webagent.site_login:request",
521
+ // Persist that login into the browser profile (`OpenLoginResult.confirm()`).
522
+ SITE_LOGIN_CONFIRM: "webagent.site_login:confirm"
523
+ };
524
+ var QoniProductScopes = {
525
+ doAnything: [
526
+ QoniScopes.DO_ANYTHING_READ,
527
+ QoniScopes.DO_ANYTHING_MANAGE
528
+ ],
529
+ webSearch: [
530
+ QoniScopes.WEB_SEARCH_READ,
531
+ QoniScopes.WEB_SEARCH_MANAGE
532
+ ],
533
+ deepResearch: [
534
+ QoniScopes.DEEP_RESEARCH_READ,
535
+ QoniScopes.DEEP_RESEARCH_MANAGE
536
+ ],
537
+ track: [
538
+ QoniScopes.TRACK_READ,
539
+ QoniScopes.TRACK_MANAGE
540
+ ]
541
+ };
542
+ var KNOWN_SCOPES = Object.values(QoniScopes);
543
+ var QoniScopeBundles = {
544
+ GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
545
+ GUMEM_SESSION_RECALL: [
546
+ QoniScopes.GUMEM_MEMORY_READ,
547
+ QoniScopes.GUMEM_MEMORY_WRITE,
548
+ QoniScopes.GUMEM_MESSAGE_WRITE
549
+ ],
550
+ GUMEM_WRITE: [
551
+ QoniScopes.GUMEM_SESSION_CREATE,
552
+ QoniScopes.GUMEM_MESSAGE_WRITE,
553
+ QoniScopes.GUMEM_ACTION_WRITE
554
+ ],
555
+ // One bundle per WebAgent product = [read, manage].
556
+ DO_ANYTHING: [
557
+ QoniScopes.DO_ANYTHING_READ,
558
+ QoniScopes.DO_ANYTHING_MANAGE
559
+ ],
560
+ WEB_SEARCH: [
561
+ QoniScopes.WEB_SEARCH_READ,
562
+ QoniScopes.WEB_SEARCH_MANAGE
563
+ ],
564
+ DEEP_RESEARCH: [
565
+ QoniScopes.DEEP_RESEARCH_READ,
566
+ QoniScopes.DEEP_RESEARCH_MANAGE
567
+ ],
568
+ TRACK: [
569
+ QoniScopes.TRACK_READ,
570
+ QoniScopes.TRACK_MANAGE
571
+ ]
572
+ };
573
+
493
574
  // src/interactions.ts
494
575
  function asString2(value) {
495
576
  return typeof value === "string" && value ? value : void 0;
@@ -588,6 +669,9 @@ function normalizeInteraction(payload) {
588
669
  if (evidence) interaction.evidence = evidence;
589
670
  return interaction;
590
671
  }
672
+ function interactionActionScopes(action, productManageScope) {
673
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
674
+ }
591
675
  var InteractionHandle = class {
592
676
  constructor(interaction, post) {
593
677
  this.post = post;
@@ -612,48 +696,122 @@ var InteractionHandle = class {
612
696
  return this.interaction.actions.some((a) => a.kind === kind);
613
697
  }
614
698
  /** Answer a clarification ask. */
615
- answer(text) {
616
- return this.invoke(ActionKinds.answer, { response: text });
699
+ async answer(text) {
700
+ await this.invoke(ActionKinds.answer, { response: text });
617
701
  }
618
702
  /** Skip / dismiss the interaction (proceed without acting on it). */
619
- skip() {
620
- return this.invoke(ActionKinds.skip);
703
+ async skip() {
704
+ await this.invoke(ActionKinds.skip);
621
705
  }
622
706
  /** Confirm a pending confirmation (approve the plan / destructive op). */
623
- confirm() {
624
- return this.invoke(ActionKinds.confirm);
707
+ async confirm() {
708
+ await this.invoke(ActionKinds.confirm);
625
709
  }
626
710
  /** Reject a pending confirmation. */
627
- reject() {
628
- return this.invoke(ActionKinds.reject);
711
+ async reject() {
712
+ await this.invoke(ActionKinds.reject);
629
713
  }
630
- /** Open the login surface for a site_login interaction. */
631
- openLogin() {
632
- return this.invoke(ActionKinds.openLogin);
714
+ /**
715
+ * Start an interactive login session for a `site_login` interaction. The
716
+ * backend needs the site to sign in to: pass `siteId`, or omit it when the
717
+ * interaction lists exactly one site. Show `liveUrl` to the user; when they
718
+ * finish, call `confirm()` on the result to save the login, and act on its
719
+ * outcome (see {@link OpenLoginResult.confirm}) before {@link confirmSignedIn}.
720
+ * The delegation must include `QoniScopes.SITE_LOGIN_REQUEST` and
721
+ * `QoniScopes.SITE_LOGIN_CONFIRM`; the AccessKey policy must allow both.
722
+ */
723
+ async openLogin(options = {}) {
724
+ this.#declaredAction(ActionKinds.openLogin);
725
+ const body = { site_id: this.#loginSiteId(options.siteId) };
726
+ if (options.profileId !== void 0) {
727
+ if (typeof options.profileId !== "string" || !options.profileId) {
728
+ throw new QoniValidationError("openLogin profileId must be a non-empty string");
729
+ }
730
+ body.profile_id = options.profileId;
731
+ }
732
+ const response = await this.invoke(ActionKinds.openLogin, body);
733
+ const raw = isJsonObject(response) ? response : {};
734
+ const fields = camelizeRecord(raw);
735
+ const profileId = asString2(fields.profileId) ?? "";
736
+ const siteId = asString2(fields.siteId) ?? String(body.site_id);
737
+ return {
738
+ profileId,
739
+ siteId,
740
+ sessionId: asString2(fields.sessionId) ?? "",
741
+ liveUrl: asString2(fields.liveUrl) ?? "",
742
+ raw,
743
+ confirm: async (confirmOptions = {}) => {
744
+ if (!profileId) {
745
+ throw new QoniValidationError("openLogin response did not include a profileId to confirm");
746
+ }
747
+ const confirmBody = { site_id: siteId };
748
+ if (confirmOptions.displayLabel !== void 0) confirmBody.display_label = confirmOptions.displayLabel;
749
+ const response2 = await this.post(
750
+ {
751
+ kind: ActionKinds.openLogin,
752
+ label: "Confirm login",
753
+ method: "POST",
754
+ endpoint: `/profiles/${encodeURIComponent(profileId)}/confirm`
755
+ },
756
+ confirmBody,
757
+ { requiredScopes: [QoniScopes.SITE_LOGIN_CONFIRM] }
758
+ );
759
+ const raw2 = isJsonObject(response2) ? response2 : {};
760
+ const fields2 = camelizeRecord(raw2);
761
+ const state = asString2(fields2.state) ?? "";
762
+ return {
763
+ profileId: asString2(fields2.profileId) ?? profileId,
764
+ active: state === "active",
765
+ state,
766
+ probeResult: asString2(fields2.probeResult) ?? "",
767
+ raw: raw2
768
+ };
769
+ }
770
+ };
633
771
  }
634
772
  /** Tell the agent the user has finished signing in. */
635
- confirmSignedIn() {
636
- return this.invoke(ActionKinds.confirmSignedIn);
773
+ async confirmSignedIn() {
774
+ await this.invoke(ActionKinds.confirmSignedIn);
637
775
  }
638
776
  /** Connect to the live browser for a take_control interaction. */
639
- connectControl() {
640
- return this.invoke(ActionKinds.connectControl);
777
+ async connectControl() {
778
+ await this.invoke(ActionKinds.connectControl);
641
779
  }
642
780
  /** Refresh the live control surface / live url. */
643
- refreshControl() {
644
- return this.invoke(ActionKinds.refreshControl);
781
+ async refreshControl() {
782
+ await this.invoke(ActionKinds.refreshControl);
645
783
  }
646
784
  /** Hand control back to the agent. */
647
- releaseControl() {
648
- return this.invoke(ActionKinds.releaseControl);
785
+ async releaseControl() {
786
+ await this.invoke(ActionKinds.releaseControl);
649
787
  }
650
788
  /** Retry a wait interaction now. */
651
- retry() {
652
- return this.invoke(ActionKinds.retry);
789
+ async retry() {
790
+ await this.invoke(ActionKinds.retry);
653
791
  }
654
792
  /** Switch the profile a wait/login interaction is bound to. */
655
- switchProfile() {
656
- return this.invoke(ActionKinds.switchProfile);
793
+ async switchProfile() {
794
+ await this.invoke(ActionKinds.switchProfile);
795
+ }
796
+ /** Resolve the site for `openLogin`: explicit, or the only listed site. */
797
+ #loginSiteId(requested) {
798
+ const payload = this.interaction.payload;
799
+ const listed = Array.isArray(payload?.sites) ? payload.sites.map((site) => site.siteId).filter((id) => typeof id === "string" && id.length > 0) : [];
800
+ if (requested !== void 0) {
801
+ if (typeof requested !== "string" || !requested) {
802
+ throw new QoniValidationError("openLogin siteId must be a non-empty string");
803
+ }
804
+ if (listed.length > 0 && !listed.includes(requested)) {
805
+ throw new QoniValidationError(
806
+ `openLogin siteId "${requested}" is not one of the sites this interaction asks for: ${listed.join(", ")}`
807
+ );
808
+ }
809
+ return requested;
810
+ }
811
+ if (listed.length === 1) return listed[0];
812
+ throw new QoniValidationError(
813
+ 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(", ")}`
814
+ );
657
815
  }
658
816
  /**
659
817
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -661,6 +819,10 @@ var InteractionHandle = class {
661
819
  * method contract uniformly promise-based.
662
820
  */
663
821
  async invoke(kind, body) {
822
+ return this.post(this.#declaredAction(kind), body);
823
+ }
824
+ /** The declared action of `kind`; throws when the backend did not offer it. */
825
+ #declaredAction(kind) {
664
826
  const action = this.interaction.actions.find((a) => a.kind === kind);
665
827
  if (!action) {
666
828
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -668,7 +830,7 @@ var InteractionHandle = class {
668
830
  `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.`
669
831
  );
670
832
  }
671
- await this.post(action, body);
833
+ return action;
672
834
  }
673
835
  };
674
836
 
@@ -738,7 +900,7 @@ var RunHandle = class {
738
900
  break settled;
739
901
  }
740
902
  }
741
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
903
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
742
904
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
743
905
  terminalDetail = detail2;
744
906
  break;
@@ -752,10 +914,10 @@ var RunHandle = class {
752
914
  sawCleanClose = true;
753
915
  eventIdAtLastClose = lastEventId;
754
916
  }
755
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
917
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
756
918
  this.#adoptSessionRef(detail);
757
919
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
758
- result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
920
+ result.artifacts = this.ops.listArtifacts ? await settleRead(() => this.ops.listArtifacts(signal), signal) : [];
759
921
  return result;
760
922
  } catch (err) {
761
923
  if (timedOut() && !opts.signal?.aborted) {
@@ -778,7 +940,7 @@ var RunHandle = class {
778
940
  interactionHandle(interaction) {
779
941
  return new InteractionHandle(
780
942
  interaction,
781
- (action, body) => this.ops.postAction(action, body)
943
+ (action, body, options) => this.ops.postAction(action, body, options)
782
944
  );
783
945
  }
784
946
  /**
@@ -872,6 +1034,35 @@ function withTimeout(signal, timeoutMs) {
872
1034
  }
873
1035
  return { signal: controller.signal, timedOut: () => fired, dispose };
874
1036
  }
1037
+ var SETTLE_READ_BACKOFF_MS = [250, 1e3];
1038
+ async function settleRead(read, signal) {
1039
+ for (let attempt = 0; ; attempt++) {
1040
+ try {
1041
+ return await waitWithSignal(read, signal);
1042
+ } catch (err) {
1043
+ const delay = SETTLE_READ_BACKOFF_MS[attempt];
1044
+ if (delay === void 0 || signal?.aborted || !(err instanceof QoniError) || !err.retryable) throw err;
1045
+ await sleepWithSignal(delay, signal);
1046
+ }
1047
+ }
1048
+ }
1049
+ function sleepWithSignal(ms, signal) {
1050
+ return new Promise((resolve, reject) => {
1051
+ if (signal?.aborted) {
1052
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1053
+ return;
1054
+ }
1055
+ const onAbort = () => {
1056
+ clearTimeout(timer);
1057
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
1058
+ };
1059
+ const timer = setTimeout(() => {
1060
+ signal?.removeEventListener("abort", onAbort);
1061
+ resolve();
1062
+ }, ms);
1063
+ signal?.addEventListener("abort", onAbort, { once: true });
1064
+ });
1065
+ }
875
1066
  function waitWithSignal(operation, signal) {
876
1067
  if (!signal) return Promise.resolve().then(operation);
877
1068
  return new Promise((resolve, reject) => {
@@ -969,17 +1160,15 @@ function createDeepResearchNamespace(transport) {
969
1160
  signal: opts.signal,
970
1161
  onReconnect: opts.onReconnect
971
1162
  }),
972
- postAction: async (action, body) => {
973
- await transport.webAgentJson(
974
- action.method || "POST",
975
- action.endpoint,
976
- token,
977
- {
978
- ...body !== void 0 ? { body } : {},
979
- requiredScopes: ["webagent.deep_research:manage"]
980
- }
981
- );
982
- },
1163
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1164
+ action.method || "POST",
1165
+ action.endpoint,
1166
+ token,
1167
+ {
1168
+ ...body !== void 0 ? { body } : {},
1169
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.deep_research:manage")
1170
+ }
1171
+ )).data,
983
1172
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
984
1173
  listArtifacts: async (signal) => {
985
1174
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1244,17 +1433,15 @@ function createDoAnythingNamespace(transport) {
1244
1433
  // Skip screenshot rows in backfill when the caller did not opt in.
1245
1434
  includeScreenshots: capture?.screenshots ? void 0 : false
1246
1435
  }),
1247
- postAction: async (action, body) => {
1248
- await transport.webAgentJson(
1249
- action.method || "POST",
1250
- action.endpoint,
1251
- token,
1252
- {
1253
- ...body !== void 0 ? { body } : {},
1254
- requiredScopes: ["webagent.do_anything:manage"]
1255
- }
1256
- );
1257
- },
1436
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1437
+ action.method || "POST",
1438
+ action.endpoint,
1439
+ token,
1440
+ {
1441
+ ...body !== void 0 ? { body } : {},
1442
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.do_anything:manage")
1443
+ }
1444
+ )).data,
1258
1445
  cancel: async (reason) => asRecord2(
1259
1446
  (await api.cancel({
1260
1447
  token,
@@ -1375,81 +1562,6 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1375
1562
  };
1376
1563
  }
1377
1564
 
1378
- // src/scopes.ts
1379
- var QoniScopes = {
1380
- GUMEM_SESSION_CREATE: "gumem.session:create",
1381
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1382
- GUMEM_MEMORY_READ: "gumem.memory:read",
1383
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1384
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1385
- GUMEM_SEARCH_RUN: "gumem.search:run",
1386
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1387
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1388
- GUMEM_ACTION_WRITE: "gumem.action:write",
1389
- GUMEM_ACTION_READ: "gumem.action:read",
1390
- GUMEM_PROFILE_READ: "gumem.profile:read",
1391
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1392
- // `manage` (anything that changes execution state). See
1393
- // wa/docs/qoni-sdk-public-surface.md §3.
1394
- DO_ANYTHING_READ: "webagent.do_anything:read",
1395
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1396
- WEB_SEARCH_READ: "webagent.web_search:read",
1397
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1398
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1399
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1400
- TRACK_READ: "webagent.track:read",
1401
- TRACK_MANAGE: "webagent.track:manage"
1402
- };
1403
- var QoniProductScopes = {
1404
- doAnything: [
1405
- QoniScopes.DO_ANYTHING_READ,
1406
- QoniScopes.DO_ANYTHING_MANAGE
1407
- ],
1408
- webSearch: [
1409
- QoniScopes.WEB_SEARCH_READ,
1410
- QoniScopes.WEB_SEARCH_MANAGE
1411
- ],
1412
- deepResearch: [
1413
- QoniScopes.DEEP_RESEARCH_READ,
1414
- QoniScopes.DEEP_RESEARCH_MANAGE
1415
- ],
1416
- track: [
1417
- QoniScopes.TRACK_READ,
1418
- QoniScopes.TRACK_MANAGE
1419
- ]
1420
- };
1421
- var KNOWN_SCOPES = Object.values(QoniScopes);
1422
- var QoniScopeBundles = {
1423
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1424
- GUMEM_SESSION_RECALL: [
1425
- QoniScopes.GUMEM_MEMORY_READ,
1426
- QoniScopes.GUMEM_MEMORY_WRITE,
1427
- QoniScopes.GUMEM_MESSAGE_WRITE
1428
- ],
1429
- GUMEM_WRITE: [
1430
- QoniScopes.GUMEM_SESSION_CREATE,
1431
- QoniScopes.GUMEM_MESSAGE_WRITE,
1432
- QoniScopes.GUMEM_ACTION_WRITE
1433
- ],
1434
- // One bundle per WebAgent product = [read, manage].
1435
- DO_ANYTHING: [
1436
- QoniScopes.DO_ANYTHING_READ,
1437
- QoniScopes.DO_ANYTHING_MANAGE
1438
- ],
1439
- WEB_SEARCH: [
1440
- QoniScopes.WEB_SEARCH_READ,
1441
- QoniScopes.WEB_SEARCH_MANAGE
1442
- ],
1443
- DEEP_RESEARCH: [
1444
- QoniScopes.DEEP_RESEARCH_READ,
1445
- QoniScopes.DEEP_RESEARCH_MANAGE
1446
- ],
1447
- TRACK: [
1448
- QoniScopes.TRACK_READ,
1449
- QoniScopes.TRACK_MANAGE
1450
- ]
1451
- };
1452
-
1453
1565
  // src/genauth.ts
1454
1566
  function createGenAuthNamespace(transport, options = {}) {
1455
1567
  return {
@@ -1648,7 +1760,7 @@ var MonitorHandle = class {
1648
1760
  id;
1649
1761
  /** Refresh and return the monitor (normalized camelCase fields). */
1650
1762
  async get() {
1651
- return camelizeRecord(await this.ops.get());
1763
+ return normalizeMonitorRecord(await this.ops.get());
1652
1764
  }
1653
1765
  /**
1654
1766
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1656,11 +1768,11 @@ var MonitorHandle = class {
1656
1768
  * deleting.
1657
1769
  */
1658
1770
  async pause() {
1659
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1771
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1660
1772
  }
1661
1773
  /** Resume a paused monitor. */
1662
1774
  async resume() {
1663
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1775
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1664
1776
  }
1665
1777
  /**
1666
1778
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1678,11 +1790,11 @@ var MonitorHandle = class {
1678
1790
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1679
1791
  );
1680
1792
  }
1681
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1793
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1682
1794
  }
1683
1795
  /** Trigger an immediate tick outside the schedule. */
1684
1796
  async runNow() {
1685
- return camelizeRecord(await this.ops.runNow());
1797
+ return normalizeMonitorRecord(await this.ops.runNow());
1686
1798
  }
1687
1799
  /**
1688
1800
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1703,7 +1815,7 @@ var MonitorHandle = class {
1703
1815
  interactionHandle(interaction) {
1704
1816
  return new InteractionHandle(
1705
1817
  interaction,
1706
- (action, body) => this.ops.postAction(action, body)
1818
+ (action, body, options) => this.ops.postAction(action, body, options)
1707
1819
  );
1708
1820
  }
1709
1821
  /**
@@ -1808,17 +1920,15 @@ function createTrackNamespace(transport) {
1808
1920
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1809
1921
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1810
1922
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1811
- postAction: async (action, body) => {
1812
- await transport.webAgentJson(
1813
- action.method || "POST",
1814
- action.endpoint,
1815
- token,
1816
- {
1817
- ...body !== void 0 ? { body } : {},
1818
- requiredScopes: ["webagent.track:manage"]
1819
- }
1820
- );
1821
- },
1923
+ postAction: async (action, body, options) => (await transport.webAgentJson(
1924
+ action.method || "POST",
1925
+ action.endpoint,
1926
+ token,
1927
+ {
1928
+ ...body !== void 0 ? { body } : {},
1929
+ requiredScopes: options?.requiredScopes ?? interactionActionScopes(action, "webagent.track:manage")
1930
+ }
1931
+ )).data,
1822
1932
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1823
1933
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1824
1934
  delete: async () => {
@@ -1833,10 +1943,12 @@ function createTrackNamespace(transport) {
1833
1943
  if (!prompt || typeof prompt !== "string") {
1834
1944
  throw new QoniValidationError("track.create requires a prompt string");
1835
1945
  }
1946
+ const body = snakeifyKeys(rest);
1947
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1836
1948
  const created = await api.createMonitor({
1837
1949
  token,
1838
1950
  intent: prompt,
1839
- ...snakeifyKeys(rest)
1951
+ ...body
1840
1952
  });
1841
1953
  const monitorId = created.data.id || created.data.monitor_id;
1842
1954
  if (!monitorId) {
@@ -1854,7 +1966,7 @@ function createTrackNamespace(transport) {
1854
1966
  };
1855
1967
  }
1856
1968
  function normalizeMonitorRun(item) {
1857
- const normalized = camelizeRecord(item);
1969
+ const normalized = normalizeMonitorRecord(item);
1858
1970
  if (typeof normalized.id !== "string" || !normalized.id) {
1859
1971
  const id = normalized.runId ?? normalized.snapshotId;
1860
1972
  if (typeof id === "string" && id) normalized.id = id;
@@ -1864,11 +1976,44 @@ function normalizeMonitorRun(item) {
1864
1976
  function normalizeTrackInterveneInput(input) {
1865
1977
  return renameKeys4(input, { requestId: "request_id" });
1866
1978
  }
1979
+ var USER_KEYED_FIELDS = /* @__PURE__ */ new Set(["extraction_schema", "baseline_extracted", "extracted"]);
1980
+ var USER_KEYED_DIFF_FIELDS = /* @__PURE__ */ new Set(["before", "after", "added", "removed", "sample_unchanged"]);
1981
+ function normalizeMonitorRecord(value) {
1982
+ const out = {};
1983
+ for (const [key, item] of Object.entries(value)) {
1984
+ if (USER_KEYED_FIELDS.has(key)) out[camelKey2(key)] = item;
1985
+ else if (key === "diffs" && Array.isArray(item)) out.diffs = item.map(normalizeDiff);
1986
+ else out[camelKey2(key)] = camelizeKeys(item);
1987
+ }
1988
+ return out;
1989
+ }
1990
+ function normalizeDiff(entry) {
1991
+ if (!isJsonObject(entry)) return camelizeKeys(entry);
1992
+ const out = {};
1993
+ for (const [key, item] of Object.entries(entry)) {
1994
+ out[camelKey2(key)] = USER_KEYED_DIFF_FIELDS.has(key) ? item : camelizeKeys(item);
1995
+ }
1996
+ return out;
1997
+ }
1998
+ function camelKey2(key) {
1999
+ return key.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
2000
+ }
1867
2001
  function normalizeSchedule(schedule) {
2002
+ if (!isJsonObject(schedule)) return schedule;
1868
2003
  if (schedule.kind === "interval") {
1869
- return { kind: "interval", interval_seconds: schedule.intervalSeconds };
2004
+ const { intervalSeconds, interval_seconds, ...rest } = schedule;
2005
+ const seconds = intervalSeconds ?? interval_seconds;
2006
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
2007
+ throw new QoniValidationError(
2008
+ 'track interval schedule needs a positive number: { kind: "interval", intervalSeconds }'
2009
+ );
2010
+ }
2011
+ return { ...rest, interval_seconds: seconds };
2012
+ }
2013
+ if (schedule.kind === "cron" && (typeof schedule.expr !== "string" || !schedule.expr.trim())) {
2014
+ throw new QoniValidationError('track cron schedule needs an expression: { kind: "cron", expr }');
1870
2015
  }
1871
- return { kind: "cron", expr: schedule.expr };
2016
+ return schedule;
1872
2017
  }
1873
2018
  function snakeifyKeys(value) {
1874
2019
  const out = {};