@qoniai/qoni 0.5.2 → 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
@@ -278,6 +278,8 @@ var QoniEventTypes = {
278
278
  Interaction: "interaction",
279
279
  /** A live screenshot frame (opt in with `capture: { screenshots: true }`); image on `event.image`. */
280
280
  Screenshot: "screenshot",
281
+ /** The browser viewer moved to a new page or needs a fresh attachment. Use `event.data.liveUrl` for the live preview. */
282
+ BrowserLiveUrlChanged: "browserLiveUrlChanged",
281
283
  /** Terminal — the run finished; outcome + output in `event.data` (cost is on the `RunResult`). */
282
284
  Done: "done",
283
285
  // --- Web Search ---
@@ -408,7 +410,6 @@ var WIRE_TO_SEMANTIC = {
408
410
  "run.recording_started": "progress",
409
411
  "run.recording_paused": "progress",
410
412
  "run.recording_finalized": "progress",
411
- "run.browser_live_url_changed": "progress",
412
413
  // side-channel
413
414
  "run.feedback_submitted": "progress"
414
415
  };
@@ -470,6 +471,18 @@ function normalizeRunEvent(wire, options = {}) {
470
471
  return { ...base, type: "screenshot", data: sData, image, isTerminal: false };
471
472
  }
472
473
  }
474
+ if (wireType === "run.browser_live_url_changed") {
475
+ const liveUrl = asString(c.liveUrl);
476
+ if (!liveUrl) return null;
477
+ const data2 = {
478
+ liveUrl,
479
+ supportsLivePump: c.supportsLivePump === true,
480
+ forceViewerReload: c.forceViewerReload === true
481
+ };
482
+ const providerSessionId = asString(c.providerSessionId);
483
+ if (providerSessionId) data2.providerSessionId = providerSessionId;
484
+ return { ...base, type: "browserLiveUrlChanged", data: data2, isTerminal: false };
485
+ }
473
486
  if (wireType === INTERACTION) {
474
487
  const interaction = normalizeInteraction(payload);
475
488
  if (interaction) {
@@ -525,6 +538,87 @@ var ActionKinds = {
525
538
  };
526
539
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
527
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
+
528
622
  // src/interactions.ts
529
623
  function asString2(value) {
530
624
  return typeof value === "string" && value ? value : void 0;
@@ -623,6 +717,9 @@ function normalizeInteraction(payload) {
623
717
  if (evidence) interaction.evidence = evidence;
624
718
  return interaction;
625
719
  }
720
+ function interactionActionScopes(action, productManageScope) {
721
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
722
+ }
626
723
  var InteractionHandle = class {
627
724
  constructor(interaction, post) {
628
725
  this.post = post;
@@ -647,48 +744,122 @@ var InteractionHandle = class {
647
744
  return this.interaction.actions.some((a) => a.kind === kind);
648
745
  }
649
746
  /** Answer a clarification ask. */
650
- answer(text) {
651
- return this.invoke(ActionKinds.answer, { response: text });
747
+ async answer(text) {
748
+ await this.invoke(ActionKinds.answer, { response: text });
652
749
  }
653
750
  /** Skip / dismiss the interaction (proceed without acting on it). */
654
- skip() {
655
- return this.invoke(ActionKinds.skip);
751
+ async skip() {
752
+ await this.invoke(ActionKinds.skip);
656
753
  }
657
754
  /** Confirm a pending confirmation (approve the plan / destructive op). */
658
- confirm() {
659
- return this.invoke(ActionKinds.confirm);
755
+ async confirm() {
756
+ await this.invoke(ActionKinds.confirm);
660
757
  }
661
758
  /** Reject a pending confirmation. */
662
- reject() {
663
- return this.invoke(ActionKinds.reject);
759
+ async reject() {
760
+ await this.invoke(ActionKinds.reject);
664
761
  }
665
- /** Open the login surface for a site_login interaction. */
666
- openLogin() {
667
- 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
+ };
668
819
  }
669
820
  /** Tell the agent the user has finished signing in. */
670
- confirmSignedIn() {
671
- return this.invoke(ActionKinds.confirmSignedIn);
821
+ async confirmSignedIn() {
822
+ await this.invoke(ActionKinds.confirmSignedIn);
672
823
  }
673
824
  /** Connect to the live browser for a take_control interaction. */
674
- connectControl() {
675
- return this.invoke(ActionKinds.connectControl);
825
+ async connectControl() {
826
+ await this.invoke(ActionKinds.connectControl);
676
827
  }
677
828
  /** Refresh the live control surface / live url. */
678
- refreshControl() {
679
- return this.invoke(ActionKinds.refreshControl);
829
+ async refreshControl() {
830
+ await this.invoke(ActionKinds.refreshControl);
680
831
  }
681
832
  /** Hand control back to the agent. */
682
- releaseControl() {
683
- return this.invoke(ActionKinds.releaseControl);
833
+ async releaseControl() {
834
+ await this.invoke(ActionKinds.releaseControl);
684
835
  }
685
836
  /** Retry a wait interaction now. */
686
- retry() {
687
- return this.invoke(ActionKinds.retry);
837
+ async retry() {
838
+ await this.invoke(ActionKinds.retry);
688
839
  }
689
840
  /** Switch the profile a wait/login interaction is bound to. */
690
- switchProfile() {
691
- 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
+ );
692
863
  }
693
864
  /**
694
865
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -696,6 +867,10 @@ var InteractionHandle = class {
696
867
  * method contract uniformly promise-based.
697
868
  */
698
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) {
699
874
  const action = this.interaction.actions.find((a) => a.kind === kind);
700
875
  if (!action) {
701
876
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -703,7 +878,7 @@ var InteractionHandle = class {
703
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.`
704
879
  );
705
880
  }
706
- await this.post(action, body);
881
+ return action;
707
882
  }
708
883
  };
709
884
 
@@ -773,7 +948,7 @@ var RunHandle = class {
773
948
  break settled;
774
949
  }
775
950
  }
776
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
951
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
777
952
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
778
953
  terminalDetail = detail2;
779
954
  break;
@@ -787,10 +962,10 @@ var RunHandle = class {
787
962
  sawCleanClose = true;
788
963
  eventIdAtLastClose = lastEventId;
789
964
  }
790
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
965
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
791
966
  this.#adoptSessionRef(detail);
792
967
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
793
- 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) : [];
794
969
  return result;
795
970
  } catch (err) {
796
971
  if (timedOut() && !opts.signal?.aborted) {
@@ -813,7 +988,7 @@ var RunHandle = class {
813
988
  interactionHandle(interaction) {
814
989
  return new InteractionHandle(
815
990
  interaction,
816
- (action, body) => this.ops.postAction(action, body)
991
+ (action, body, options) => this.ops.postAction(action, body, options)
817
992
  );
818
993
  }
819
994
  /**
@@ -907,6 +1082,35 @@ function withTimeout(signal, timeoutMs) {
907
1082
  }
908
1083
  return { signal: controller.signal, timedOut: () => fired, dispose };
909
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
+ }
910
1114
  function waitWithSignal(operation, signal) {
911
1115
  if (!signal) return Promise.resolve().then(operation);
912
1116
  return new Promise((resolve, reject) => {
@@ -1004,17 +1208,15 @@ function createDeepResearchNamespace(transport) {
1004
1208
  signal: opts.signal,
1005
1209
  onReconnect: opts.onReconnect
1006
1210
  }),
1007
- postAction: async (action, body) => {
1008
- await transport.webAgentJson(
1009
- action.method || "POST",
1010
- action.endpoint,
1011
- token,
1012
- {
1013
- ...body !== void 0 ? { body } : {},
1014
- requiredScopes: ["webagent.deep_research:manage"]
1015
- }
1016
- );
1017
- },
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,
1018
1220
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
1019
1221
  listArtifacts: async (signal) => {
1020
1222
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1279,17 +1481,15 @@ function createDoAnythingNamespace(transport) {
1279
1481
  // Skip screenshot rows in backfill when the caller did not opt in.
1280
1482
  includeScreenshots: capture?.screenshots ? void 0 : false
1281
1483
  }),
1282
- postAction: async (action, body) => {
1283
- await transport.webAgentJson(
1284
- action.method || "POST",
1285
- action.endpoint,
1286
- token,
1287
- {
1288
- ...body !== void 0 ? { body } : {},
1289
- requiredScopes: ["webagent.do_anything:manage"]
1290
- }
1291
- );
1292
- },
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,
1293
1493
  cancel: async (reason) => asRecord2(
1294
1494
  (await api.cancel({
1295
1495
  token,
@@ -1410,81 +1610,6 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1410
1610
  };
1411
1611
  }
1412
1612
 
1413
- // src/scopes.ts
1414
- var QoniScopes = {
1415
- GUMEM_SESSION_CREATE: "gumem.session:create",
1416
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1417
- GUMEM_MEMORY_READ: "gumem.memory:read",
1418
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1419
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1420
- GUMEM_SEARCH_RUN: "gumem.search:run",
1421
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1422
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1423
- GUMEM_ACTION_WRITE: "gumem.action:write",
1424
- GUMEM_ACTION_READ: "gumem.action:read",
1425
- GUMEM_PROFILE_READ: "gumem.profile:read",
1426
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1427
- // `manage` (anything that changes execution state). See
1428
- // wa/docs/qoni-sdk-public-surface.md §3.
1429
- DO_ANYTHING_READ: "webagent.do_anything:read",
1430
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1431
- WEB_SEARCH_READ: "webagent.web_search:read",
1432
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1433
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1434
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1435
- TRACK_READ: "webagent.track:read",
1436
- TRACK_MANAGE: "webagent.track:manage"
1437
- };
1438
- var QoniProductScopes = {
1439
- doAnything: [
1440
- QoniScopes.DO_ANYTHING_READ,
1441
- QoniScopes.DO_ANYTHING_MANAGE
1442
- ],
1443
- webSearch: [
1444
- QoniScopes.WEB_SEARCH_READ,
1445
- QoniScopes.WEB_SEARCH_MANAGE
1446
- ],
1447
- deepResearch: [
1448
- QoniScopes.DEEP_RESEARCH_READ,
1449
- QoniScopes.DEEP_RESEARCH_MANAGE
1450
- ],
1451
- track: [
1452
- QoniScopes.TRACK_READ,
1453
- QoniScopes.TRACK_MANAGE
1454
- ]
1455
- };
1456
- var KNOWN_SCOPES = Object.values(QoniScopes);
1457
- var QoniScopeBundles = {
1458
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1459
- GUMEM_SESSION_RECALL: [
1460
- QoniScopes.GUMEM_MEMORY_READ,
1461
- QoniScopes.GUMEM_MEMORY_WRITE,
1462
- QoniScopes.GUMEM_MESSAGE_WRITE
1463
- ],
1464
- GUMEM_WRITE: [
1465
- QoniScopes.GUMEM_SESSION_CREATE,
1466
- QoniScopes.GUMEM_MESSAGE_WRITE,
1467
- QoniScopes.GUMEM_ACTION_WRITE
1468
- ],
1469
- // One bundle per WebAgent product = [read, manage].
1470
- DO_ANYTHING: [
1471
- QoniScopes.DO_ANYTHING_READ,
1472
- QoniScopes.DO_ANYTHING_MANAGE
1473
- ],
1474
- WEB_SEARCH: [
1475
- QoniScopes.WEB_SEARCH_READ,
1476
- QoniScopes.WEB_SEARCH_MANAGE
1477
- ],
1478
- DEEP_RESEARCH: [
1479
- QoniScopes.DEEP_RESEARCH_READ,
1480
- QoniScopes.DEEP_RESEARCH_MANAGE
1481
- ],
1482
- TRACK: [
1483
- QoniScopes.TRACK_READ,
1484
- QoniScopes.TRACK_MANAGE
1485
- ]
1486
- };
1487
-
1488
1613
  // src/genauth.ts
1489
1614
  function createGenAuthNamespace(transport, options = {}) {
1490
1615
  return {
@@ -1683,7 +1808,7 @@ var MonitorHandle = class {
1683
1808
  id;
1684
1809
  /** Refresh and return the monitor (normalized camelCase fields). */
1685
1810
  async get() {
1686
- return camelizeRecord(await this.ops.get());
1811
+ return normalizeMonitorRecord(await this.ops.get());
1687
1812
  }
1688
1813
  /**
1689
1814
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1691,11 +1816,11 @@ var MonitorHandle = class {
1691
1816
  * deleting.
1692
1817
  */
1693
1818
  async pause() {
1694
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1819
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1695
1820
  }
1696
1821
  /** Resume a paused monitor. */
1697
1822
  async resume() {
1698
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1823
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1699
1824
  }
1700
1825
  /**
1701
1826
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1713,11 +1838,11 @@ var MonitorHandle = class {
1713
1838
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1714
1839
  );
1715
1840
  }
1716
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1841
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1717
1842
  }
1718
1843
  /** Trigger an immediate tick outside the schedule. */
1719
1844
  async runNow() {
1720
- return camelizeRecord(await this.ops.runNow());
1845
+ return normalizeMonitorRecord(await this.ops.runNow());
1721
1846
  }
1722
1847
  /**
1723
1848
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1738,7 +1863,7 @@ var MonitorHandle = class {
1738
1863
  interactionHandle(interaction) {
1739
1864
  return new InteractionHandle(
1740
1865
  interaction,
1741
- (action, body) => this.ops.postAction(action, body)
1866
+ (action, body, options) => this.ops.postAction(action, body, options)
1742
1867
  );
1743
1868
  }
1744
1869
  /**
@@ -1843,17 +1968,15 @@ function createTrackNamespace(transport) {
1843
1968
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1844
1969
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1845
1970
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1846
- postAction: async (action, body) => {
1847
- await transport.webAgentJson(
1848
- action.method || "POST",
1849
- action.endpoint,
1850
- token,
1851
- {
1852
- ...body !== void 0 ? { body } : {},
1853
- requiredScopes: ["webagent.track:manage"]
1854
- }
1855
- );
1856
- },
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,
1857
1980
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1858
1981
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1859
1982
  delete: async () => {
@@ -1868,10 +1991,12 @@ function createTrackNamespace(transport) {
1868
1991
  if (!prompt || typeof prompt !== "string") {
1869
1992
  throw new QoniValidationError("track.create requires a prompt string");
1870
1993
  }
1994
+ const body = snakeifyKeys(rest);
1995
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1871
1996
  const created = await api.createMonitor({
1872
1997
  token,
1873
1998
  intent: prompt,
1874
- ...snakeifyKeys(rest)
1999
+ ...body
1875
2000
  });
1876
2001
  const monitorId = created.data.id || created.data.monitor_id;
1877
2002
  if (!monitorId) {
@@ -1889,7 +2014,7 @@ function createTrackNamespace(transport) {
1889
2014
  };
1890
2015
  }
1891
2016
  function normalizeMonitorRun(item) {
1892
- const normalized = camelizeRecord(item);
2017
+ const normalized = normalizeMonitorRecord(item);
1893
2018
  if (typeof normalized.id !== "string" || !normalized.id) {
1894
2019
  const id = normalized.runId ?? normalized.snapshotId;
1895
2020
  if (typeof id === "string" && id) normalized.id = id;
@@ -1899,11 +2024,44 @@ function normalizeMonitorRun(item) {
1899
2024
  function normalizeTrackInterveneInput(input) {
1900
2025
  return renameKeys4(input, { requestId: "request_id" });
1901
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
+ }
1902
2049
  function normalizeSchedule(schedule) {
2050
+ if (!isJsonObject(schedule)) return schedule;
1903
2051
  if (schedule.kind === "interval") {
1904
- 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 }');
1905
2063
  }
1906
- return { kind: "cron", expr: schedule.expr };
2064
+ return schedule;
1907
2065
  }
1908
2066
  function snakeifyKeys(value) {
1909
2067
  const out = {};