@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.js CHANGED
@@ -230,6 +230,8 @@ var QoniEventTypes = {
230
230
  Interaction: "interaction",
231
231
  /** A live screenshot frame (opt in with `capture: { screenshots: true }`); image on `event.image`. */
232
232
  Screenshot: "screenshot",
233
+ /** The browser viewer moved to a new page or needs a fresh attachment. Use `event.data.liveUrl` for the live preview. */
234
+ BrowserLiveUrlChanged: "browserLiveUrlChanged",
233
235
  /** Terminal — the run finished; outcome + output in `event.data` (cost is on the `RunResult`). */
234
236
  Done: "done",
235
237
  // --- Web Search ---
@@ -360,7 +362,6 @@ var WIRE_TO_SEMANTIC = {
360
362
  "run.recording_started": "progress",
361
363
  "run.recording_paused": "progress",
362
364
  "run.recording_finalized": "progress",
363
- "run.browser_live_url_changed": "progress",
364
365
  // side-channel
365
366
  "run.feedback_submitted": "progress"
366
367
  };
@@ -422,6 +423,18 @@ function normalizeRunEvent(wire, options = {}) {
422
423
  return { ...base, type: "screenshot", data: sData, image, isTerminal: false };
423
424
  }
424
425
  }
426
+ if (wireType === "run.browser_live_url_changed") {
427
+ const liveUrl = asString(c.liveUrl);
428
+ if (!liveUrl) return null;
429
+ const data2 = {
430
+ liveUrl,
431
+ supportsLivePump: c.supportsLivePump === true,
432
+ forceViewerReload: c.forceViewerReload === true
433
+ };
434
+ const providerSessionId = asString(c.providerSessionId);
435
+ if (providerSessionId) data2.providerSessionId = providerSessionId;
436
+ return { ...base, type: "browserLiveUrlChanged", data: data2, isTerminal: false };
437
+ }
425
438
  if (wireType === INTERACTION) {
426
439
  const interaction = normalizeInteraction(payload);
427
440
  if (interaction) {
@@ -477,6 +490,87 @@ var ActionKinds = {
477
490
  };
478
491
  var ACTION_KIND_VALUES = Object.values(ActionKinds);
479
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
+
480
574
  // src/interactions.ts
481
575
  function asString2(value) {
482
576
  return typeof value === "string" && value ? value : void 0;
@@ -575,6 +669,9 @@ function normalizeInteraction(payload) {
575
669
  if (evidence) interaction.evidence = evidence;
576
670
  return interaction;
577
671
  }
672
+ function interactionActionScopes(action, productManageScope) {
673
+ return action.kind === ActionKinds.openLogin ? [QoniScopes.SITE_LOGIN_REQUEST] : [productManageScope];
674
+ }
578
675
  var InteractionHandle = class {
579
676
  constructor(interaction, post) {
580
677
  this.post = post;
@@ -599,48 +696,122 @@ var InteractionHandle = class {
599
696
  return this.interaction.actions.some((a) => a.kind === kind);
600
697
  }
601
698
  /** Answer a clarification ask. */
602
- answer(text) {
603
- return this.invoke(ActionKinds.answer, { response: text });
699
+ async answer(text) {
700
+ await this.invoke(ActionKinds.answer, { response: text });
604
701
  }
605
702
  /** Skip / dismiss the interaction (proceed without acting on it). */
606
- skip() {
607
- return this.invoke(ActionKinds.skip);
703
+ async skip() {
704
+ await this.invoke(ActionKinds.skip);
608
705
  }
609
706
  /** Confirm a pending confirmation (approve the plan / destructive op). */
610
- confirm() {
611
- return this.invoke(ActionKinds.confirm);
707
+ async confirm() {
708
+ await this.invoke(ActionKinds.confirm);
612
709
  }
613
710
  /** Reject a pending confirmation. */
614
- reject() {
615
- return this.invoke(ActionKinds.reject);
711
+ async reject() {
712
+ await this.invoke(ActionKinds.reject);
616
713
  }
617
- /** Open the login surface for a site_login interaction. */
618
- openLogin() {
619
- 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
+ };
620
771
  }
621
772
  /** Tell the agent the user has finished signing in. */
622
- confirmSignedIn() {
623
- return this.invoke(ActionKinds.confirmSignedIn);
773
+ async confirmSignedIn() {
774
+ await this.invoke(ActionKinds.confirmSignedIn);
624
775
  }
625
776
  /** Connect to the live browser for a take_control interaction. */
626
- connectControl() {
627
- return this.invoke(ActionKinds.connectControl);
777
+ async connectControl() {
778
+ await this.invoke(ActionKinds.connectControl);
628
779
  }
629
780
  /** Refresh the live control surface / live url. */
630
- refreshControl() {
631
- return this.invoke(ActionKinds.refreshControl);
781
+ async refreshControl() {
782
+ await this.invoke(ActionKinds.refreshControl);
632
783
  }
633
784
  /** Hand control back to the agent. */
634
- releaseControl() {
635
- return this.invoke(ActionKinds.releaseControl);
785
+ async releaseControl() {
786
+ await this.invoke(ActionKinds.releaseControl);
636
787
  }
637
788
  /** Retry a wait interaction now. */
638
- retry() {
639
- return this.invoke(ActionKinds.retry);
789
+ async retry() {
790
+ await this.invoke(ActionKinds.retry);
640
791
  }
641
792
  /** Switch the profile a wait/login interaction is bound to. */
642
- switchProfile() {
643
- 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
+ );
644
815
  }
645
816
  /**
646
817
  * Invoke an action by kind with an optional body. `async` so a missing-action
@@ -648,6 +819,10 @@ var InteractionHandle = class {
648
819
  * method contract uniformly promise-based.
649
820
  */
650
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) {
651
826
  const action = this.interaction.actions.find((a) => a.kind === kind);
652
827
  if (!action) {
653
828
  const offered = this.interaction.actions.map((a) => a.kind).join(", ") || "none";
@@ -655,7 +830,7 @@ var InteractionHandle = class {
655
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.`
656
831
  );
657
832
  }
658
- await this.post(action, body);
833
+ return action;
659
834
  }
660
835
  };
661
836
 
@@ -725,7 +900,7 @@ var RunHandle = class {
725
900
  break settled;
726
901
  }
727
902
  }
728
- const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
903
+ const detail2 = await settleRead(() => this.ops.getDetail(signal), signal);
729
904
  if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
730
905
  terminalDetail = detail2;
731
906
  break;
@@ -739,10 +914,10 @@ var RunHandle = class {
739
914
  sawCleanClose = true;
740
915
  eventIdAtLastClose = lastEventId;
741
916
  }
742
- const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
917
+ const detail = terminalDetail ?? await settleRead(() => this.ops.getDetail(signal), signal);
743
918
  this.#adoptSessionRef(detail);
744
919
  const result = settleRunResult(this.id, detail, [], terminalDoneReason);
745
- 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) : [];
746
921
  return result;
747
922
  } catch (err) {
748
923
  if (timedOut() && !opts.signal?.aborted) {
@@ -765,7 +940,7 @@ var RunHandle = class {
765
940
  interactionHandle(interaction) {
766
941
  return new InteractionHandle(
767
942
  interaction,
768
- (action, body) => this.ops.postAction(action, body)
943
+ (action, body, options) => this.ops.postAction(action, body, options)
769
944
  );
770
945
  }
771
946
  /**
@@ -859,6 +1034,35 @@ function withTimeout(signal, timeoutMs) {
859
1034
  }
860
1035
  return { signal: controller.signal, timedOut: () => fired, dispose };
861
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
+ }
862
1066
  function waitWithSignal(operation, signal) {
863
1067
  if (!signal) return Promise.resolve().then(operation);
864
1068
  return new Promise((resolve, reject) => {
@@ -956,17 +1160,15 @@ function createDeepResearchNamespace(transport) {
956
1160
  signal: opts.signal,
957
1161
  onReconnect: opts.onReconnect
958
1162
  }),
959
- postAction: async (action, body) => {
960
- await transport.webAgentJson(
961
- action.method || "POST",
962
- action.endpoint,
963
- token,
964
- {
965
- ...body !== void 0 ? { body } : {},
966
- requiredScopes: ["webagent.deep_research:manage"]
967
- }
968
- );
969
- },
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,
970
1172
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
971
1173
  listArtifacts: async (signal) => {
972
1174
  const res = await api.listArtifacts({ token, runId, signal });
@@ -1231,17 +1433,15 @@ function createDoAnythingNamespace(transport) {
1231
1433
  // Skip screenshot rows in backfill when the caller did not opt in.
1232
1434
  includeScreenshots: capture?.screenshots ? void 0 : false
1233
1435
  }),
1234
- postAction: async (action, body) => {
1235
- await transport.webAgentJson(
1236
- action.method || "POST",
1237
- action.endpoint,
1238
- token,
1239
- {
1240
- ...body !== void 0 ? { body } : {},
1241
- requiredScopes: ["webagent.do_anything:manage"]
1242
- }
1243
- );
1244
- },
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,
1245
1445
  cancel: async (reason) => asRecord2(
1246
1446
  (await api.cancel({
1247
1447
  token,
@@ -1362,81 +1562,6 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1362
1562
  };
1363
1563
  }
1364
1564
 
1365
- // src/scopes.ts
1366
- var QoniScopes = {
1367
- GUMEM_SESSION_CREATE: "gumem.session:create",
1368
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
1369
- GUMEM_MEMORY_READ: "gumem.memory:read",
1370
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
1371
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1372
- GUMEM_SEARCH_RUN: "gumem.search:run",
1373
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1374
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1375
- GUMEM_ACTION_WRITE: "gumem.action:write",
1376
- GUMEM_ACTION_READ: "gumem.action:read",
1377
- GUMEM_PROFILE_READ: "gumem.profile:read",
1378
- // WebAgent products: exactly two verbs per product — `read` (observe) and
1379
- // `manage` (anything that changes execution state). See
1380
- // wa/docs/qoni-sdk-public-surface.md §3.
1381
- DO_ANYTHING_READ: "webagent.do_anything:read",
1382
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1383
- WEB_SEARCH_READ: "webagent.web_search:read",
1384
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1385
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
1386
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1387
- TRACK_READ: "webagent.track:read",
1388
- TRACK_MANAGE: "webagent.track:manage"
1389
- };
1390
- var QoniProductScopes = {
1391
- doAnything: [
1392
- QoniScopes.DO_ANYTHING_READ,
1393
- QoniScopes.DO_ANYTHING_MANAGE
1394
- ],
1395
- webSearch: [
1396
- QoniScopes.WEB_SEARCH_READ,
1397
- QoniScopes.WEB_SEARCH_MANAGE
1398
- ],
1399
- deepResearch: [
1400
- QoniScopes.DEEP_RESEARCH_READ,
1401
- QoniScopes.DEEP_RESEARCH_MANAGE
1402
- ],
1403
- track: [
1404
- QoniScopes.TRACK_READ,
1405
- QoniScopes.TRACK_MANAGE
1406
- ]
1407
- };
1408
- var KNOWN_SCOPES = Object.values(QoniScopes);
1409
- var QoniScopeBundles = {
1410
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1411
- GUMEM_SESSION_RECALL: [
1412
- QoniScopes.GUMEM_MEMORY_READ,
1413
- QoniScopes.GUMEM_MEMORY_WRITE,
1414
- QoniScopes.GUMEM_MESSAGE_WRITE
1415
- ],
1416
- GUMEM_WRITE: [
1417
- QoniScopes.GUMEM_SESSION_CREATE,
1418
- QoniScopes.GUMEM_MESSAGE_WRITE,
1419
- QoniScopes.GUMEM_ACTION_WRITE
1420
- ],
1421
- // One bundle per WebAgent product = [read, manage].
1422
- DO_ANYTHING: [
1423
- QoniScopes.DO_ANYTHING_READ,
1424
- QoniScopes.DO_ANYTHING_MANAGE
1425
- ],
1426
- WEB_SEARCH: [
1427
- QoniScopes.WEB_SEARCH_READ,
1428
- QoniScopes.WEB_SEARCH_MANAGE
1429
- ],
1430
- DEEP_RESEARCH: [
1431
- QoniScopes.DEEP_RESEARCH_READ,
1432
- QoniScopes.DEEP_RESEARCH_MANAGE
1433
- ],
1434
- TRACK: [
1435
- QoniScopes.TRACK_READ,
1436
- QoniScopes.TRACK_MANAGE
1437
- ]
1438
- };
1439
-
1440
1565
  // src/genauth.ts
1441
1566
  function createGenAuthNamespace(transport, options = {}) {
1442
1567
  return {
@@ -1635,7 +1760,7 @@ var MonitorHandle = class {
1635
1760
  id;
1636
1761
  /** Refresh and return the monitor (normalized camelCase fields). */
1637
1762
  async get() {
1638
- return camelizeRecord(await this.ops.get());
1763
+ return normalizeMonitorRecord(await this.ops.get());
1639
1764
  }
1640
1765
  /**
1641
1766
  * Pause the monitor — stops scheduled ticks until {@link resume}. The
@@ -1643,11 +1768,11 @@ var MonitorHandle = class {
1643
1768
  * deleting.
1644
1769
  */
1645
1770
  async pause() {
1646
- return camelizeRecord(await this.ops.update({ action: "pause" }));
1771
+ return normalizeMonitorRecord(await this.ops.update({ action: "pause" }));
1647
1772
  }
1648
1773
  /** Resume a paused monitor. */
1649
1774
  async resume() {
1650
- return camelizeRecord(await this.ops.update({ action: "resume" }));
1775
+ return normalizeMonitorRecord(await this.ops.update({ action: "resume" }));
1651
1776
  }
1652
1777
  /**
1653
1778
  * Change the monitor definition. Pass any subset of fields (at least one);
@@ -1665,11 +1790,11 @@ var MonitorHandle = class {
1665
1790
  "track refine requires at least one field to change (schedule / targetUrls / extractionSchema / triggerDsl / stopConditionDsl / notifyChannel)"
1666
1791
  );
1667
1792
  }
1668
- return camelizeRecord(await this.ops.update({ action: "refine", patch: body }));
1793
+ return normalizeMonitorRecord(await this.ops.update({ action: "refine", patch: body }));
1669
1794
  }
1670
1795
  /** Trigger an immediate tick outside the schedule. */
1671
1796
  async runNow() {
1672
- return camelizeRecord(await this.ops.runNow());
1797
+ return normalizeMonitorRecord(await this.ops.runNow());
1673
1798
  }
1674
1799
  /**
1675
1800
  * Stream semantic monitor events. Monitors are resident, so the stream has
@@ -1690,7 +1815,7 @@ var MonitorHandle = class {
1690
1815
  interactionHandle(interaction) {
1691
1816
  return new InteractionHandle(
1692
1817
  interaction,
1693
- (action, body) => this.ops.postAction(action, body)
1818
+ (action, body, options) => this.ops.postAction(action, body, options)
1694
1819
  );
1695
1820
  }
1696
1821
  /**
@@ -1795,17 +1920,15 @@ function createTrackNamespace(transport) {
1795
1920
  update: async (input) => asRecord3((await api.updateMonitor({ token, monitorId, ...input })).data),
1796
1921
  runNow: async () => asRecord3((await api.runNow({ token, monitorId })).data),
1797
1922
  streamEvents: (opts) => api.events({ token, monitorId, lastEventId: opts.lastEventId, signal: opts.signal }),
1798
- postAction: async (action, body) => {
1799
- await transport.webAgentJson(
1800
- action.method || "POST",
1801
- action.endpoint,
1802
- token,
1803
- {
1804
- ...body !== void 0 ? { body } : {},
1805
- requiredScopes: ["webagent.track:manage"]
1806
- }
1807
- );
1808
- },
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,
1809
1932
  listRuns: async (opts) => asRecord3((await api.listRuns({ token, monitorId, ...opts })).data),
1810
1933
  getRun: async (runId) => asRecord3((await api.getRun({ token, monitorId, runId })).data),
1811
1934
  delete: async () => {
@@ -1820,10 +1943,12 @@ function createTrackNamespace(transport) {
1820
1943
  if (!prompt || typeof prompt !== "string") {
1821
1944
  throw new QoniValidationError("track.create requires a prompt string");
1822
1945
  }
1946
+ const body = snakeifyKeys(rest);
1947
+ if (body.schedule !== void 0) body.schedule = normalizeSchedule(body.schedule);
1823
1948
  const created = await api.createMonitor({
1824
1949
  token,
1825
1950
  intent: prompt,
1826
- ...snakeifyKeys(rest)
1951
+ ...body
1827
1952
  });
1828
1953
  const monitorId = created.data.id || created.data.monitor_id;
1829
1954
  if (!monitorId) {
@@ -1841,7 +1966,7 @@ function createTrackNamespace(transport) {
1841
1966
  };
1842
1967
  }
1843
1968
  function normalizeMonitorRun(item) {
1844
- const normalized = camelizeRecord(item);
1969
+ const normalized = normalizeMonitorRecord(item);
1845
1970
  if (typeof normalized.id !== "string" || !normalized.id) {
1846
1971
  const id = normalized.runId ?? normalized.snapshotId;
1847
1972
  if (typeof id === "string" && id) normalized.id = id;
@@ -1851,11 +1976,44 @@ function normalizeMonitorRun(item) {
1851
1976
  function normalizeTrackInterveneInput(input) {
1852
1977
  return renameKeys4(input, { requestId: "request_id" });
1853
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
+ }
1854
2001
  function normalizeSchedule(schedule) {
2002
+ if (!isJsonObject(schedule)) return schedule;
1855
2003
  if (schedule.kind === "interval") {
1856
- 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 }');
1857
2015
  }
1858
- return { kind: "cron", expr: schedule.expr };
2016
+ return schedule;
1859
2017
  }
1860
2018
  function snakeifyKeys(value) {
1861
2019
  const out = {};