@appilots/sdk 0.6.0 → 0.8.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/README.md CHANGED
@@ -33,6 +33,47 @@ function App() {
33
33
  }
34
34
  ```
35
35
 
36
+ ## What happens when Appilots breaks
37
+
38
+ The SDK renders inside your app, so a bug of ours must never become an
39
+ outage of yours. Both render surfaces sit behind an error boundary:
40
+
41
+ - If the **provider** throws, your app keeps rendering. The assistant
42
+ goes inert — `AppilotsChat` renders nothing, and `useAppilots()` keeps
43
+ returning a context (with `degraded: true`) so your own code does not
44
+ throw either.
45
+ - If the **chat** throws, only the chat disappears.
46
+ - If the **auto-tracking interceptor** throws on some element, that
47
+ element renders untouched; the agent just does not see it.
48
+
49
+ In every case we log a `console.error` that says plainly it is our bug,
50
+ not yours, and the failure is reported to the Appilots dashboard on the
51
+ next session so we find out without you having to file anything.
52
+
53
+ Degradation lasts for the session — a surface that crashed once usually
54
+ crashes again, and an assistant blinking in and out of your product is
55
+ worse than an absent one. An exception thrown by **your** components is
56
+ never swallowed: it propagates to your own error boundary as it would
57
+ without us.
58
+
59
+ ## Native confirmation dialogs
60
+
61
+ When the user approves a destructive action on the chat's confirm card
62
+ and your code then calls `Alert.alert` to double-check, the SDK answers
63
+ that dialog once on their behalf — otherwise they are asked the same
64
+ question twice and the second dialog is one the agent cannot press.
65
+
66
+ Doing that means briefly replacing `Alert.alert`, a global that belongs
67
+ to your app. The replacement is refcounted, restored even if the action
68
+ throws, answers at most one dialog, never presses a `style: 'cancel'`
69
+ button, and expires about a second after the press so unrelated alerts
70
+ (background sync, push handlers) are never touched. To turn it off and
71
+ show your own dialog instead:
72
+
73
+ ```tsx
74
+ <AppilotsProvider config={{ projectId: '…', suppressNativeConfirm: false }}>
75
+ ```
76
+
36
77
  ## Documentation
37
78
 
38
79
  Full documentation available at [docs.appilots.com](https://docs.appilots.com)
@@ -82,7 +82,9 @@ var AppilotsClient = class {
82
82
  "Content-Type": "application/json",
83
83
  // Version telemetry: lets the server correlate behavior per SDK
84
84
  // release and pick the MCP doc matching this app build (B.4).
85
- "X-Appilots-Sdk-Version": SDK_VERSION,
85
+ // The platform SDK supplies its own published version; the
86
+ // client-core constant is only the fallback (see `sdkVersion`).
87
+ "X-Appilots-Sdk-Version": options.sdkVersion ?? SDK_VERSION,
86
88
  ...options.appVersion ? { "X-App-Version": options.appVersion } : {},
87
89
  ...options.mcpVersion ? { "X-Appilots-Mcp-Version": options.mcpVersion } : {},
88
90
  ...options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
@@ -197,7 +199,8 @@ var AppilotsClient = class {
197
199
  introspectionFragment() {
198
200
  try {
199
201
  const report = this.introspectionReporter?.();
200
- if (!report || report.captured !== false) return {};
202
+ if (!report) return {};
203
+ if (report.captured !== false && !report.failureReason) return {};
201
204
  return { introspection: report };
202
205
  } catch {
203
206
  return {};
@@ -2848,6 +2851,114 @@ function attachElementIdsToChoiceGroups(choiceGroups, elements) {
2848
2851
  })
2849
2852
  }));
2850
2853
  }
2854
+ var WIRE_LIMITS = {
2855
+ route: 200,
2856
+ texts: 500,
2857
+ textLength: 2e3,
2858
+ inputs: 300,
2859
+ buttons: 300,
2860
+ toggles: 300,
2861
+ sliders: 100,
2862
+ lists: 100,
2863
+ listItems: 500,
2864
+ listDataPreview: 500,
2865
+ listDataPreviewTextLength: 500,
2866
+ choiceGroups: 100,
2867
+ choiceOptions: 200,
2868
+ elements: 1e3,
2869
+ elementTexts: 50,
2870
+ elementTextLength: 500,
2871
+ elementActions: 20,
2872
+ idLength: 160,
2873
+ labelLength: 300,
2874
+ valueLength: 4096,
2875
+ containerTypeLength: 60};
2876
+ var Cut = class {
2877
+ dropped = false;
2878
+ /** Cap an array's length, remembering whether entries were lost. */
2879
+ array(values, max) {
2880
+ if (!values || values.length <= max) return values;
2881
+ this.dropped = true;
2882
+ return values.slice(0, max);
2883
+ }
2884
+ string(value, max) {
2885
+ if (value === void 0 || value.length <= max) return value;
2886
+ this.dropped = true;
2887
+ return value.slice(0, max);
2888
+ }
2889
+ };
2890
+ function clampInput(cut, input) {
2891
+ input.id = cut.string(input.id, WIRE_LIMITS.idLength);
2892
+ input.label = cut.string(input.label, WIRE_LIMITS.labelLength);
2893
+ input.placeholder = cut.string(input.placeholder, WIRE_LIMITS.labelLength);
2894
+ input.value = cut.string(input.value, WIRE_LIMITS.valueLength);
2895
+ return input;
2896
+ }
2897
+ function clampList(cut, list) {
2898
+ list.id = cut.string(list.id, WIRE_LIMITS.idLength);
2899
+ list.containerType = cut.string(list.containerType, WIRE_LIMITS.containerTypeLength);
2900
+ list.label = cut.string(list.label, WIRE_LIMITS.labelLength);
2901
+ list.items = cut.array(list.items, WIRE_LIMITS.listItems) ?? [];
2902
+ if (list.dataPreview) {
2903
+ list.dataPreview = cut.array(list.dataPreview, WIRE_LIMITS.listDataPreview);
2904
+ for (const entry of list.dataPreview ?? []) {
2905
+ entry.key = cut.string(entry.key, WIRE_LIMITS.idLength);
2906
+ entry.text = cut.string(entry.text, WIRE_LIMITS.listDataPreviewTextLength);
2907
+ }
2908
+ }
2909
+ return list;
2910
+ }
2911
+ function clampChoiceGroup(cut, group) {
2912
+ group.id = cut.string(group.id, WIRE_LIMITS.idLength);
2913
+ group.label = cut.string(group.label, WIRE_LIMITS.labelLength);
2914
+ group.options = cut.array(group.options, WIRE_LIMITS.choiceOptions) ?? [];
2915
+ return group;
2916
+ }
2917
+ function clampSnapshotToWireLimits(snapshot) {
2918
+ const cut = new Cut();
2919
+ if (typeof snapshot.route === "string") {
2920
+ snapshot.route = cut.string(snapshot.route, WIRE_LIMITS.route);
2921
+ }
2922
+ snapshot.texts = (cut.array(snapshot.texts, WIRE_LIMITS.texts) ?? []).map(
2923
+ (text) => cut.string(text, WIRE_LIMITS.textLength)
2924
+ );
2925
+ snapshot.inputs = (cut.array(snapshot.inputs, WIRE_LIMITS.inputs) ?? []).map(
2926
+ (input) => clampInput(cut, input)
2927
+ );
2928
+ snapshot.buttons = (cut.array(snapshot.buttons, WIRE_LIMITS.buttons) ?? []).map((button) => {
2929
+ button.id = cut.string(button.id, WIRE_LIMITS.idLength);
2930
+ button.label = cut.string(button.label, WIRE_LIMITS.labelLength);
2931
+ return button;
2932
+ });
2933
+ snapshot.toggles = (cut.array(snapshot.toggles, WIRE_LIMITS.toggles) ?? []).map((toggle) => {
2934
+ toggle.id = cut.string(toggle.id, WIRE_LIMITS.idLength);
2935
+ toggle.label = cut.string(toggle.label, WIRE_LIMITS.labelLength);
2936
+ return toggle;
2937
+ });
2938
+ snapshot.sliders = (cut.array(snapshot.sliders, WIRE_LIMITS.sliders) ?? []).map((slider) => {
2939
+ slider.id = cut.string(slider.id, WIRE_LIMITS.idLength);
2940
+ slider.label = cut.string(slider.label, WIRE_LIMITS.labelLength);
2941
+ return slider;
2942
+ });
2943
+ snapshot.lists = (cut.array(snapshot.lists, WIRE_LIMITS.lists) ?? []).map(
2944
+ (list) => clampList(cut, list)
2945
+ );
2946
+ snapshot.choiceGroups = (cut.array(snapshot.choiceGroups, WIRE_LIMITS.choiceGroups) ?? []).map(
2947
+ (group) => clampChoiceGroup(cut, group)
2948
+ );
2949
+ snapshot.elements = (cut.array(snapshot.elements, WIRE_LIMITS.elements) ?? []).map((element) => {
2950
+ element.id = cut.string(element.id, WIRE_LIMITS.idLength);
2951
+ element.label = cut.string(element.label, WIRE_LIMITS.labelLength);
2952
+ element.targetId = cut.string(element.targetId, WIRE_LIMITS.idLength);
2953
+ element.texts = (cut.array(element.texts, WIRE_LIMITS.elementTexts) ?? []).map(
2954
+ (text) => cut.string(text, WIRE_LIMITS.elementTextLength)
2955
+ );
2956
+ element.actions = cut.array(element.actions, WIRE_LIMITS.elementActions) ?? [];
2957
+ return element;
2958
+ });
2959
+ if (cut.dropped) snapshot.truncated = true;
2960
+ return snapshot;
2961
+ }
2851
2962
 
2852
2963
  // src/registry/ComponentRegistry.ts
2853
2964
  var ComponentRegistryImpl = class {
@@ -3125,6 +3236,40 @@ function sectionListFlatIndex(sections, section, localIndex) {
3125
3236
  return base + localIndex;
3126
3237
  }
3127
3238
 
3239
+ // src/auto/interceptGuard.ts
3240
+ var _intercept = null;
3241
+ var _failures = 0;
3242
+ var INTERCEPT_FAILURE_LIMIT = 25;
3243
+ function setInterceptor(fn) {
3244
+ _intercept = fn;
3245
+ _failures = 0;
3246
+ }
3247
+ function isInterceptionActive() {
3248
+ return _intercept !== null;
3249
+ }
3250
+ function safeIntercept(type, props) {
3251
+ const intercept = _intercept;
3252
+ if (!intercept) return null;
3253
+ try {
3254
+ return intercept(type, props);
3255
+ } catch (err) {
3256
+ _failures += 1;
3257
+ if (_failures === 1) {
3258
+ console.error(
3259
+ "[Appilots] Auto-tracking failed to inspect an element and passed it through unchanged. Your app is unaffected; the agent may not see this component. This is an Appilots bug \u2014 please report it: https://github.com/Axtern-Labs/Appilots/issues",
3260
+ err
3261
+ );
3262
+ }
3263
+ if (_failures >= INTERCEPT_FAILURE_LIMIT) {
3264
+ _intercept = null;
3265
+ console.error(
3266
+ `[Appilots] Auto-tracking disabled after ${INTERCEPT_FAILURE_LIMIT} failures. The agent will only see components registered explicitly via registerScreen(). Your app is unaffected.`
3267
+ );
3268
+ }
3269
+ return null;
3270
+ }
3271
+ }
3272
+
3128
3273
  // src/auto/enableAutoTracking.tsx
3129
3274
  function extractId(props) {
3130
3275
  const raw = props?.testID ?? props?.nativeID ?? props?.id ?? props?.accessibilityLabel ?? props?.name ?? props?.placeholder ?? (typeof props?.autoComplete === "string" && props.autoComplete !== "off" ? props.autoComplete : void 0);
@@ -3583,15 +3728,15 @@ function enableAppilotsAutoTracking(options) {
3583
3728
  }
3584
3729
  return null;
3585
3730
  }
3731
+ setInterceptor(interceptType);
3586
3732
  React2__default.default.createElement = function(type, props, ...children) {
3587
- const replacement = interceptType(type, props);
3733
+ const replacement = safeIntercept(type, props);
3588
3734
  if (replacement) {
3589
3735
  return originalCreateElement(replacement.type, replacement.props, ...children);
3590
3736
  }
3591
3737
  return originalCreateElement(type, props, ...children);
3592
3738
  };
3593
3739
  console.log("[Appilots] Auto-tracking: React.createElement patched");
3594
- _interceptType = interceptType;
3595
3740
  try {
3596
3741
  const jsxRuntime = __require("react/jsx-runtime");
3597
3742
  if (jsxRuntime) {
@@ -3609,18 +3754,16 @@ function enableAppilotsAutoTracking(options) {
3609
3754
  console.log("[Appilots] Auto-tracking: react/jsx-dev-runtime not available internally (will be patched by metro shim)");
3610
3755
  }
3611
3756
  }
3612
- var _interceptType = null;
3613
3757
  var _jsxRuntimePatched = false;
3614
3758
  var _jsxDevRuntimePatched = false;
3615
3759
  function _patchJsxRuntimeModule(jsxRuntime) {
3616
- if (_jsxRuntimePatched || !_interceptType || !jsxRuntime) return;
3760
+ if (_jsxRuntimePatched || !isInterceptionActive() || !jsxRuntime) return;
3617
3761
  _jsxRuntimePatched = true;
3618
- const interceptType = _interceptType;
3619
3762
  const origJsx = jsxRuntime.jsx;
3620
3763
  const origJsxs = jsxRuntime.jsxs;
3621
3764
  if (origJsx) {
3622
3765
  jsxRuntime.jsx = function(type, props, key) {
3623
- const replacement = interceptType(type, props);
3766
+ const replacement = safeIntercept(type, props);
3624
3767
  if (replacement) {
3625
3768
  return origJsx(replacement.type, replacement.props, key);
3626
3769
  }
@@ -3630,7 +3773,7 @@ function _patchJsxRuntimeModule(jsxRuntime) {
3630
3773
  }
3631
3774
  if (origJsxs) {
3632
3775
  jsxRuntime.jsxs = function(type, props, key) {
3633
- const replacement = interceptType(type, props);
3776
+ const replacement = safeIntercept(type, props);
3634
3777
  if (replacement) {
3635
3778
  return origJsxs(replacement.type, replacement.props, key);
3636
3779
  }
@@ -3640,13 +3783,12 @@ function _patchJsxRuntimeModule(jsxRuntime) {
3640
3783
  }
3641
3784
  }
3642
3785
  function _patchJsxDevRuntimeModule(jsxDevRuntime) {
3643
- if (_jsxDevRuntimePatched || !_interceptType || !jsxDevRuntime) return;
3786
+ if (_jsxDevRuntimePatched || !isInterceptionActive() || !jsxDevRuntime) return;
3644
3787
  _jsxDevRuntimePatched = true;
3645
- const interceptType = _interceptType;
3646
3788
  if (jsxDevRuntime.jsxDEV) {
3647
3789
  const origJsxDEV = jsxDevRuntime.jsxDEV;
3648
3790
  jsxDevRuntime.jsxDEV = function(type, props, key, isStaticChildren, source, self) {
3649
- const replacement = interceptType(type, props);
3791
+ const replacement = safeIntercept(type, props);
3650
3792
  if (replacement) {
3651
3793
  return origJsxDEV(replacement.type, replacement.props, key, isStaticChildren, source, self);
3652
3794
  }
@@ -3656,7 +3798,7 @@ function _patchJsxDevRuntimeModule(jsxDevRuntime) {
3656
3798
  }
3657
3799
  }
3658
3800
  function _patchJsxRuntimes(jsxRuntime, jsxDevRuntime) {
3659
- if (!_interceptType) {
3801
+ if (!isInterceptionActive()) {
3660
3802
  console.warn("[Appilots] _patchJsxRuntimes called but auto-tracking is not enabled");
3661
3803
  return;
3662
3804
  }
@@ -3726,7 +3868,14 @@ function setFiberRoot(fiber) {
3726
3868
  _diagnostics.captured = true;
3727
3869
  }
3728
3870
  function getFiberRoot() {
3729
- return _fiberRoot;
3871
+ return resolveCurrentRoot(_fiberRoot);
3872
+ }
3873
+ function resolveCurrentRoot(fiber) {
3874
+ const current = fiber?.stateNode?.current;
3875
+ if (current && (current === fiber || current.alternate === fiber)) {
3876
+ return current;
3877
+ }
3878
+ return fiber;
3730
3879
  }
3731
3880
  function recordIntrospectionFailure(reason, reactVersion) {
3732
3881
  _diagnostics.failureCount += 1;
@@ -3735,6 +3884,13 @@ function recordIntrospectionFailure(reason, reactVersion) {
3735
3884
  _diagnostics.reactVersion = reactVersion;
3736
3885
  }
3737
3886
  }
3887
+ function recordRenderFailure(reactVersion) {
3888
+ _fiberRoot = null;
3889
+ _diagnostics.captured = false;
3890
+ _diagnostics.failureCount += 1;
3891
+ _diagnostics.failureReason = "render-crashed";
3892
+ _diagnostics.reactVersion = reactVersion;
3893
+ }
3738
3894
  function getIntrospectionDiagnostics() {
3739
3895
  return { ..._diagnostics };
3740
3896
  }
@@ -3746,6 +3902,9 @@ function typeName(fiber) {
3746
3902
  if (typeof t === "object") return t.displayName ?? t.render?.displayName ?? t.render?.name ?? "forwardRef/memo";
3747
3903
  return String(t);
3748
3904
  }
3905
+
3906
+ // src/version.ts
3907
+ var SDK_VERSION2 = "0.8.0";
3749
3908
  var FiberSentinel = class extends React2__default.default.Component {
3750
3909
  componentDidMount() {
3751
3910
  const fiber = this._reactInternals ?? this._reactInternalFiber ?? null;
@@ -3795,10 +3954,83 @@ function appilotsDebugWarn(...args) {
3795
3954
  if (!isDebugEnabled()) return;
3796
3955
  console.log("[Appilots]", ...args);
3797
3956
  }
3957
+ var AppilotsErrorBoundary = class extends React2__default.default.Component {
3958
+ state = { degraded: false, escalate: null };
3959
+ /**
3960
+ * Instance field rather than state: `componentDidCatch` needs to know
3961
+ * whether THIS is the first failure, and by the time it runs
3962
+ * `getDerivedStateFromError` has already flipped `state.degraded`.
3963
+ */
3964
+ hasDegradedOnce = false;
3965
+ static getDerivedStateFromError() {
3966
+ return { degraded: true };
3967
+ }
3968
+ componentDidCatch(error, info) {
3969
+ if (this.hasDegradedOnce) {
3970
+ this.setState({ escalate: error ?? new Error("Appilots: re-thrown host render error") });
3971
+ return;
3972
+ }
3973
+ this.hasDegradedOnce = true;
3974
+ console.error(
3975
+ `[Appilots] The Appilots ${this.props.surface} threw while rendering and has been disabled for the rest of this session. Your app keeps running \u2014 this is an Appilots bug, not a bug in your code. Please report it with the stack below: https://github.com/Axtern-Labs/Appilots/issues`,
3976
+ error
3977
+ );
3978
+ try {
3979
+ this.props.onError?.(error, info?.componentStack ?? null);
3980
+ } catch {
3981
+ }
3982
+ }
3983
+ render() {
3984
+ if (this.state.escalate !== null) throw this.state.escalate;
3985
+ if (this.state.degraded) return this.props.fallback;
3986
+ return this.props.children;
3987
+ }
3988
+ };
3798
3989
 
3799
3990
  // src/context/AppilotsProvider.tsx
3800
3991
  var AppilotsContext = React2.createContext(null);
3801
- function AppilotsProvider({ config: configProp, children, client: externalClient }) {
3992
+ function createInertClient() {
3993
+ const unavailable = () => Promise.reject(
3994
+ new Error("Appilots is unavailable: the SDK degraded after a render error.")
3995
+ );
3996
+ return new Proxy({}, {
3997
+ get: (_target, prop) => {
3998
+ if (prop === "then" || typeof prop === "symbol") return void 0;
3999
+ return unavailable;
4000
+ }
4001
+ });
4002
+ }
4003
+ function createDegradedContext(config) {
4004
+ return {
4005
+ config: config ?? { projectId: "" },
4006
+ client: createInertClient(),
4007
+ subscribe: () => () => {
4008
+ },
4009
+ emit: () => {
4010
+ },
4011
+ remotePersonalization: null,
4012
+ degraded: true
4013
+ };
4014
+ }
4015
+ function AppilotsProvider({ config, children, client }) {
4016
+ return /* @__PURE__ */ React2__default.default.createElement(
4017
+ AppilotsErrorBoundary,
4018
+ {
4019
+ surface: "provider",
4020
+ onError: reportProviderRenderFailure,
4021
+ fallback: /* @__PURE__ */ React2__default.default.createElement(AppilotsContext.Provider, { value: createDegradedContext(config ?? null) }, children)
4022
+ },
4023
+ /* @__PURE__ */ React2__default.default.createElement(AppilotsProviderInner, { config, client }, children)
4024
+ );
4025
+ }
4026
+ function reportProviderRenderFailure() {
4027
+ recordRenderFailure(readReactVersion());
4028
+ }
4029
+ function readReactVersion() {
4030
+ const version = React2__default.default.version;
4031
+ return typeof version === "string" ? version : null;
4032
+ }
4033
+ function AppilotsProviderInner({ config: configProp, children, client: externalClient }) {
3802
4034
  console.log(`[Appilots] AppilotsProvider initializing \u2014 config source: ${configProp ? "prop" : "auto (globalThis.__APPILOTS_RC__)"}`);
3803
4035
  tryAutoConfig();
3804
4036
  const globalConfig = getGlobalConfig();
@@ -3811,7 +4043,8 @@ function AppilotsProvider({ config: configProp, children, client: externalClient
3811
4043
  debug: globalConfig.debug,
3812
4044
  appVersion: globalConfig.appVersion,
3813
4045
  mcpVersion: globalConfig.mcpVersion,
3814
- fetchPersonalization: globalConfig.fetchPersonalization
4046
+ fetchPersonalization: globalConfig.fetchPersonalization,
4047
+ suppressNativeConfirm: globalConfig.suppressNativeConfirm
3815
4048
  } : null);
3816
4049
  if (!config?.projectId) {
3817
4050
  throw new Error(
@@ -3850,6 +4083,9 @@ function AppilotsProvider({ config: configProp, children, client: externalClient
3850
4083
  debug: config.debug,
3851
4084
  appVersion: config.appVersion,
3852
4085
  mcpVersion: config.mcpVersion,
4086
+ // This package's published version, so the server knows which
4087
+ // SDK is actually in the field (issue #312).
4088
+ sdkVersion: SDK_VERSION2,
3853
4089
  user: config.user,
3854
4090
  // Only reports a FAILED reading; the happy path sends nothing.
3855
4091
  introspectionReporter: getIntrospectionDiagnostics
@@ -4394,17 +4630,19 @@ function clearScreenRegistry() {
4394
4630
 
4395
4631
  exports.ActionQueueMachine = ActionQueueMachine;
4396
4632
  exports.AppilotsClient = AppilotsClient;
4633
+ exports.AppilotsErrorBoundary = AppilotsErrorBoundary;
4397
4634
  exports.AppilotsProvider = AppilotsProvider;
4398
4635
  exports.AppilotsRegistryProvider = AppilotsRegistryProvider;
4399
4636
  exports.ChatSessionMachine = ChatSessionMachine;
4400
4637
  exports.OPTIONAL_STEP_AUTOMATION_HINT = OPTIONAL_STEP_AUTOMATION_HINT;
4401
4638
  exports.RateLimitedError = RateLimitedError;
4402
- exports.SDK_VERSION = SDK_VERSION;
4639
+ exports.SDK_VERSION = SDK_VERSION2;
4403
4640
  exports._patchJsxRuntimes = _patchJsxRuntimes;
4404
4641
  exports.actionPressTargetId = actionPressTargetId;
4405
4642
  exports.appilotsDebugLog = appilotsDebugLog;
4406
4643
  exports.appilotsDebugWarn = appilotsDebugWarn;
4407
4644
  exports.attachElementIdsToChoiceGroups = attachElementIdsToChoiceGroups;
4645
+ exports.clampSnapshotToWireLimits = clampSnapshotToWireLimits;
4408
4646
  exports.clearAppilotsDebugTraces = clearAppilotsDebugTraces;
4409
4647
  exports.clearScreenRegistry = clearScreenRegistry;
4410
4648
  exports.componentRegistry = componentRegistry;