@runhuman/sensor 0.2.5 → 0.3.1

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.mjs CHANGED
@@ -164,6 +164,10 @@ var apiRoutes = {
164
164
  jobs: defineRoute("/jobs"),
165
165
  /** Get/update/delete specific job */
166
166
  job: defineRoute("/jobs/:jobId"),
167
+ /** Cancel a job */
168
+ jobCancel: defineRoute("/jobs/:jobId/cancel"),
169
+ /** Create a GitHub issue from an extracted finding (user-initiated) */
170
+ jobCreateIssue: defineRoute("/jobs/:jobId/create-issue"),
167
171
  /** Get job status (for polling) */
168
172
  jobStatus: defineRoute("/jobs/:jobId/status"),
169
173
  /** Get individual job artifact by type */
@@ -282,6 +286,8 @@ var apiRoutes = {
282
286
  githubLink: defineRoute("/github/link"),
283
287
  /** List issues for a repo (by owner/repo) */
284
288
  githubIssuesByRepo: defineRoute("/github/issues/:owner/:repo"),
289
+ /** Get a single issue for a repo (by owner/repo/issueNumber) */
290
+ githubIssueByRepo: defineRoute("/github/issues/:owner/:repo/:issueNumber"),
285
291
  /** List issues (by projectId query param) */
286
292
  githubIssues: defineRoute("/github/issues"),
287
293
  /** Get a single issue */
@@ -436,8 +442,10 @@ var apiRoutes = {
436
442
  organizationJobs: defineRoute("/organizations/:organizationId/jobs"),
437
443
  /** Transfer organization ownership */
438
444
  organizationTransferOwnership: defineRoute("/organizations/:organizationId/transfer-ownership"),
439
- /** Create organization API key */
445
+ /** List/create organization API keys */
440
446
  organizationApiKeys: defineRoute("/organizations/:organizationId/api-keys"),
447
+ /** Get/revoke/delete a specific organization API key */
448
+ organizationApiKey: defineRoute("/organizations/:organizationId/api-keys/:keyId"),
441
449
  /** Get GitHub installations for an organization */
442
450
  organizationGitHubInstallations: defineRoute("/organizations/:organizationId/github/installations"),
443
451
  /** Get/delete specific GitHub installation for an organization */
@@ -546,7 +554,7 @@ var ApiClient = class {
546
554
  return this.get(apiRoutes.telemetrySessionStatus.build({ jobId }));
547
555
  }
548
556
  async resolveShortCode(code) {
549
- const normalized = code.replace(/^RH-/i, "").toUpperCase();
557
+ const normalized = code.toUpperCase().trim();
550
558
  try {
551
559
  return await this.get(
552
560
  apiRoutes.telemetryShortCodeResolve.build({ code: normalized })
@@ -821,6 +829,7 @@ var InterceptorManager = class {
821
829
  var SessionManager = class {
822
830
  constructor(config) {
823
831
  this.state = "idle";
832
+ this._disabled = false;
824
833
  this.sessionId = null;
825
834
  this.activeJobId = null;
826
835
  this.pollTimer = null;
@@ -843,6 +852,10 @@ var SessionManager = class {
843
852
  getActiveJobId() {
844
853
  return this.activeJobId;
845
854
  }
855
+ /** True when the API rejected the session (e.g. subscription tier). Hides the overlay. */
856
+ isDisabled() {
857
+ return this._disabled;
858
+ }
846
859
  /**
847
860
  * Subscribe to state changes. Returns an unsubscribe function.
848
861
  * Compatible with React's useSyncExternalStore.
@@ -927,12 +940,22 @@ var SessionManager = class {
927
940
  async startSession() {
928
941
  this.setState("active");
929
942
  const now = /* @__PURE__ */ new Date();
930
- const response = await this.config.apiClient.createSession({
931
- jobId: this.activeJobId,
932
- platform: this.config.platform,
933
- sdkVersion: this.config.sdkVersion,
934
- clientStartTime: now.toISOString()
935
- });
943
+ let response;
944
+ try {
945
+ response = await this.config.apiClient.createSession({
946
+ jobId: this.activeJobId,
947
+ platform: this.config.platform,
948
+ sdkVersion: this.config.sdkVersion,
949
+ clientStartTime: now.toISOString()
950
+ });
951
+ } catch (error) {
952
+ const is403 = error instanceof Error && error.message.includes("403");
953
+ this.log(is403 ? "Subscription not eligible \u2014 disabling sensor" : "Session creation failed", { error });
954
+ if (is403) this._disabled = true;
955
+ this.activeJobId = null;
956
+ this.setState("idle");
957
+ return;
958
+ }
936
959
  this.sessionId = response.sessionId;
937
960
  const sessionStartTime = now.getTime();
938
961
  this.log("Session started", {
@@ -1140,11 +1163,11 @@ var Runhuman = class _Runhuman {
1140
1163
  _Runhuman.instance = null;
1141
1164
  this.log("Destroyed");
1142
1165
  }
1143
- /** Access the session manager (used by <RunhumanOverlay /> for reactive state) */
1166
+ /** Access the session manager (used by <RunhumanProvider /> for reactive state) */
1144
1167
  getSessionManager() {
1145
1168
  return this.sessionManager;
1146
1169
  }
1147
- /** Access the API client (used by <RunhumanOverlay /> for short code resolution) */
1170
+ /** Access the API client (used by <RunhumanProvider /> for short code resolution) */
1148
1171
  getApiClient() {
1149
1172
  return this.apiClient;
1150
1173
  }
@@ -1155,13 +1178,14 @@ var Runhuman = class _Runhuman {
1155
1178
  }
1156
1179
  };
1157
1180
 
1158
- // src/overlay/RunhumanOverlay.tsx
1181
+ // src/overlay/RunhumanProvider.tsx
1182
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1159
1183
  import { View as View2, StyleSheet as StyleSheet2 } from "react-native";
1160
1184
 
1161
1185
  // src/overlay/use-sensor-state.ts
1162
1186
  import { useCallback, useRef } from "react";
1163
1187
  import { useSyncExternalStore } from "react";
1164
- var IDLE_STATE = { state: "idle", activeJobId: null, sessionId: null };
1188
+ var IDLE_STATE = { state: "idle", activeJobId: null, sessionId: null, disabled: false };
1165
1189
  function tryGetInstance() {
1166
1190
  try {
1167
1191
  return Runhuman.getInstance();
@@ -1197,11 +1221,12 @@ function useSensorState() {
1197
1221
  const state = sm.getSnapshot();
1198
1222
  const activeJobId = sm.getActiveJobId();
1199
1223
  const sessionId = sm.getSessionId();
1224
+ const disabled = sm.isDisabled();
1200
1225
  const prev = cached.current;
1201
- if (prev.state === state && prev.activeJobId === activeJobId && prev.sessionId === sessionId) {
1226
+ if (prev.state === state && prev.activeJobId === activeJobId && prev.sessionId === sessionId && prev.disabled === disabled) {
1202
1227
  return prev;
1203
1228
  }
1204
- cached.current = { state, activeJobId, sessionId };
1229
+ cached.current = { state, activeJobId, sessionId, disabled };
1205
1230
  return cached.current;
1206
1231
  }, []);
1207
1232
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
@@ -1242,6 +1267,11 @@ var overlayStyles = StyleSheet.create({
1242
1267
  fontWeight: "600",
1243
1268
  marginBottom: 8
1244
1269
  },
1270
+ inputRow: {
1271
+ flexDirection: "row",
1272
+ alignItems: "center",
1273
+ gap: 6
1274
+ },
1245
1275
  input: {
1246
1276
  backgroundColor: "rgba(255, 255, 255, 0.1)",
1247
1277
  borderRadius: 8,
@@ -1255,9 +1285,25 @@ var overlayStyles = StyleSheet.create({
1255
1285
  padding: 10,
1256
1286
  textAlign: "center"
1257
1287
  },
1288
+ inputFlex: {
1289
+ flex: 1
1290
+ },
1258
1291
  inputError: {
1259
1292
  borderColor: BRAND_RED
1260
1293
  },
1294
+ pasteButton: {
1295
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
1296
+ borderRadius: 8,
1297
+ borderWidth: 1,
1298
+ borderColor: OVERLAY_BORDER,
1299
+ paddingVertical: 10,
1300
+ paddingHorizontal: 8
1301
+ },
1302
+ pasteButtonText: {
1303
+ color: "rgba(255, 255, 255, 0.6)",
1304
+ fontSize: 11,
1305
+ fontWeight: "600"
1306
+ },
1261
1307
  submitButton: {
1262
1308
  backgroundColor: BRAND_BLUE,
1263
1309
  borderRadius: 8,
@@ -1324,6 +1370,14 @@ var overlayStyles = StyleSheet.create({
1324
1370
  }
1325
1371
  });
1326
1372
 
1373
+ // src/overlay/clipboard.ts
1374
+ import * as ReactNative from "react-native";
1375
+ var RN = ReactNative;
1376
+ var clipboard = RN["Clipboard"];
1377
+ async function getClipboardText() {
1378
+ return clipboard.getString();
1379
+ }
1380
+
1327
1381
  // src/overlay/CodeEntryPanel.tsx
1328
1382
  import { jsx, jsxs } from "react/jsx-runtime";
1329
1383
  function CodeEntryPanel({ position }) {
@@ -1336,38 +1390,55 @@ function CodeEntryPanel({ position }) {
1336
1390
  if (!trimmed) return;
1337
1391
  setResolving(true);
1338
1392
  setError(null);
1339
- const instance = Runhuman.getInstance();
1340
- const result = await instance.getApiClient().resolveShortCode(trimmed);
1341
- if (result) {
1342
- instance.activate(result.jobId);
1343
- } else {
1344
- setError("Invalid or expired code");
1393
+ try {
1394
+ const instance = Runhuman.getInstance();
1395
+ const result = await instance.getApiClient().resolveShortCode(trimmed);
1396
+ if (result) {
1397
+ await instance.activate(result.jobId);
1398
+ } else {
1399
+ setError("Invalid or expired code");
1400
+ setResolving(false);
1401
+ }
1402
+ } catch (err) {
1403
+ const message = err instanceof Error ? err.message : "Activation failed";
1404
+ setError(message);
1345
1405
  setResolving(false);
1346
1406
  }
1347
1407
  };
1408
+ const handlePaste = async () => {
1409
+ const text = await getClipboardText();
1410
+ const trimmed = text.trim().toUpperCase().slice(0, 6);
1411
+ if (trimmed) {
1412
+ setCode(trimmed);
1413
+ setError(null);
1414
+ }
1415
+ };
1348
1416
  if (minimized) {
1349
1417
  return /* @__PURE__ */ jsx(Pressable, { style: [overlayStyles.fab, overlayStyles[position]], onPress: () => setMinimized(false), children: /* @__PURE__ */ jsx(Text, { style: overlayStyles.fabText, children: "RH" }) });
1350
1418
  }
1351
1419
  return /* @__PURE__ */ jsxs(View, { style: [overlayStyles.panel, overlayStyles[position]], children: [
1352
1420
  /* @__PURE__ */ jsx(Pressable, { style: overlayStyles.minimizeButton, onPress: () => setMinimized(true), children: /* @__PURE__ */ jsx(Text, { style: overlayStyles.minimizeText, children: "-" }) }),
1353
1421
  /* @__PURE__ */ jsx(Text, { style: overlayStyles.panelTitle, children: "Runhuman Sensor" }),
1354
- /* @__PURE__ */ jsx(
1355
- TextInput,
1356
- {
1357
- style: error ? { ...overlayStyles.input, ...overlayStyles.inputError } : overlayStyles.input,
1358
- value: code,
1359
- onChangeText: (text) => {
1360
- setCode(text.toUpperCase());
1361
- setError(null);
1362
- },
1363
- placeholder: "RH-XXXX",
1364
- placeholderTextColor: "rgba(255,255,255,0.3)",
1365
- autoCapitalize: "characters",
1366
- maxLength: 10,
1367
- editable: !resolving,
1368
- onSubmitEditing: handleSubmit
1369
- }
1370
- ),
1422
+ /* @__PURE__ */ jsxs(View, { style: overlayStyles.inputRow, children: [
1423
+ /* @__PURE__ */ jsx(
1424
+ TextInput,
1425
+ {
1426
+ style: [error ? { ...overlayStyles.input, ...overlayStyles.inputError } : overlayStyles.input, overlayStyles.inputFlex],
1427
+ value: code,
1428
+ onChangeText: (text) => {
1429
+ setCode(text.toUpperCase());
1430
+ setError(null);
1431
+ },
1432
+ placeholder: "XXXX",
1433
+ placeholderTextColor: "rgba(255,255,255,0.3)",
1434
+ autoCapitalize: "characters",
1435
+ maxLength: 6,
1436
+ editable: !resolving,
1437
+ onSubmitEditing: handleSubmit
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsx(Pressable, { style: overlayStyles.pasteButton, onPress: handlePaste, disabled: resolving, children: /* @__PURE__ */ jsx(Text, { style: overlayStyles.pasteButtonText, children: "Paste" }) })
1441
+ ] }),
1371
1442
  error ? /* @__PURE__ */ jsx(Text, { style: overlayStyles.errorText, children: error }) : null,
1372
1443
  /* @__PURE__ */ jsx(
1373
1444
  Pressable,
@@ -1410,7 +1481,7 @@ function ActiveIndicator({ state, position }) {
1410
1481
  return /* @__PURE__ */ jsx2(Animated.View, { style: [overlayStyles.indicator, overlayStyles[position], colorStyle, { opacity: pulse }] });
1411
1482
  }
1412
1483
 
1413
- // src/overlay/RunhumanOverlay.tsx
1484
+ // src/overlay/RunhumanProvider.tsx
1414
1485
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1415
1486
  var positionMap = {
1416
1487
  "top-left": "topLeft",
@@ -1418,13 +1489,48 @@ var positionMap = {
1418
1489
  "bottom-left": "bottomLeft",
1419
1490
  "bottom-right": "bottomRight"
1420
1491
  };
1421
- function RunhumanOverlay({ children, position = "bottom-right" }) {
1422
- const { state, activeJobId } = useSensorState();
1492
+ function RunhumanProvider({
1493
+ children,
1494
+ apiKey,
1495
+ enabled = true,
1496
+ position = "bottom-right",
1497
+ baseUrl,
1498
+ jobId,
1499
+ platform,
1500
+ flushIntervalMs,
1501
+ pollIntervalMs,
1502
+ maxBufferSize,
1503
+ enableDeepLinks,
1504
+ debug
1505
+ }) {
1506
+ const initializedRef = useRef3(false);
1507
+ useEffect2(() => {
1508
+ if (!enabled || initializedRef.current) return;
1509
+ initializedRef.current = true;
1510
+ const config = {
1511
+ apiKey,
1512
+ baseUrl,
1513
+ jobId,
1514
+ platform,
1515
+ flushIntervalMs,
1516
+ pollIntervalMs,
1517
+ maxBufferSize,
1518
+ enableDeepLinks,
1519
+ debug
1520
+ };
1521
+ Runhuman.init(config);
1522
+ return () => {
1523
+ initializedRef.current = false;
1524
+ Runhuman.getInstance().destroy();
1525
+ };
1526
+ }, [enabled]);
1527
+ const { state, activeJobId, disabled } = useSensorState();
1528
+ const showOverlay = enabled && !disabled;
1423
1529
  const posKey = positionMap[position];
1424
1530
  return /* @__PURE__ */ jsxs2(View2, { style: styles.container, children: [
1425
1531
  children,
1426
- state === "idle" && !activeJobId ? /* @__PURE__ */ jsx3(CodeEntryPanel, { position: posKey }) : null,
1427
- state !== "idle" ? /* @__PURE__ */ jsx3(ActiveIndicator, { state, position: posKey }) : null
1532
+ showOverlay && state === "idle" && !activeJobId ? /* @__PURE__ */ jsx3(CodeEntryPanel, { position: posKey }) : null,
1533
+ showOverlay && state !== "idle" ? /* @__PURE__ */ jsx3(ActiveIndicator, { state, position: posKey }) : null
1428
1534
  ] });
1429
1535
  }
1430
1536
  var styles = StyleSheet2.create({
@@ -1434,7 +1540,7 @@ var styles = StyleSheet2.create({
1434
1540
  });
1435
1541
  export {
1436
1542
  Runhuman,
1437
- RunhumanOverlay,
1543
+ RunhumanProvider,
1438
1544
  useSensorState
1439
1545
  };
1440
1546
  //# sourceMappingURL=index.mjs.map