rhombus-node-mcp 0.1.55 → 0.1.57

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.
Files changed (108) hide show
  1. package/dist/api/access-control-tool-api.js +231 -32
  2. package/dist/api/alarm-monitoring-tool-api.js +50 -1
  3. package/dist/api/automated-prompts-tool-api.js +9 -17
  4. package/dist/api/camera-policy-tool-api.js +122 -0
  5. package/dist/api/camera-tool-api.js +3 -5
  6. package/dist/api/camera-uptime-tool-api.js +62 -25
  7. package/dist/api/clips-tool-api.js +45 -9
  8. package/dist/api/create-tool-api.js +55 -7
  9. package/dist/api/door-tool-api.js +74 -13
  10. package/dist/api/entity-lookup-tool-api.js +8 -1
  11. package/dist/api/events-tool-api.js +79 -6
  12. package/dist/api/faces-tool-api.js +112 -7
  13. package/dist/api/get-accessible-apps.js +68 -6
  14. package/dist/api/get-entity-tool-api.js +12 -12
  15. package/dist/api/get-org-information-tool-api.js +2 -2
  16. package/dist/api/guest-management-tool-api.js +75 -1
  17. package/dist/api/location-tool-api.js +120 -28
  18. package/dist/api/lpr-tool-api.js +71 -16
  19. package/dist/api/policy-alerts-tool-api.js +7 -3
  20. package/dist/api/report-tool-api.js +4 -1
  21. package/dist/api/rules-tool-api.js +6 -16
  22. package/dist/api/schedule-tool-api.js +107 -0
  23. package/dist/api/search-tool-api.js +5 -13
  24. package/dist/api/update-tool-api.js +96 -1
  25. package/dist/api/user-tool-api.js +58 -13
  26. package/dist/createServer.js +18 -6
  27. package/dist/filtering-utils.js +421 -29
  28. package/dist/index.js +1 -1
  29. package/dist/network/network.js +122 -10
  30. package/dist/network/org-reference-cache.js +74 -0
  31. package/dist/telemetry/tracingProxy.js +4 -0
  32. package/dist/tools/entity-lookup-tool.js +8 -4
  33. package/dist/tools/get-entity-tool.js +46 -21
  34. package/dist/tools/get-org-information-tool.js +36 -3
  35. package/dist/tools/time-conversion-tool.js +65 -37
  36. package/dist/tools/time-tool.js +18 -4
  37. package/dist/tools/user-tool.js +101 -1
  38. package/dist/tools-console/access-anomaly-tool.js +4 -19
  39. package/dist/tools-console/access-control-tool.js +330 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +73 -1
  41. package/dist/tools-console/analytics-tool.js +4 -2
  42. package/dist/tools-console/automated-prompts-tool.js +2 -0
  43. package/dist/tools-console/badge-timeline-tool.js +4 -21
  44. package/dist/tools-console/camera-policy-tool.js +252 -0
  45. package/dist/tools-console/camera-tool.js +113 -89
  46. package/dist/tools-console/camera-uptime-tool.js +2 -0
  47. package/dist/tools-console/clips-tool.js +23 -31
  48. package/dist/tools-console/count-tool.js +19 -6
  49. package/dist/tools-console/create-camera-policy-tool.js +304 -122
  50. package/dist/tools-console/door-schedule-exception-tool.js +4 -33
  51. package/dist/tools-console/door-tool.js +131 -1
  52. package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
  53. package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
  54. package/dist/tools-console/elements-lost-badge-tool.js +4 -17
  55. package/dist/tools-console/elements-tool.js +4 -29
  56. package/dist/tools-console/events-tool.js +24 -101
  57. package/dist/tools-console/faces-tool.js +255 -27
  58. package/dist/tools-console/guest-management-tool.js +128 -1
  59. package/dist/tools-console/location-tool.js +161 -35
  60. package/dist/tools-console/lost-badge-tool.js +4 -17
  61. package/dist/tools-console/lpr-tool.js +90 -22
  62. package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
  63. package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
  64. package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
  65. package/dist/tools-console/netbox-tool.js +4 -29
  66. package/dist/tools-console/onguard-tool.js +4 -30
  67. package/dist/tools-console/person-tracking-tool.js +4 -2
  68. package/dist/tools-console/policy-alerts-tool.js +6 -28
  69. package/dist/tools-console/reboot-cameras-tool.js +23 -3
  70. package/dist/tools-console/report-tool.js +12 -40
  71. package/dist/tools-console/rules-tool.js +2 -0
  72. package/dist/tools-console/schedule-tool.js +190 -0
  73. package/dist/tools-console/search-tool.js +2 -0
  74. package/dist/tools-console/update-tool.js +300 -168
  75. package/dist/tools-console/user-access-trail-tool.js +2 -0
  76. package/dist/tools-console/user-audit-tool.js +2 -0
  77. package/dist/tools-console/video-walls-tool.js +94 -20
  78. package/dist/types/access-anomaly-tool-types.js +22 -2
  79. package/dist/types/access-control-tool-types.js +116 -1
  80. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  81. package/dist/types/badge-timeline-tool-types.js +18 -3
  82. package/dist/types/camera-policy-tool-types.js +104 -0
  83. package/dist/types/camera-tool-types.js +80 -1
  84. package/dist/types/clips-tool-types.js +27 -3
  85. package/dist/types/create-camera-policy-tool-types.js +275 -26
  86. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  87. package/dist/types/door-tool-types.js +36 -4
  88. package/dist/types/entity-lookup-tool-types.js +6 -1
  89. package/dist/types/events-tools-types.js +24 -16
  90. package/dist/types/faces-tools-types.js +106 -3
  91. package/dist/types/get-entity-tool-types.js +76 -12
  92. package/dist/types/guest-management-tool-types.js +103 -1
  93. package/dist/types/location-tool-types.js +100 -14
  94. package/dist/types/lost-badge-tool-types.js +17 -2
  95. package/dist/types/lpr-tool-types.js +47 -8
  96. package/dist/types/onguard-tool-types.js +21 -1
  97. package/dist/types/policy-alerts-tool-types.js +1 -1
  98. package/dist/types/report-tool-types.js +29 -1
  99. package/dist/types/schedule-tool-types.js +370 -0
  100. package/dist/types/schema.js +479 -9
  101. package/dist/types/time-conversion-tool-types.js +3 -4
  102. package/dist/types/update-tool-types.js +40 -18
  103. package/dist/types/user-tool-types.js +44 -1
  104. package/dist/types/video-walls-tool-types.js +30 -2
  105. package/dist/types/zod-schemas.js +5128 -996
  106. package/dist/util.js +62 -3
  107. package/dist/utils/entity-name-match.js +11 -0
  108. package/package.json +3 -1
@@ -1,4 +1,5 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiWarning, postApi, throwIfApiError } from "../network/network.js";
2
+ import { cachedPostApi } from "../network/org-reference-cache.js";
2
3
  const PERMISSION_RANK = {
3
4
  LIVEONLY: 0,
4
5
  READONLY: 1,
@@ -14,9 +15,7 @@ export async function unlockDoor(doorUuid, requestModifiers, sessionId) {
14
15
  modifiers: requestModifiers,
15
16
  sessionId,
16
17
  });
17
- if (res.error) {
18
- throw new Error(JSON.stringify(res));
19
- }
18
+ throwIfApiError(res);
20
19
  return { success: true, doorUuid };
21
20
  }
22
21
  export async function getAccessControlGroups(requestModifiers, sessionId) {
@@ -26,9 +25,7 @@ export async function getAccessControlGroups(requestModifiers, sessionId) {
26
25
  modifiers: requestModifiers,
27
26
  sessionId,
28
27
  });
29
- if (res.error) {
30
- throw new Error(JSON.stringify(res));
31
- }
28
+ throwIfApiError(res);
32
29
  return (res.groups?.map((group) => ({
33
30
  uuid: group.uuid ?? undefined,
34
31
  name: group.name ?? undefined,
@@ -44,9 +41,7 @@ export async function getCredentialsByUser(userUuid, requestModifiers, sessionId
44
41
  modifiers: requestModifiers,
45
42
  sessionId,
46
43
  });
47
- if (res.error) {
48
- throw new Error(JSON.stringify(res));
49
- }
44
+ throwIfApiError(res);
50
45
  return (res.credentials?.map((cred) => ({
51
46
  uuid: cred.uuid ?? undefined,
52
47
  userUuid: cred.userUuid ?? undefined,
@@ -62,9 +57,7 @@ export async function getLockdownPlans(requestModifiers, sessionId) {
62
57
  modifiers: requestModifiers,
63
58
  sessionId,
64
59
  });
65
- if (res.error) {
66
- throw new Error(JSON.stringify(res));
67
- }
60
+ throwIfApiError(res);
68
61
  return (res.lockdownPlans?.map((plan) => ({
69
62
  uuid: plan.uuid ?? undefined,
70
63
  name: plan.name ?? undefined,
@@ -80,9 +73,7 @@ export async function activateLockdown(locationUuid, lockdownPlanUuid, requestMo
80
73
  modifiers: requestModifiers,
81
74
  sessionId,
82
75
  });
83
- if (res.error) {
84
- throw new Error(JSON.stringify(res));
85
- }
76
+ throwIfApiError(res);
86
77
  return { success: true, locationUuid, action: "activated" };
87
78
  }
88
79
  export async function deactivateLockdown(locationUuid, lockdownPlanUuid, requestModifiers, sessionId) {
@@ -92,9 +83,7 @@ export async function deactivateLockdown(locationUuid, lockdownPlanUuid, request
92
83
  modifiers: requestModifiers,
93
84
  sessionId,
94
85
  });
95
- if (res.error) {
96
- throw new Error(JSON.stringify(res));
97
- }
86
+ throwIfApiError(res);
98
87
  return { success: true, locationUuid, action: "deactivated" };
99
88
  }
100
89
  export async function getDoorScheduleExceptions(locationUuid, requestModifiers, sessionId) {
@@ -104,9 +93,7 @@ export async function getDoorScheduleExceptions(locationUuid, requestModifiers,
104
93
  modifiers: requestModifiers,
105
94
  sessionId,
106
95
  });
107
- if (res.error) {
108
- throw new Error(JSON.stringify(res));
109
- }
96
+ throwIfApiError(res);
110
97
  return (res.exceptions?.map((exc) => ({
111
98
  uuid: exc.uuid ?? undefined,
112
99
  name: exc.name ?? undefined,
@@ -129,9 +116,7 @@ export async function getAccessGrants(locationUuid, requestModifiers, sessionId)
129
116
  modifiers: requestModifiers,
130
117
  sessionId,
131
118
  });
132
- if (res.error) {
133
- throw new Error(JSON.stringify(res));
134
- }
119
+ throwIfApiError(res);
135
120
  const filterNulls = (arr) => arr?.filter((v) => v !== null) ?? [];
136
121
  return (res.accessGrants?.map((grant) => ({
137
122
  uuid: grant.uuid ?? undefined,
@@ -177,7 +162,7 @@ export async function getRemoteUnlockUsers(locationUuid, requestModifiers, sessi
177
162
  modifiers: requestModifiers,
178
163
  sessionId,
179
164
  }),
180
- postApi({
165
+ cachedPostApi({
181
166
  route: "/component/findAccessControlledDoors",
182
167
  body: {},
183
168
  modifiers: requestModifiers,
@@ -190,12 +175,9 @@ export async function getRemoteUnlockUsers(locationUuid, requestModifiers, sessi
190
175
  sessionId,
191
176
  }),
192
177
  ]);
193
- if (permGroupsRes.error)
194
- throw new Error(JSON.stringify(permGroupsRes));
195
- if (doorsRes.error)
196
- throw new Error(JSON.stringify(doorsRes));
197
- if (usersRes.error)
198
- throw new Error(JSON.stringify(usersRes));
178
+ throwIfApiError(permGroupsRes);
179
+ throwIfApiError(doorsRes);
180
+ throwIfApiError(usersRes);
199
181
  const permissionGroups = permGroupsRes.permissionGroups ?? [];
200
182
  const groupMembership = {};
201
183
  for (const [groupUuid, userUuids] of Object.entries(permGroupsRes.groupMembership ?? {})) {
@@ -254,3 +236,220 @@ export async function getRemoteUnlockUsers(locationUuid, requestModifiers, sessi
254
236
  const totalUsers = seenUsers.size;
255
237
  return { doors: doorNames, totalUsers, groups };
256
238
  }
239
+ // ---------------------------------------------------------------------------
240
+ // Access control groups
241
+ // ---------------------------------------------------------------------------
242
+ export async function createAccessControlGroup(name, description, userUuids, requestModifiers, sessionId) {
243
+ const res = await postApi({
244
+ route: "/accesscontrol/createAccessControlGroup",
245
+ body: {
246
+ name,
247
+ description: description || undefined,
248
+ userUuids: userUuids ?? [],
249
+ },
250
+ modifiers: requestModifiers,
251
+ sessionId,
252
+ });
253
+ throwIfApiError(res);
254
+ return {
255
+ success: true,
256
+ uuid: res.group?.uuid ?? undefined,
257
+ memberCount: res.groupMembers?.length ?? userUuids?.length ?? 0,
258
+ };
259
+ }
260
+ export async function updateAccessControlGroup(groupUuid, changes, requestModifiers, sessionId) {
261
+ const res = await postApi({
262
+ route: "/accesscontrol/updateAccessControlGroup",
263
+ body: {
264
+ groupUuid,
265
+ name: changes.name,
266
+ description: changes.description,
267
+ },
268
+ modifiers: requestModifiers,
269
+ sessionId,
270
+ });
271
+ throwIfApiError(res);
272
+ return { success: true, uuid: groupUuid };
273
+ }
274
+ export async function deleteAccessControlGroup(groupUuid, requestModifiers, sessionId) {
275
+ const res = await postApi({
276
+ route: "/accesscontrol/deleteAccessControlGroup",
277
+ body: { groupUuid },
278
+ modifiers: requestModifiers,
279
+ sessionId,
280
+ });
281
+ throwIfApiError(res);
282
+ return { success: true, uuid: groupUuid };
283
+ }
284
+ export async function changeAccessControlGroupMembers(groupUuid, userUuids, action, requestModifiers, sessionId) {
285
+ const res = await postApi({
286
+ route: action === "add"
287
+ ? "/accesscontrol/addUsersToAccessControlGroup"
288
+ : "/accesscontrol/removeUsersFromAccessControlGroup",
289
+ body: { groupUuid, userUuids },
290
+ modifiers: requestModifiers,
291
+ sessionId,
292
+ });
293
+ throwIfApiError(res);
294
+ return { success: true, uuid: groupUuid, userCount: userUuids.length };
295
+ }
296
+ // ---------------------------------------------------------------------------
297
+ // Location access grants
298
+ // ---------------------------------------------------------------------------
299
+ /**
300
+ * `createAccessGrant` / `updateAccessGrant` both take the WHOLE grant object,
301
+ * so update callers must read the current grant and merge. Both can also
302
+ * succeed while reporting doors whose access-control licences are expired or
303
+ * unassigned — those doors silently do not get access, so they are surfaced.
304
+ */
305
+ export async function writeAccessGrant(accessGrant, mode, requestModifiers, sessionId) {
306
+ const res = await postApi({
307
+ route: mode === "create"
308
+ ? "/accesscontrol/createAccessGrant"
309
+ : "/accesscontrol/updateAccessGrant",
310
+ body: {
311
+ accessGrant,
312
+ },
313
+ modifiers: requestModifiers,
314
+ sessionId,
315
+ });
316
+ throwIfApiError(res);
317
+ return {
318
+ success: true,
319
+ uuid: res.accessGrant?.uuid ?? accessGrant.uuid,
320
+ expiredACDLicensesDoorUuids: res.expiredACDLicensesDoorUuids?.filter((value) => !!value) ?? [],
321
+ unassignedACDLicensesDoorUuids: res.unassignedACDLicensesDoorUuids?.filter((value) => !!value) ?? [],
322
+ warningMsg: res.warningMsg ?? undefined,
323
+ };
324
+ }
325
+ export async function deleteAccessGrant(accessGrantUuid, requestModifiers, sessionId) {
326
+ const res = await postApi({
327
+ route: "/accesscontrol/deleteLocationAccessGrant",
328
+ body: {
329
+ accessGrantUuid,
330
+ },
331
+ modifiers: requestModifiers,
332
+ sessionId,
333
+ });
334
+ throwIfApiError(res);
335
+ return { success: true, uuid: accessGrantUuid, warningMsg: res.warningMsg ?? undefined };
336
+ }
337
+ // ---------------------------------------------------------------------------
338
+ // Credentials
339
+ // ---------------------------------------------------------------------------
340
+ /** Attach an existing (unassigned) physical credential to a user. */
341
+ export async function assignCredential(credentialHexValue, userUuid, requestModifiers, sessionId) {
342
+ const res = await postApi({
343
+ route: "/accesscontrol/assignAccessControlCredential",
344
+ body: {
345
+ credentialHexValue,
346
+ userUuid,
347
+ },
348
+ modifiers: requestModifiers,
349
+ sessionId,
350
+ });
351
+ throwIfApiError(res);
352
+ return { success: true, userUuid, warningMsg: apiWarning(res) };
353
+ }
354
+ /**
355
+ * The four credential state changes that share a `{credentialUuid}` body.
356
+ *
357
+ * They are NOT interchangeable and the difference matters operationally:
358
+ * suspend is reversible (unsuspend restores it), revoke detaches the credential
359
+ * from its user but keeps the record, and delete destroys the record entirely.
360
+ */
361
+ export async function changeCredentialState(credentialUuid, action, requestModifiers, sessionId) {
362
+ const routes = {
363
+ revoke: "/accesscontrol/revokeAccessControlCredential",
364
+ suspend: "/accesscontrol/suspendAccessControlCredential",
365
+ unsuspend: "/accesscontrol/unsuspendAccessControlCredential",
366
+ delete: "/accesscontrol/deleteAccessControlCredential",
367
+ };
368
+ const res = await postApi({
369
+ route: routes[action],
370
+ body: {
371
+ credentialUuid,
372
+ },
373
+ modifiers: requestModifiers,
374
+ sessionId,
375
+ });
376
+ throwIfApiError(res);
377
+ return { success: true, uuid: credentialUuid, warningMsg: apiWarning(res) };
378
+ }
379
+ export async function updateCredentialNote(credentialUuid, note, requestModifiers, sessionId) {
380
+ const res = await postApi({
381
+ route: "/accesscontrol/updateAccessControlCredentialNote",
382
+ body: {
383
+ credentialUuid,
384
+ note,
385
+ },
386
+ modifiers: requestModifiers,
387
+ sessionId,
388
+ });
389
+ throwIfApiError(res);
390
+ return { success: true, uuid: credentialUuid, warningMsg: apiWarning(res) };
391
+ }
392
+ // ---------------------------------------------------------------------------
393
+ // Lockdown plans
394
+ // ---------------------------------------------------------------------------
395
+ export async function getLockdownPlan(lockdownPlanUuid, requestModifiers, sessionId) {
396
+ const res = await postApi({
397
+ route: "/accesscontrol/lockdownPlan/getLockdownPlan",
398
+ body: {
399
+ lockdownPlanUuid,
400
+ },
401
+ modifiers: requestModifiers,
402
+ sessionId,
403
+ });
404
+ throwIfApiError(res);
405
+ return res.lockdownPlan ?? undefined;
406
+ }
407
+ /**
408
+ * Rename a lockdown plan.
409
+ *
410
+ * `updateLocationLockdownPlan` takes the plan's door-state map, activation,
411
+ * deactivation and test plans as sibling fields, so a name-only body would blank
412
+ * them — which on a lockdown plan means doors silently stop locking in an
413
+ * emergency. Everything except the name is therefore read back and resent
414
+ * verbatim. Authoring those nested plans from natural language is deliberately
415
+ * NOT exposed.
416
+ */
417
+ export async function renameLockdownPlan(lockdownPlanUuid, name, requestModifiers, sessionId) {
418
+ const existing = await getLockdownPlan(lockdownPlanUuid, requestModifiers, sessionId);
419
+ if (!existing)
420
+ return { success: false, missing: true };
421
+ const res = await postApi({
422
+ route: "/accesscontrol/lockdownPlan/updateLocationLockdownPlan",
423
+ body: {
424
+ lockdownPlanUuid,
425
+ name,
426
+ activationPlan: existing.activationPlan,
427
+ deactivationPlan: existing.deactivationPlan,
428
+ defaultLockdownState: existing.defaultLockdownState,
429
+ doorLockdownStateMap: existing.doorLockdownStateMap,
430
+ physicalAccess: existing.physicalAccess,
431
+ testPlan: existing.testPlan,
432
+ },
433
+ modifiers: requestModifiers,
434
+ sessionId,
435
+ });
436
+ throwIfApiError(res);
437
+ return {
438
+ success: true,
439
+ missing: false,
440
+ uuid: lockdownPlanUuid,
441
+ previousName: existing.name ?? undefined,
442
+ };
443
+ }
444
+ export async function deleteLockdownPlan(lockdownPlanUuid, requestModifiers, sessionId) {
445
+ const res = await postApi({
446
+ route: "/accesscontrol/lockdownPlan/deleteLockdownPlan",
447
+ body: {
448
+ lockdownPlanUuid,
449
+ },
450
+ modifiers: requestModifiers,
451
+ sessionId,
452
+ });
453
+ throwIfApiError(res);
454
+ return { success: true, uuid: lockdownPlanUuid };
455
+ }
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { apiWarning, postApi, throwIfApiError } from "../network/network.js";
2
2
  export async function getOrgAlarmStatus(requestModifiers, sessionId) {
3
3
  const res = await postApi({
4
4
  route: "/alertmonitoring/orgStatus",
@@ -63,3 +63,52 @@ export async function getLocationAlarmStatus(locationUuid, requestModifiers, ses
63
63
  notEnabledReason: res.status?.notEnabledReason ?? undefined,
64
64
  };
65
65
  }
66
+ // ---------------------------------------------------------------------------
67
+ // Alarm monitoring write paths
68
+ //
69
+ // `armMonitoring` / `disarmMonitoring` are deliberately NOT exposed: their
70
+ // `armingRule` / `disarmingRule` bodies (activeSinceMs, armAllUntilMs,
71
+ // armedAlarmPolicies) have semantics that are not documented in the api2 spec,
72
+ // and getting them wrong leaves a real alarm system in a state nobody asked
73
+ // for. enable/disable below are the unambiguous pair, and they require the
74
+ // location's alarm PIN.
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * Turn alarm monitoring on or off for a location.
78
+ *
79
+ * `pin` is the location's alarm PIN, supplied by the user. It is passed straight
80
+ * through to api2 and must never be echoed into a tool result, a note, or a log
81
+ * line.
82
+ */
83
+ export async function setLocationMonitoring(locationUuid, pin, enabled, requestModifiers, sessionId) {
84
+ const res = await postApi({
85
+ route: enabled
86
+ ? "/alertmonitoring/enableMonitoringForLocation"
87
+ : "/alertmonitoring/disableMonitoringForLocation",
88
+ body: {
89
+ locationUuid,
90
+ pin,
91
+ },
92
+ modifiers: requestModifiers,
93
+ sessionId,
94
+ });
95
+ throwIfApiError(res);
96
+ return { success: true, locationUuid, enabled, warningMsg: res.warningMsg ?? undefined };
97
+ }
98
+ export async function changeThreatCaseState(threatCaseUuid, action, requestModifiers, sessionId) {
99
+ const routes = {
100
+ dismiss: "/alertmonitoring/dismissThreatCase",
101
+ cancel: "/alertmonitoring/cancelThreatCase",
102
+ escalate: "/alertmonitoring/escalateThreatCaseToAlarm",
103
+ };
104
+ const res = await postApi({
105
+ route: routes[action],
106
+ body: {
107
+ uuid: threatCaseUuid,
108
+ },
109
+ modifiers: requestModifiers,
110
+ sessionId,
111
+ });
112
+ throwIfApiError(res);
113
+ return { success: true, uuid: threatCaseUuid, action, warningMsg: apiWarning(res) };
114
+ }
@@ -1,4 +1,4 @@
1
- import { postApi } from "../network/network.js";
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
2
  import { ChatFollowUpActionEnum, } from "../types/automated-prompts-tool-types.js";
3
3
  import { formatIsoWithOffset } from "../util.js";
4
4
  function extractNotifyUserUuids(followUpActions) {
@@ -88,8 +88,7 @@ export async function listAutomatedPrompts(pageRequest, requestModifiers, sessio
88
88
  modifiers: requestModifiers,
89
89
  sessionId,
90
90
  });
91
- if (res.error)
92
- throw new Error(JSON.stringify(res));
91
+ throwIfApiError(res);
93
92
  const settingsList = (res.settingsList ?? [])
94
93
  .map((p) => toSummary(p, timeZone))
95
94
  .filter((s) => s !== undefined);
@@ -105,8 +104,7 @@ export async function getAutomatedPrompt(promptUuid, requestModifiers, sessionId
105
104
  modifiers: requestModifiers,
106
105
  sessionId,
107
106
  });
108
- if (res.error)
109
- throw new Error(JSON.stringify(res));
107
+ throwIfApiError(res);
110
108
  return toSummary(res.settings, timeZone);
111
109
  }
112
110
  export async function createAutomatedPrompt(input, requestModifiers, sessionId, timeZone) {
@@ -117,8 +115,7 @@ export async function createAutomatedPrompt(input, requestModifiers, sessionId,
117
115
  modifiers: requestModifiers,
118
116
  sessionId,
119
117
  });
120
- if (res.error)
121
- throw new Error(JSON.stringify(res));
118
+ throwIfApiError(res);
122
119
  return toSummary(res.settings, timeZone);
123
120
  }
124
121
  export async function updateAutomatedPrompt(input, requestModifiers, sessionId, timeZone) {
@@ -129,8 +126,7 @@ export async function updateAutomatedPrompt(input, requestModifiers, sessionId,
129
126
  modifiers: requestModifiers,
130
127
  sessionId,
131
128
  });
132
- if (res.error)
133
- throw new Error(JSON.stringify(res));
129
+ throwIfApiError(res);
134
130
  return toSummary(res.settings, timeZone);
135
131
  }
136
132
  export async function deleteAutomatedPrompt(promptUuid, requestModifiers, sessionId) {
@@ -140,8 +136,7 @@ export async function deleteAutomatedPrompt(promptUuid, requestModifiers, sessio
140
136
  modifiers: requestModifiers,
141
137
  sessionId,
142
138
  });
143
- if (res.error)
144
- throw new Error(JSON.stringify(res));
139
+ throwIfApiError(res);
145
140
  return { ok: true, action: "deleted" };
146
141
  }
147
142
  export async function getAutomatedPromptChatHistory(promptUuid, pageRequest, requestModifiers, sessionId, timeZone) {
@@ -155,8 +150,7 @@ export async function getAutomatedPromptChatHistory(promptUuid, pageRequest, req
155
150
  modifiers: requestModifiers,
156
151
  sessionId,
157
152
  });
158
- if (res.error)
159
- throw new Error(JSON.stringify(res));
153
+ throwIfApiError(res);
160
154
  const chatHistory = (res.chatHistory ?? [])
161
155
  .map((r) => toHistoryEntry(r, timeZone))
162
156
  .filter((e) => e !== undefined);
@@ -175,8 +169,7 @@ export async function shareAutomatedPromptResponse(chatUuid, visibility, request
175
169
  modifiers: requestModifiers,
176
170
  sessionId,
177
171
  });
178
- if (res.error)
179
- throw new Error(JSON.stringify(res));
172
+ throwIfApiError(res);
180
173
  return { ok: true, action: `visibility set to ${visibility}` };
181
174
  }
182
175
  export async function verifyJobScheduled(promptUuid, requestModifiers, sessionId) {
@@ -186,8 +179,7 @@ export async function verifyJobScheduled(promptUuid, requestModifiers, sessionId
186
179
  modifiers: requestModifiers,
187
180
  sessionId,
188
181
  });
189
- if (res.error)
190
- throw new Error(JSON.stringify(res));
182
+ throwIfApiError(res);
191
183
  return {
192
184
  scheduleExpression: res.scheduleExpression ?? undefined,
193
185
  scheduleTimezone: res.scheduleTimezone ?? undefined,
@@ -0,0 +1,122 @@
1
+ import { postApi, throwIfApiError } from "../network/network.js";
2
+ import { describeWeeklyInterval } from "../util.js";
3
+ function mapPolicy(raw) {
4
+ return {
5
+ uuid: raw.uuid ?? undefined,
6
+ name: raw.name ?? undefined,
7
+ description: raw.description ?? undefined,
8
+ defaultActivities: raw.defaultTriggers?.flatMap(trigger => trigger?.activity ? [trigger.activity] : []) ?? [],
9
+ scheduledTriggers: raw.scheduledTriggers?.flatMap(trigger => {
10
+ if (!trigger)
11
+ return [];
12
+ return [
13
+ {
14
+ scheduleUuid: trigger.schedule?.uuid ?? undefined,
15
+ scheduleName: trigger.schedule?.name ?? undefined,
16
+ scheduleWindows: trigger.schedule?.intervalList?.flatMap(interval => {
17
+ const window = describeWeeklyInterval(interval?.minuteOfWeekStart, interval?.minuteOfWeekStop);
18
+ return window ? [window] : [];
19
+ }) ?? [],
20
+ activities: trigger.triggerSet?.flatMap(entry => entry?.activity ? [entry.activity] : []) ?? [],
21
+ },
22
+ ];
23
+ }) ?? [],
24
+ };
25
+ }
26
+ export async function getCameraPolicies(requestModifiers, sessionId) {
27
+ const res = await postApi({
28
+ route: "/policy/getCameraPolicies",
29
+ body: {},
30
+ modifiers: requestModifiers,
31
+ sessionId,
32
+ });
33
+ throwIfApiError(res);
34
+ return {
35
+ policies: res.policies?.flatMap(raw => (raw ? [mapPolicy(raw)] : [])) ?? [],
36
+ warningMsg: res.warningMsg ?? undefined,
37
+ };
38
+ }
39
+ /**
40
+ * api2's `updateCameraPolicy` REPLACES the whole policy object, so any field the
41
+ * caller leaves out is blanked. Callers must read the current policy and merge
42
+ * before calling this — see the tool's update branch.
43
+ */
44
+ export async function updateCameraPolicy(policy, requestModifiers, sessionId) {
45
+ const res = await postApi({
46
+ route: "/policy/updateCameraPolicy",
47
+ body: { policy },
48
+ modifiers: requestModifiers,
49
+ sessionId,
50
+ });
51
+ throwIfApiError(res);
52
+ return {
53
+ updated: { success: true, policyUuid: policy.uuid },
54
+ warningMsg: res.warningMsg ?? undefined,
55
+ };
56
+ }
57
+ export async function deleteCameraPolicy(policyUuid, requestModifiers, sessionId) {
58
+ const res = await postApi({
59
+ route: "/policy/deleteCameraPolicy",
60
+ body: { policyUuid },
61
+ modifiers: requestModifiers,
62
+ sessionId,
63
+ });
64
+ throwIfApiError(res);
65
+ return {
66
+ deleted: { success: true, policyUuid },
67
+ warningMsg: res.warningMsg ?? undefined,
68
+ };
69
+ }
70
+ /**
71
+ * The camera holds its `policyUuid`, not the policy a camera list — so "which
72
+ * cameras does this policy cover" is only answerable by listing cameras and
73
+ * grouping. Callers use this both to enrich reads and to warn before a delete.
74
+ */
75
+ export async function getCamerasByPolicy(requestModifiers, sessionId) {
76
+ const res = await postApi({
77
+ route: "/camera/getMinimalCameraStateList",
78
+ body: {},
79
+ modifiers: requestModifiers,
80
+ sessionId,
81
+ });
82
+ throwIfApiError(res);
83
+ const byPolicy = new Map();
84
+ for (const camera of res.cameraStates ?? []) {
85
+ if (!camera?.policyUuid)
86
+ continue;
87
+ const bucket = byPolicy.get(camera.policyUuid) ?? [];
88
+ bucket.push({ uuid: camera.uuid ?? undefined, name: camera.name ?? undefined });
89
+ byPolicy.set(camera.policyUuid, bucket);
90
+ }
91
+ return byPolicy;
92
+ }
93
+ /**
94
+ * Assign or clear the policy on a set of cameras. Passing a null `policyUuid`
95
+ * with `policyUuidUpdated: true` is how api2 spells "unassign", so the caller
96
+ * must be explicit about which one it means.
97
+ *
98
+ * It must be `null` and not `""`: `policyUuid` is declared
99
+ * `format: RUUID, nullable: true`, so an empty string fails deserialization and
100
+ * api2 rejects the whole bulk body with
101
+ * `HTTP 400 {"msg":"JSON doesn't match expected object structure"}`.
102
+ * Verified against api2.itg on 2026-08-05 — a mocked test cannot catch this.
103
+ */
104
+ export async function setPolicyOnCameras(cameraUuids, policyUuid, requestModifiers, sessionId) {
105
+ const res = await postApi({
106
+ route: "/camera/updateDetailsBulkV2",
107
+ body: {
108
+ cameraBulkDetails: cameraUuids.map(uuid => ({
109
+ uuid,
110
+ policyUuid,
111
+ policyUuidUpdated: true,
112
+ })),
113
+ },
114
+ modifiers: requestModifiers,
115
+ sessionId,
116
+ });
117
+ throwIfApiError(res);
118
+ return {
119
+ cameraCount: cameraUuids.length,
120
+ warningMsg: res.warningMsg ?? undefined,
121
+ };
122
+ }
@@ -1,5 +1,5 @@
1
1
  import { getLogger } from "../logger.js";
2
- import { postApi } from "../network/network.js";
2
+ import { postApi, throwIfApiError } from "../network/network.js";
3
3
  import { removeNullFields } from "../util.js";
4
4
  const logger = getLogger("camera-tool");
5
5
  const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
@@ -142,8 +142,7 @@ export async function getCameraMediaUris(cameraUuid, requestModifiers, sessionId
142
142
  modifiers: requestModifiers,
143
143
  sessionId,
144
144
  });
145
- if (res.error)
146
- throw new Error(JSON.stringify(res));
145
+ throwIfApiError(res);
147
146
  return res;
148
147
  }
149
148
  export async function getCameraAIThresholds(cameraUuid, requestModifiers, sessionId) {
@@ -153,8 +152,7 @@ export async function getCameraAIThresholds(cameraUuid, requestModifiers, sessio
153
152
  modifiers: requestModifiers,
154
153
  sessionId,
155
154
  });
156
- if (res.error)
157
- throw new Error(JSON.stringify(res));
155
+ throwIfApiError(res);
158
156
  return res;
159
157
  }
160
158
  export async function updateCameraSettings(cameraUuid, update, requestModifiers, sessionId) {