rhombus-node-mcp 0.1.56 → 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 (102) 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 +6 -4
  33. package/dist/tools/get-entity-tool.js +40 -20
  34. package/dist/tools/get-org-information-tool.js +30 -2
  35. package/dist/tools/time-conversion-tool.js +59 -36
  36. package/dist/tools/time-tool.js +12 -3
  37. package/dist/tools/user-tool.js +100 -2
  38. package/dist/tools-console/access-anomaly-tool.js +2 -19
  39. package/dist/tools-console/access-control-tool.js +328 -2
  40. package/dist/tools-console/alarm-monitoring-tool.js +72 -2
  41. package/dist/tools-console/analytics-tool.js +2 -2
  42. package/dist/tools-console/badge-timeline-tool.js +2 -21
  43. package/dist/tools-console/camera-policy-tool.js +252 -0
  44. package/dist/tools-console/camera-tool.js +111 -89
  45. package/dist/tools-console/clips-tool.js +21 -31
  46. package/dist/tools-console/count-tool.js +8 -0
  47. package/dist/tools-console/create-camera-policy-tool.js +302 -122
  48. package/dist/tools-console/door-schedule-exception-tool.js +2 -33
  49. package/dist/tools-console/door-tool.js +130 -2
  50. package/dist/tools-console/elements-access-anomaly-tool.js +2 -19
  51. package/dist/tools-console/elements-badge-timeline-tool.js +2 -21
  52. package/dist/tools-console/elements-lost-badge-tool.js +2 -17
  53. package/dist/tools-console/elements-tool.js +2 -29
  54. package/dist/tools-console/events-tool.js +22 -101
  55. package/dist/tools-console/faces-tool.js +254 -28
  56. package/dist/tools-console/guest-management-tool.js +127 -2
  57. package/dist/tools-console/location-tool.js +156 -35
  58. package/dist/tools-console/lost-badge-tool.js +2 -17
  59. package/dist/tools-console/lpr-tool.js +89 -23
  60. package/dist/tools-console/netbox-access-anomaly-tool.js +2 -19
  61. package/dist/tools-console/netbox-badge-timeline-tool.js +2 -21
  62. package/dist/tools-console/netbox-lost-badge-tool.js +2 -17
  63. package/dist/tools-console/netbox-tool.js +2 -29
  64. package/dist/tools-console/onguard-tool.js +2 -30
  65. package/dist/tools-console/person-tracking-tool.js +2 -2
  66. package/dist/tools-console/policy-alerts-tool.js +5 -29
  67. package/dist/tools-console/reboot-cameras-tool.js +17 -2
  68. package/dist/tools-console/report-tool.js +10 -40
  69. package/dist/tools-console/schedule-tool.js +190 -0
  70. package/dist/tools-console/update-tool.js +298 -168
  71. package/dist/tools-console/video-walls-tool.js +93 -21
  72. package/dist/types/access-anomaly-tool-types.js +22 -2
  73. package/dist/types/access-control-tool-types.js +116 -1
  74. package/dist/types/alarm-monitoring-tool-types.js +42 -1
  75. package/dist/types/badge-timeline-tool-types.js +18 -3
  76. package/dist/types/camera-policy-tool-types.js +104 -0
  77. package/dist/types/camera-tool-types.js +80 -1
  78. package/dist/types/clips-tool-types.js +27 -3
  79. package/dist/types/create-camera-policy-tool-types.js +275 -26
  80. package/dist/types/door-schedule-exception-tool-types.js +20 -6
  81. package/dist/types/door-tool-types.js +36 -4
  82. package/dist/types/entity-lookup-tool-types.js +6 -1
  83. package/dist/types/events-tools-types.js +24 -16
  84. package/dist/types/faces-tools-types.js +106 -3
  85. package/dist/types/get-entity-tool-types.js +76 -12
  86. package/dist/types/guest-management-tool-types.js +103 -1
  87. package/dist/types/location-tool-types.js +100 -14
  88. package/dist/types/lost-badge-tool-types.js +17 -2
  89. package/dist/types/lpr-tool-types.js +47 -8
  90. package/dist/types/onguard-tool-types.js +21 -1
  91. package/dist/types/policy-alerts-tool-types.js +1 -1
  92. package/dist/types/report-tool-types.js +29 -1
  93. package/dist/types/schedule-tool-types.js +370 -0
  94. package/dist/types/schema.js +479 -9
  95. package/dist/types/time-conversion-tool-types.js +3 -4
  96. package/dist/types/update-tool-types.js +40 -18
  97. package/dist/types/user-tool-types.js +44 -1
  98. package/dist/types/video-walls-tool-types.js +30 -2
  99. package/dist/types/zod-schemas.js +5128 -996
  100. package/dist/util.js +62 -3
  101. package/dist/utils/entity-name-match.js +11 -0
  102. package/package.json +3 -1
@@ -10,6 +10,12 @@ export var RequestType;
10
10
  RequestType["SEARCH_SIMILAR_FACES"] = "search-similar-faces";
11
11
  RequestType["GET_FACE_MATCHMAKERS"] = "get-face-matchmakers";
12
12
  RequestType["GET_FACE_EVENTS_BY_PERSON"] = "get-face-events-by-person";
13
+ RequestType["CREATE_PERSON"] = "create-person";
14
+ RequestType["UPDATE_PERSON"] = "update-person";
15
+ RequestType["DELETE_PERSON"] = "delete-person";
16
+ RequestType["ADD_PERSON_LABEL"] = "add-person-label";
17
+ RequestType["REMOVE_PERSON_LABEL"] = "remove-person-label";
18
+ RequestType["DELETE_FACE_MATCHMAKER"] = "delete-face-matchmaker";
13
19
  })(RequestType || (RequestType = {}));
14
20
  export const GetRegisteredFacesArgsSchema = z.object({
15
21
  requestType: z.literal(RequestType.GET_REGISTERED_FACES),
@@ -88,13 +94,59 @@ export const GetFaceEventsArgs = z.object({
88
94
  .describe("Search criteria for filtering face events. Only applies to tool calls with requestType 'get-face-events'."),
89
95
  });
90
96
  export const TOOL_ARGS = {
91
- requestType: z.nativeEnum(RequestType),
92
- faceEventFilter: GetFaceEventsArgs,
97
+ requestType: z.nativeEnum(RequestType).describe(`Which face-recognition request to run.
98
+
99
+ "get-face-events" — face sightings; use it for reporting on who was seen by the camera system.
100
+ - **Automatic name resolution:** faceNames accepts partial or first-name-only names (e.g. "Brandon", "Omar"); the tool looks up the registered-faces directory and resolves them to exact names and person UUIDs before searching. The response's "resolvedNames" field shows what each queried name matched (null = no match).
101
+ - Filter with faceNames, hasEmbedding, hasName, labels, locationUuids, personUuids, and a time range via searchFilter.timestampFilter.rangeStart / rangeEnd, which are ISO 8601 strings WITH a UTC offset (e.g. "2026-08-03T00:00:00-07:00") — not epoch milliseconds.
102
+ - **For "who was seen" questions, set searchFilter.hasName to true.** Unnamed detections cannot answer a who-question and they consume the page budget: an unfiltered page is typically ~85% nameless, so the named people you need get pushed onto later pages.
103
+ - **Read the faceEventSummary field in the response.** It lists every distinct identified person on the page with their event count and first/last sighting, already deduplicated. Enumerate people from it rather than by scanning getFaceEventsResponse by hand, and page until morePagesAvailable is false before saying who was or wasn't seen.
104
+ - For all face events at a location, pass only the location UUID in searchFilter and NO device UUIDs (searchFilter.deviceUuids), so the API returns every face detected there. If that location has no face events the tool falls back to an org-wide search and says so in the "note" field — read it before attributing results to the requested location.
105
+ - When the user asks about a specific person at a location (e.g. "Jane Doe at Main Office"), call get-registered-faces first, find the best match, then call get-face-events with that precise name — this request expects names exactly as stored.
106
+
107
+ "get-registered-faces" — every person (registered face) known to the org, each with a "labels" array showing the label groups they belong to. Returns ALL people regardless of any timestamp filter.
108
+
109
+ "get-person-labels" — a mapping of person UUIDs to their assigned labels across the org. Use it to discover what label groups exist; for a group question ("was anyone from Engineering seen today?") get the labels first, then query face events filtered by those personUuids or labels.`),
110
+ faceEventFilter: GetFaceEventsArgs.partial().nullish().describe("For get-face-events, put pageRequest and searchFilter inside this object: " +
111
+ "{pageRequest: {lastEvaluatedKey: null, maxPageSize: 200}, searchFilter: {...}}. " +
112
+ "Omit or pass null for get-registered-faces and other requests that do not search events."),
113
+ searchFilter: GetFaceEventsArgs.shape.searchFilter.nullish().describe("Compatibility alias for faceEventFilter.searchFilter. Prefer nesting searchFilter " +
114
+ "inside faceEventFilter; leave this null when using the nested form. " +
115
+ "If both filters are provided, they must be identical."),
116
+ // pageRequest and searchFilter belong inside faceEventFilter. Everything
117
+ // below is a top-level sibling — nesting these under faceEventFilter is the
118
+ // most common way this tool gets called wrong and costs a rejected round trip.
93
119
  timeZone: z
94
120
  .string()
95
121
  .describe("The timezone for formatting timestamps which should come from the location of the camera for the face event. This is necessary for the tool to produce accurate formatted timestamps."),
96
122
  faceEventUuid: z.string().nullable().describe("UUID of a face event to search similar faces from. Required for 'search-similar-faces'."),
97
- personUuid: z.string().nullable().describe("UUID of a person to get face events for. Required for 'get-face-events-by-person'."),
123
+ personUuid: z
124
+ .string()
125
+ .nullable()
126
+ .describe("UUID of a person. Required for 'get-face-events-by-person', 'update-person', 'delete-person', 'add-person-label' and 'remove-person-label'. Get it from 'get-registered-faces' — do not guess one, because acting on the wrong person's biometric record is not visible in the response."),
127
+ personName: z
128
+ .string()
129
+ .nullable()
130
+ .describe("A person's name. Required for 'create-person'; for 'update-person' it is the new name (omit to leave it unchanged)."),
131
+ personEmail: z
132
+ .string()
133
+ .nullable()
134
+ .describe("A person's email address. Optional for 'update-person'; omit to leave it unchanged. " +
135
+ "CAVEAT: the Rhombus API never returns a person's email on any read path (verified " +
136
+ "2026-08-05), so a stored email cannot be displayed or verified afterwards — tell the " +
137
+ "user that if they ask to see it."),
138
+ personLabel: z
139
+ .string()
140
+ .nullable()
141
+ .describe("A single person label. Required for 'add-person-label' and 'remove-person-label'."),
142
+ faceId: z
143
+ .string()
144
+ .nullable()
145
+ .describe("The id of ONE enrolled face image (a 'face matchmaker'). Required for 'delete-face-matchmaker'. Get it from 'get-face-matchmakers'. A person can have several enrolled faces; deleting one leaves the person and their other faces in place."),
146
+ confirmDelete: z
147
+ .boolean()
148
+ .nullable()
149
+ .describe("Required to be true for 'delete-person' and 'delete-face-matchmaker'. Both destroy enrolled biometric data and cannot be undone — re-enrolling needs new photos. They refuse without explicit confirmation from the user."),
98
150
  includeFields: INCLUDE_FIELDS_ARG,
99
151
  filterBy: FILTER_BY_ARG,
100
152
  };
@@ -143,6 +195,9 @@ export const OUTPUT_SCHEMA = z.object({
143
195
  .optional(z.array(z.object({
144
196
  createdOn: z.optional(z.number()),
145
197
  name: z.optional(z.string()),
198
+ email: z
199
+ .optional(z.string())
200
+ .describe("Currently always absent — api2 does not serialize Person.email on any read path (verified 2026-08-05). Mapped so it appears automatically if that changes."),
146
201
  orgUuid: z.optional(z.string()),
147
202
  updatedOn: z.optional(z.number()),
148
203
  uuid: z.optional(z.string()),
@@ -174,6 +229,54 @@ export const OUTPUT_SCHEMA = z.object({
174
229
  eventTimestamp: z.string().optional(),
175
230
  deviceUuid: z.string().optional(),
176
231
  })).optional().describe("Face events for a specific person"),
232
+ faceEventSummary: z
233
+ .optional(z.object({
234
+ totalEventsThisPage: z.number().describe("How many face events this page contains."),
235
+ namedEvents: z.number().describe("How many of them resolved to a registered person."),
236
+ unnamedEvents: z
237
+ .number()
238
+ .describe("How many were unrecognized faces. These cannot answer a 'who' question."),
239
+ identifiedPeople: z
240
+ .array(z.object({
241
+ name: z.string(),
242
+ eventCount: z.number(),
243
+ firstSeen: z.string().describe("Human-readable timestamp of their earliest sighting on this page."),
244
+ lastSeen: z.string().describe("Human-readable timestamp of their latest sighting on this page."),
245
+ }))
246
+ .describe("Every DISTINCT identified person on this page, most-seen first. This is the complete roster for this page — report all of these names, not a sample."),
247
+ morePagesAvailable: z
248
+ .boolean()
249
+ .describe("True when lastEvaluatedKey is set. More people may appear on later pages; do not state who was or wasn't seen until this is false."),
250
+ }))
251
+ .describe("Pre-computed roster of distinct people on this page of face events. Enumerate people from here instead of deduplicating getFaceEventsResponse by hand."),
177
252
  lastEvaluatedKey: z.string().optional().describe("For paginated requests, this is the returned last evaluated key that can be passed back in on the next request to get the next page of results"),
253
+ created: z
254
+ .object({ success: z.boolean().optional(), uuid: z.string().optional() })
255
+ .optional()
256
+ .describe("Result of 'create-person'."),
257
+ updated: z
258
+ .object({
259
+ success: z.boolean().optional(),
260
+ uuid: z.string().optional(),
261
+ label: z.string().optional(),
262
+ })
263
+ .optional()
264
+ .describe("Result of 'update-person' or a person-label change."),
265
+ deleted: z
266
+ .object({
267
+ success: z.boolean().optional(),
268
+ uuid: z.string().optional(),
269
+ faceId: z.string().optional(),
270
+ })
271
+ .optional()
272
+ .describe("Result of 'delete-person' or 'delete-face-matchmaker'."),
273
+ warningMsg: z
274
+ .string()
275
+ .optional()
276
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
277
+ note: z
278
+ .string()
279
+ .optional()
280
+ .describe("Diagnostic note about why a result set may be empty, incomplete, or scoped differently than requested. Read it before attributing results to the filters you asked for."),
178
281
  error: z.optional(z.string()),
179
282
  });
@@ -1,28 +1,92 @@
1
1
  import { z } from "zod";
2
- import { createUuidSchema } from "../types.js";
3
2
  import DeviceType from "./deviceType.js";
4
3
  import { TempUnit } from "../utils/temp.js";
5
- const filterByObjectSchema = z.object({
6
- locationUuids: z
7
- .array(createUuidSchema())
8
- .nullish()
9
- .describe("The UUIDs of the locations to filter by. Set to null or an empty array to not filter by location."),
10
- });
11
4
  export const TOOL_ARGS = {
12
5
  entityTypes: z
13
6
  .array(z.nativeEnum(DeviceType).describe("The entity type to retreive"))
14
7
  .describe("What type of entities to retrieve."),
15
- filterBy: z
16
- .union([filterByObjectSchema, z.null()])
17
- .optional()
18
- .transform((v) => v ?? { locationUuids: null })
19
- .describe("Additional filters that can be applied to the result. Omit or pass null for no filtering."),
8
+ detail: z
9
+ .enum(["core", "full"])
10
+ .nullish()
11
+ .transform((v) => v ?? "core")
12
+ .describe('Level of per-device detail. "core" (default) returns each device\'s key fields: uuid, name, connection/health status, location, camera associations, temperature, door capabilities. "full" returns every field (model, firmware, serial, network info, ...) — when describing a single device, combine it with the filterBy output filter (e.g. [{field:"name", op:"contains", value:"..."}]) to avoid a huge response.'),
20
13
  timeZone: z
21
14
  .string()
22
15
  .describe("The timezone for formatting timestamps. This is necessary for the tool to produce accurate formatted timestamps."),
23
16
  tempUnit: z
24
17
  .nativeEnum(TempUnit)
25
18
  .nullable()
19
+ .default(null)
26
20
  .describe("The unit of temperature to return, if applicable. Default is Celsius."),
27
21
  };
28
22
  const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
23
+ // ---------------------------------------------------------------------------
24
+ // Output schema — drives the filtering proxy's auto-generated catalog of
25
+ // available includeFields / filterBy paths (unbilled: it lands on the param
26
+ // description). Fields mirror what "core" detail actually returns per type.
27
+ // ---------------------------------------------------------------------------
28
+ const ConnectedDeviceSchema = z.object({
29
+ uuid: z.string().optional(),
30
+ name: z.string().optional(),
31
+ connected: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("True = online, false = offline. Filter on this for offline/online questions."),
35
+ connectionStatus: z
36
+ .string()
37
+ .optional()
38
+ .describe('Raw platform status: GREEN, YELLOW, ORANGE or RED (RED = disconnected).'),
39
+ healthStatus: z.string().optional(),
40
+ healthStatusDetails: z.string().optional(),
41
+ locationUuid: z.string().optional(),
42
+ floorNumber: z.number().optional(),
43
+ policyUuid: z.string().optional(),
44
+ });
45
+ const ClimateSensorSchema = ConnectedDeviceSchema.extend({
46
+ // NUMBER, not a formatted string: the api layer converts the upstream
47
+ // `temperatureCelcius` with tempFunc() and emits the raw value in whatever
48
+ // unit `tempUnit` asked for. Declaring it a string made the SDK reject every
49
+ // environmental-sensor call with "-32602 Output validation error" — the
50
+ // proxy's deepOptionalizeSchema relaxes required-ness, never scalar types.
51
+ temperature: z
52
+ .number()
53
+ .optional()
54
+ .describe("Temperature in the unit requested via the tempUnit arg (Celsius by default)."),
55
+ humidity: z.number().optional(),
56
+ batteryStatus: z.string().optional(),
57
+ });
58
+ const AccessControlledDoorSchema = z.object({
59
+ uuid: z.string().optional(),
60
+ name: z.string().optional(),
61
+ locationUuid: z.string().optional(),
62
+ policyUuid: z.string().optional(),
63
+ remoteUnlockEnabled: z.boolean().optional(),
64
+ geofenceEnabled: z.boolean().optional(),
65
+ associatedCameras: z.array(z.string()).optional(),
66
+ });
67
+ export const OUTPUT_SCHEMA = z.object({
68
+ cameras: z.array(ConnectedDeviceSchema).optional(),
69
+ camerasCount: z.number().optional(),
70
+ doorbellCameras: z.array(ConnectedDeviceSchema).optional(),
71
+ doorbellCamerasCount: z.number().optional(),
72
+ badgeReaders: z.array(ConnectedDeviceSchema).optional(),
73
+ badgeReadersCount: z.number().optional(),
74
+ accessControlledDoors: z.array(AccessControlledDoorSchema).optional(),
75
+ accessControlledDoorsCount: z.number().optional(),
76
+ audioGateways: z.array(ConnectedDeviceSchema).optional(),
77
+ audioGatewaysCount: z.number().optional(),
78
+ doorStates: z.array(ConnectedDeviceSchema).optional(),
79
+ doorStatesCount: z.number().optional(),
80
+ climateStates: z.array(ClimateSensorSchema).optional(),
81
+ climateStatesCount: z.number().optional(),
82
+ occupancySensorStates: z.array(ConnectedDeviceSchema).optional(),
83
+ occupancySensorStatesCount: z.number().optional(),
84
+ buttonStates: z.array(ConnectedDeviceSchema).optional(),
85
+ buttonStatesCount: z.number().optional(),
86
+ keypadStates: z.array(ConnectedDeviceSchema).optional(),
87
+ keypadStatesCount: z.number().optional(),
88
+ minimalEnvironmentalGatewayStates: z.array(ConnectedDeviceSchema).optional(),
89
+ minimalEnvironmentalGatewayStatesCount: z.number().optional(),
90
+ filterByWarnings: z.array(z.string()).optional(),
91
+ error: z.string().optional(),
92
+ });
@@ -5,7 +5,23 @@ export var GuestManagementRequestType;
5
5
  GuestManagementRequestType["GET_ALL_GUESTS"] = "get-all-guests";
6
6
  GuestManagementRequestType["GET_ACTIVITY_LOGS"] = "get-activity-logs";
7
7
  GuestManagementRequestType["GET_ACTIVITIES_FOR_LOCATION"] = "get-activities-for-location";
8
+ GuestManagementRequestType["CREATE_GUEST"] = "create-guest";
9
+ GuestManagementRequestType["DELETE_GUEST"] = "delete-guest";
10
+ GuestManagementRequestType["GET_GUEST_PASSES"] = "get-guest-passes";
11
+ GuestManagementRequestType["EXPIRE_GUEST_PASS"] = "expire-guest-pass";
12
+ GuestManagementRequestType["SUSPEND_GUEST_PASS"] = "suspend-guest-pass";
13
+ GuestManagementRequestType["UNSUSPEND_GUEST_PASS"] = "unsuspend-guest-pass";
14
+ GuestManagementRequestType["DELETE_GUEST_PASS"] = "delete-guest-pass";
8
15
  })(GuestManagementRequestType || (GuestManagementRequestType = {}));
16
+ /** api2's `GuestTypeEnum`. Anything outside this list is rejected upstream. */
17
+ export const GUEST_TYPES = [
18
+ "CONTRACTOR",
19
+ "INTERVIEWER",
20
+ "GENERAL_VISITOR",
21
+ "UNPLANNED_VISITOR",
22
+ "PARTNER",
23
+ "MEETINGS",
24
+ ];
9
25
  export const TOOL_ARGS = {
10
26
  requestType: z
11
27
  .nativeEnum(GuestManagementRequestType)
@@ -13,7 +29,47 @@ export const TOOL_ARGS = {
13
29
  locationUuid: z
14
30
  .string()
15
31
  .nullable()
16
- .describe("Location UUID. Required for 'get-activities-for-location'."),
32
+ .describe("Location UUID. Required for 'get-activities-for-location' and 'create-guest'."),
33
+ guestFirstName: z
34
+ .string()
35
+ .nullable()
36
+ .describe("The guest's first name. Required for 'create-guest'."),
37
+ guestLastName: z
38
+ .string()
39
+ .nullable()
40
+ .describe("The guest's last name. Required for 'create-guest'."),
41
+ guestEmail: z
42
+ .string()
43
+ .nullable()
44
+ .describe("The guest's email address. Required for 'create-guest' and 'delete-guest'. A guest is identified by email, not a UUID, so this has to be exact — read it back to the user before creating, since an invitation may be sent to it."),
45
+ guestCompanyName: z
46
+ .string()
47
+ .nullable()
48
+ .describe("The guest's company. Optional for 'create-guest'."),
49
+ guestType: z
50
+ .enum(GUEST_TYPES)
51
+ .nullable()
52
+ .describe(`The kind of visit. Optional for 'create-guest'; defaults to GENERAL_VISITOR. One of: ${GUEST_TYPES.join(", ")}.`),
53
+ hostUserUuid: z
54
+ .string()
55
+ .nullable()
56
+ .describe("UUID of the Rhombus user hosting the guest. Optional for 'create-guest'. Resolve the host's name with user-tool — do not guess."),
57
+ accessStartTimeMs: z
58
+ .number()
59
+ .nullable()
60
+ .describe("When the guest's access begins (ms since epoch). Optional for 'create-guest'."),
61
+ accessEndTimeMs: z
62
+ .number()
63
+ .nullable()
64
+ .describe("When the guest's access ends (ms since epoch). Optional for 'create-guest', but a guest with no end time keeps access indefinitely — say so if it is left unset."),
65
+ guestPassUuid: z
66
+ .string()
67
+ .nullable()
68
+ .describe("UUID of a guest pass. Required for 'expire-guest-pass', 'suspend-guest-pass', 'unsuspend-guest-pass' and 'delete-guest-pass'. Get it from 'get-guest-passes' — do not guess one."),
69
+ confirmDelete: z
70
+ .boolean()
71
+ .nullable()
72
+ .describe("Required to be true for 'delete-guest' and 'delete-guest-pass'. Both are irreversible; for a pass, 'expire-guest-pass' ends access while keeping the record and history."),
17
73
  startTimeMs: z
18
74
  .number()
19
75
  .nullable()
@@ -53,5 +109,51 @@ export const OUTPUT_SCHEMA = z.object({
53
109
  .array(GuestActivitySchema)
54
110
  .optional()
55
111
  .describe("List of guest activity log entries"),
112
+ guestPasses: z
113
+ .array(z.object({
114
+ uuid: z.string().optional(),
115
+ locationUuid: z.string().optional(),
116
+ lifecycle: z
117
+ .string()
118
+ .optional()
119
+ .describe("ACTIVE, SUSPENDED, EXPIRED or ARCHIVED."),
120
+ note: z.string().optional(),
121
+ passStartTimeMs: z.number().optional(),
122
+ principalType: z.string().optional(),
123
+ }))
124
+ .optional()
125
+ .describe("Guest passes in the organization."),
126
+ created: z
127
+ .object({ success: z.boolean().optional(), email: z.string().optional() })
128
+ .optional()
129
+ .describe("Result of 'create-guest'."),
130
+ deleted: z
131
+ .object({
132
+ success: z.boolean().optional(),
133
+ email: z.string().optional(),
134
+ uuid: z.string().optional(),
135
+ })
136
+ .optional()
137
+ .describe("Result of 'delete-guest' or 'delete-guest-pass'."),
138
+ guestPassChange: z
139
+ .object({
140
+ success: z.boolean().optional(),
141
+ uuid: z.string().optional(),
142
+ action: z.string().optional(),
143
+ })
144
+ .optional()
145
+ .describe("Result of a guest pass state change."),
146
+ lastEvaluatedKey: z
147
+ .string()
148
+ .optional()
149
+ .describe("Pagination cursor — more guest passes exist when this is set."),
150
+ note: z
151
+ .string()
152
+ .optional()
153
+ .describe("A caveat about this result that the user needs to be told."),
154
+ warningMsg: z
155
+ .string()
156
+ .optional()
157
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
56
158
  error: z.string().optional().describe("An error message if the request failed."),
57
159
  });
@@ -1,20 +1,106 @@
1
1
  import { z } from "zod";
2
+ export var LocationToolAction;
3
+ (function (LocationToolAction) {
4
+ LocationToolAction["GET"] = "get";
5
+ LocationToolAction["CREATE"] = "create";
6
+ LocationToolAction["UPDATE"] = "update";
7
+ LocationToolAction["DELETE"] = "delete";
8
+ LocationToolAction["GET_LABELS"] = "get-labels";
9
+ LocationToolAction["ADD_LABEL"] = "add-label";
10
+ LocationToolAction["REMOVE_LABEL"] = "remove-label";
11
+ })(LocationToolAction || (LocationToolAction = {}));
2
12
  export const TOOL_ARGS = {
3
- action: z.enum(["get", "update", "create", "get-labels"]),
4
- locationUpdate: z
5
- .object({
6
- uuid: z.string(),
7
- name: z.string(),
8
- })
9
- .nullable(),
10
- locationName: z.string().nullable().describe("Name for the new location. Required for 'create'."),
11
- locationAddress: z.string().nullable().describe("Address for the location. Optional for 'create' and 'update'."),
12
- locationUuid: z.string().nullable().describe("UUID of the location. Required for 'update'."),
13
+ action: z
14
+ .nativeEnum(LocationToolAction)
15
+ .describe("'get' = list all locations with their address, timezone and labels; " +
16
+ "'create' = create a location (requires locationName); " +
17
+ "'update' = change a location's name, address, timezone and/or labels (requires locationUuid); " +
18
+ "'delete' = delete a location (requires locationUuid); " +
19
+ "'get-labels' = list all location labels used in the org; " +
20
+ "'add-label' / 'remove-label' = attach or detach one label on one location (requires locationUuid and label)."),
21
+ locationUuid: z
22
+ .string()
23
+ .nullable()
24
+ .describe("UUID of the location. Required for 'update', 'delete', 'add-label' and 'remove-label'. Get it from 'get' — do not guess one."),
25
+ locationName: z
26
+ .string()
27
+ .nullable()
28
+ .describe("For 'get', optional name search ignoring case, spaces and punctuation (e.g. 'iceblocks' finds 'Ice Blocks - Headquarters'); omit to list all locations. Required for 'create'; for 'update' it is the new name (omit to leave the name unchanged)."),
29
+ locationAddress: z
30
+ .string()
31
+ .nullable()
32
+ .describe("Street address for the location. Optional for 'create' and 'update'. For 'update', omit to leave the address unchanged."),
33
+ postalCode: z
34
+ .string()
35
+ .nullable()
36
+ .describe("Postal/ZIP code. Optional for 'update'; omit to leave it unchanged."),
37
+ countryCode: z
38
+ .string()
39
+ .nullable()
40
+ .describe("Two-letter ISO country code, e.g. \"US\". Optional for 'update'; omit to leave it unchanged."),
41
+ timezoneId: z
42
+ .string()
43
+ .nullable()
44
+ .describe('IANA timezone id for the location, e.g. "America/Los_Angeles". Optional for \'update\'; omit to leave it unchanged. ' +
45
+ "A location's timezone decides how its schedules and reports are interpreted, so changing it shifts every schedule at that location — say so before doing it."),
46
+ label: z
47
+ .string()
48
+ .nullable()
49
+ .describe("A single label name. Required for 'add-label' and 'remove-label'."),
50
+ confirmDelete: z
51
+ .boolean()
52
+ .nullable()
53
+ .describe("Only for 'delete'. Deleting a location is not reversible and affects every device assigned to it, so 'delete' refuses by default. Set true to delete anyway, after the user has confirmed."),
13
54
  };
14
55
  const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
56
+ const LOCATION_OUTPUT = z.object({
57
+ uuid: z.string().optional(),
58
+ name: z.string().optional(),
59
+ address: z
60
+ .string()
61
+ .optional()
62
+ .describe("The address parts joined for reading. Prefer this over reassembling them."),
63
+ address1: z.string().optional(),
64
+ address2: z.string().optional(),
65
+ postalCode: z.string().optional(),
66
+ countryCode: z.string().optional(),
67
+ timezone: z
68
+ .string()
69
+ .optional()
70
+ .describe("IANA timezone id. Schedules and reports at this location are interpreted in it."),
71
+ labels: z.array(z.string()).optional(),
72
+ latitude: z.number().optional(),
73
+ longitude: z.number().optional(),
74
+ policyUuid: z.string().optional(),
75
+ });
15
76
  export const OUTPUT_SCHEMA = z.object({
16
- createdLocation: z.object({ uuid: z.string().optional(), success: z.boolean().optional() }).optional(),
17
- updatedLocation: z.object({ success: z.boolean().optional() }).optional(),
18
- locationLabels: z.array(z.object({ uuid: z.string().optional(), name: z.string().optional() })).optional(),
19
- error: z.string().optional(),
77
+ locations: z.array(LOCATION_OUTPUT).optional().describe("All locations in the organization."),
78
+ createdLocation: z
79
+ .object({ uuid: z.string().optional(), success: z.boolean().optional() })
80
+ .optional(),
81
+ updated: z
82
+ .object({ success: z.boolean().optional(), locationUuid: z.string().optional() })
83
+ .optional(),
84
+ deleted: z
85
+ .object({ success: z.boolean().optional(), locationUuid: z.string().optional() })
86
+ .optional(),
87
+ labelChanged: z
88
+ .object({
89
+ success: z.boolean().optional(),
90
+ locationUuid: z.string().optional(),
91
+ label: z.string().optional(),
92
+ })
93
+ .optional(),
94
+ locationLabels: z
95
+ .array(z.object({ uuid: z.string().optional(), name: z.string().optional() }))
96
+ .optional(),
97
+ note: z
98
+ .string()
99
+ .optional()
100
+ .describe("A caveat about this result that the user needs to be told."),
101
+ warningMsg: z
102
+ .string()
103
+ .optional()
104
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
105
+ error: z.string().optional().describe("An error message if the request failed."),
20
106
  });
@@ -1,6 +1,20 @@
1
1
  import { z } from "zod";
2
2
  import { createUuidSchema } from "../types.js";
3
3
  import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
4
+ /**
5
+ * Description shared by the three lost-badge tools (OnGuard / Elements /
6
+ * NetBox), which differ only in vendor. Kept short because tool descriptions
7
+ * are billed on every LLM call even while deferred, and this text is
8
+ * duplicated across all three siblings; presentation guidance and the
9
+ * reliability caveat live on the arguments below. See PERF_MASTER_PLAN P2 #4a.
10
+ */
11
+ export function buildLostBadgeToolDescription(vendor) {
12
+ return `
13
+ Lost / stolen-badge live response for ${vendor} access. Use for "a lost badge was just used — who is it and where did they go?", or to review lost/inactive-badge use over a window.
14
+
15
+ For each lost/inactive-badge use it returns cardholderOfRecord (who the badge belongs to — may NOT be who used it) and badgeStatus, the door deviceUuid + time with clip/still hints, facesAtDoor (the face captured at the door — a recognized name, or UNIDENTIFIED, which on a valid badge is the strongest stolen/shared-badge signal), and sightings of that same face tracked across cameras in time order, ending in lastKnownSighting.
16
+ `;
17
+ }
4
18
  export const TOOL_ARGS = {
5
19
  area: z.string().nullable().describe("Optional: restrict to events entering this area (full-text)."),
6
20
  locationUuids: z.array(createUuidSchema()).nullable().describe("Optional: restrict to these location UUIDs."),
@@ -9,7 +23,8 @@ export const TOOL_ARGS = {
9
23
  .string()
10
24
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
11
25
  .nullable()
12
- .describe("Start of the window to scan for lost/inactive-badge use (inclusive). " + ISOTimestampFormatDescription),
26
+ .describe('Start of the window to scan for lost/inactive-badge use (inclusive). Resolve relative phrasing like "in the last hour" with time-tool first, then pass ISO 8601 here. ' +
27
+ ISOTimestampFormatDescription),
13
28
  endTime: z
14
29
  .string()
15
30
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
@@ -18,7 +33,7 @@ export const TOOL_ARGS = {
18
33
  faceWindowSeconds: z
19
34
  .number()
20
35
  .nullable()
21
- .describe("± seconds around each badge event to look for the face at the door (default 30)."),
36
+ .describe("± seconds around each badge event to look for the face at the door (default 30). To present the result: show the door still/clip (camera-tool requestType \"image\" / clips-tool \"createClip\" with the returned hints), the face at the door, and the cross-camera track to the last-known location. Detection and door evidence are reliable; the face track depends on face-recognition coverage, so treat it as investigative, not proof."),
22
37
  limit: z.number().nullable().describe("Max badge events to scan in the window (default 50)."),
23
38
  timeZone: z
24
39
  .string()
@@ -8,6 +8,10 @@ export var LprToolRequestType;
8
8
  LprToolRequestType["GET_VEHICLE_LABELS"] = "get-vehicle-labels";
9
9
  LprToolRequestType["SEARCH_LICENSE_PLATES"] = "search-license-plates";
10
10
  LprToolRequestType["SAVE_VEHICLE"] = "save-vehicle";
11
+ LprToolRequestType["UPDATE_VEHICLE"] = "update-vehicle";
12
+ LprToolRequestType["DELETE_VEHICLE"] = "delete-vehicle";
13
+ LprToolRequestType["ADD_VEHICLE_LABEL"] = "add-vehicle-label";
14
+ LprToolRequestType["REMOVE_VEHICLE_LABEL"] = "remove-vehicle-label";
11
15
  })(LprToolRequestType || (LprToolRequestType = {}));
12
16
  export const VehicleEventsArgs = z.object({
13
17
  deviceUuidFilter: z
@@ -33,7 +37,7 @@ export const VehicleEventsArgs = z.object({
33
37
  message: "Invalid datetime string. Expected ISO 8601 format.",
34
38
  offset: true,
35
39
  })
36
- .describe("The end of the time range (inclusive) for filtering license plate events." +
40
+ .describe("The start of the time range (inclusive) for filtering license plate events." +
37
41
  ISOTimestampFormatDescription),
38
42
  endTime: z
39
43
  .string()
@@ -41,21 +45,38 @@ export const VehicleEventsArgs = z.object({
41
45
  message: "Invalid datetime string. Expected ISO 8601 format.",
42
46
  offset: true,
43
47
  })
44
- .describe("The start of the time range (inclusive) for filtering license plate events." +
48
+ .describe("The end of the time range (inclusive) for filtering license plate events." +
45
49
  ISOTimestampFormatDescription),
46
50
  });
47
51
  export const TOOL_ARGS = {
48
52
  requestType: z
49
53
  .nativeEnum(LprToolRequestType)
50
- .describe("Org LPR operation (vehicle events, saved vehicles, labels, plate search, save vehicle). Per-camera VOD timeline seekpoints use events-tool (eventType camera)."),
51
- vehicleEventsArgs: VehicleEventsArgs.nullable().describe("Only necessary for requestType 'get-vehicle-events'"),
54
+ .describe(`Org LPR operation (vehicle events, saved vehicles, labels, plate search, save vehicle). Per-camera VOD timeline seekpoints use events-tool (eventType camera).
55
+ - get-vehicle-events: detected plate events. Recognition can be a PARTIAL match — keep that in mind when asserting a plate was seen. When a saved vehicle has a label, its events carry that label.
56
+ - get-saved-vehicles: plates users saved (optionally named/labelled).
57
+ - get-vehicle-labels: the org's vehicle labels. For any label question, best practice is get-vehicle-labels first, then get-vehicle-events.
58
+ Pair events to locations by NAME via location-tool — never show location UUIDs in responses or reports.`),
59
+ vehicleEventsArgs: VehicleEventsArgs.nullable().describe("Only necessary for requestType 'get-vehicle-events'. Can return a lot of data — if the user gives no time range, 7 days is a reasonable default."),
52
60
  timeZone: z
53
61
  .string()
54
62
  .describe("The timezone for formatting timestamps which should come from the location of the device for the LPR event, or the user's timezone. This is necessary for the tool to produce accurate formatted timestamps."),
55
63
  licensePlateQuery: z.string().nullable().describe("License plate number to search. Required for 'search-license-plates'."),
56
- vehicleName: z.string().nullable().describe("Name for the vehicle. Required for 'save-vehicle'."),
57
- vehicleLicensePlate: z.string().nullable().describe("License plate for the vehicle. Required for 'save-vehicle'."),
58
- vehicleDescription: z.string().nullable().describe("Description for the vehicle. Optional for 'save-vehicle'."),
64
+ vehicleName: z
65
+ .string()
66
+ .nullable()
67
+ .describe("Name for the vehicle. Required for 'save-vehicle'; for 'update-vehicle' it is the new name (omit to leave it unchanged)."),
68
+ vehicleLicensePlate: z
69
+ .string()
70
+ .nullable()
71
+ .describe("License plate for the vehicle. This is the vehicle's identity — there is no separate UUID. Required for 'save-vehicle', 'update-vehicle', 'delete-vehicle', 'add-vehicle-label' and 'remove-vehicle-label'. A plate cannot be edited: to correct a mistyped plate, 'delete-vehicle' the wrong one and 'save-vehicle' the right one."),
72
+ vehicleDescription: z
73
+ .string()
74
+ .nullable()
75
+ .describe("Description for the vehicle. Optional for 'save-vehicle'; for 'update-vehicle' it is the new description (omit to leave it unchanged)."),
76
+ vehicleLabel: z
77
+ .string()
78
+ .nullable()
79
+ .describe("A single vehicle label name. Required for 'add-vehicle-label' and 'remove-vehicle-label'."),
59
80
  };
60
81
  const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
61
82
  // cherry-picked fields from /getVehicleEvents
@@ -108,6 +129,24 @@ export const OUTPUT_SCHEMA = z.object({
108
129
  })).optional().describe("License plate search results"),
109
130
  saveVehicleResult: z.object({
110
131
  success: z.boolean().optional(),
111
- }).optional().describe("Result of saving a vehicle"),
132
+ licensePlate: z.string().optional(),
133
+ }).optional().describe("Result of saving or updating a vehicle"),
134
+ deleteVehicleResult: z.object({
135
+ success: z.boolean().optional(),
136
+ licensePlate: z.string().optional(),
137
+ }).optional().describe("Result of deleting a saved vehicle"),
138
+ vehicleLabelResult: z.object({
139
+ success: z.boolean().optional(),
140
+ licensePlate: z.string().optional(),
141
+ label: z.string().optional(),
142
+ }).optional().describe("Result of adding or removing a vehicle label"),
143
+ note: z
144
+ .string()
145
+ .optional()
146
+ .describe("A caveat about this result that the user needs to be told."),
147
+ warningMsg: z
148
+ .string()
149
+ .optional()
150
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
112
151
  error: z.string().optional().describe("An error message if the request failed."),
113
152
  });