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,4 @@
1
- import { getVideoWalls, handleCreateVideoWallRequest, } from "../api/create-tool-api.js";
1
+ import { computeGridSettings, deleteVideoWall, findVideoWall, getVideoWalls, handleCreateVideoWallRequest, updateVideoWall, } from "../api/create-tool-api.js";
2
2
  import { OUTPUT_SCHEMA, TOOL_ARGS, } from "../types/video-walls-tool-types.js";
3
3
  import { createToolStructuredContent, createToolTextContent, extractFromToolExtra, } from "../util.js";
4
4
  const TOOL_NAME = "video-walls-tool";
@@ -6,36 +6,110 @@ const TOOL_DESCRIPTION = `
6
6
  This tool interacts with Rhombus video walls. Rhombus video walls are a collection of camera feeds combined into a single view,
7
7
  allowing users to monitor multiple cameras.
8
8
 
9
+ It has the following modes of operation, determined by the "requestType" parameter:
10
+ - 'list': List all video walls in the organization.
11
+ - 'create': Create a new video wall. Requires videoWallCreateOptions with a displayName and a deviceList of camera UUIDs (use get-entity-tool or location-tool to resolve camera UUIDs first). Returns the new video wall's uuid.
12
+ - 'update': Rename a video wall and/or change which cameras it shows. Requires videoWallUuid plus displayName and/or deviceList.
13
+ - 'delete': Delete a video wall. Requires videoWallUuid.
14
+
9
15
  The layout of created video walls is automatically determined by the number of cameras in video wall settings "numVisibleDevicesAtOnce".
10
16
  `;
11
17
  const TOOL_HANDLER = async (args, extra) => {
12
- const { requestType, videoWallCreateOptions } = args;
18
+ const { requestType, videoWallCreateOptions, videoWallUuid, displayName, deviceList } = args;
13
19
  const { requestModifiers, sessionId } = extractFromToolExtra(extra);
14
- switch (requestType) {
15
- case "create":
16
- if (!videoWallCreateOptions) {
17
- return createToolTextContent(JSON.stringify({
18
- error: "videoWallCreateOptions is required. Please try again.",
19
- }));
20
+ try {
21
+ switch (requestType) {
22
+ case "create": {
23
+ if (!videoWallCreateOptions) {
24
+ return createToolTextContent("videoWallCreateOptions is required for 'create'.");
25
+ }
26
+ return createToolStructuredContent(await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers, sessionId));
27
+ }
28
+ case "list":
29
+ return createToolStructuredContent(await getVideoWalls(requestModifiers, sessionId));
30
+ case "update": {
31
+ if (!videoWallUuid) {
32
+ return createToolTextContent("videoWallUuid is required for 'update'.");
33
+ }
34
+ if (!displayName?.trim() && !deviceList) {
35
+ return createToolTextContent("'update' needs at least one of displayName or deviceList — both were empty, so there is nothing to change.");
36
+ }
37
+ if (deviceList && deviceList.length === 0) {
38
+ return createToolTextContent("RETRYABLE — nothing was changed. deviceList was an empty array, which would leave the video wall with no cameras. Pass the full set of camera uuids the wall should show, or use 'delete' to remove the wall.");
39
+ }
40
+ // api2's updateVideoWalls REPLACES the whole wall object, so sending
41
+ // only the changed field would blank the rest.
42
+ const existing = await findVideoWall(videoWallUuid, requestModifiers, sessionId);
43
+ if (!existing) {
44
+ return createToolTextContent(`No video wall in this organization has the uuid "${videoWallUuid}". Use requestType 'list' to see the walls that exist — do not guess a uuid.`);
45
+ }
46
+ const existingDevices = existing.deviceList?.filter((uuid) => !!uuid) ?? [];
47
+ const nextDevices = deviceList ?? existingDevices;
48
+ const existingSettings = (existing.settings ?? {});
49
+ const { updated, warningMsg } = await updateVideoWall({
50
+ uuid: videoWallUuid,
51
+ displayName: displayName?.trim() || existing.displayName || undefined,
52
+ deviceList: nextDevices,
53
+ orgUuid: existing.orgUuid ?? undefined,
54
+ othersCanEdit: existing.othersCanEdit ?? undefined,
55
+ shared: existing.shared ?? undefined,
56
+ settings: {
57
+ ...existingSettings,
58
+ // The grid is a preset chosen by camera count, so a wall that
59
+ // grew would otherwise keep a layout too small to show the new
60
+ // cameras at all.
61
+ ...computeGridSettings(nextDevices.length),
62
+ },
63
+ }, requestModifiers, sessionId);
64
+ const removed = existingDevices.filter(uuid => !nextDevices.includes(uuid));
65
+ const added = nextDevices.filter(uuid => !existingDevices.includes(uuid));
66
+ const changes = [];
67
+ if (added.length > 0)
68
+ changes.push(`added ${added.length}`);
69
+ if (removed.length > 0)
70
+ changes.push(`removed ${removed.length}`);
71
+ return createToolStructuredContent({
72
+ updated,
73
+ warningMsg,
74
+ note: deviceList
75
+ ? `The wall now shows ${nextDevices.length} camera(s)${changes.length > 0 ? ` (${changes.join(", ")})` : ""}, and its grid layout was resized to match.`
76
+ : `Renamed to "${displayName?.trim()}". Its cameras are unchanged.`,
77
+ });
20
78
  }
21
- return createToolStructuredContent(await handleCreateVideoWallRequest(videoWallCreateOptions, requestModifiers, sessionId));
22
- case "list":
23
- return createToolStructuredContent(await getVideoWalls(requestModifiers, sessionId));
24
- default:
79
+ case "delete": {
80
+ if (!videoWallUuid) {
81
+ return createToolTextContent("videoWallUuid is required for 'delete'.");
82
+ }
83
+ const existing = await findVideoWall(videoWallUuid, requestModifiers, sessionId);
84
+ if (!existing) {
85
+ return createToolTextContent(`No video wall in this organization has the uuid "${videoWallUuid}". Use requestType 'list' to see the walls that exist.`);
86
+ }
87
+ const { deleted, warningMsg } = await deleteVideoWall(videoWallUuid, requestModifiers, sessionId);
88
+ return createToolStructuredContent({
89
+ deleted,
90
+ warningMsg,
91
+ note: (existing.numShares ?? 0) > 0
92
+ ? `Deleted "${existing.displayName ?? videoWallUuid}". It had ${existing.numShares} active share(s), which no longer work.`
93
+ : `Deleted "${existing.displayName ?? videoWallUuid}". Deleting a wall does not affect the cameras that were on it.`,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ catch (error) {
99
+ return createToolStructuredContent({
100
+ error: error instanceof Error ? error.message : "Unknown error",
101
+ });
25
102
  }
26
- return {
27
- content: [
28
- {
29
- type: "text",
30
- text: "Invalid entity type. Please try again.",
31
- },
32
- ],
33
- };
103
+ // Must carry structuredContent (or isError) — this tool registers an
104
+ // outputSchema, and the SDK replaces a bare text result with an opaque -32602.
105
+ return createToolTextContent("Invalid requestType. Valid values are: list, create, update, delete.");
34
106
  };
35
107
  export function createTool(server) {
36
108
  server.registerTool(TOOL_NAME, {
109
+ title: "Video Walls",
37
110
  description: TOOL_DESCRIPTION,
38
111
  inputSchema: TOOL_ARGS,
39
112
  outputSchema: OUTPUT_SCHEMA.shape,
113
+ annotations: { readOnlyHint: false, destructiveHint: true },
40
114
  }, TOOL_HANDLER);
41
115
  }
@@ -8,6 +8,20 @@ export const ANOMALY_RULES = [
8
8
  "impossible_travel",
9
9
  "area_novelty",
10
10
  ];
11
+ /**
12
+ * Description shared by the three access-anomaly tools (OnGuard / Elements /
13
+ * NetBox), which differ only in vendor. Kept short because tool descriptions
14
+ * are billed on every LLM call even while deferred, and this text is
15
+ * duplicated across all three siblings; rule detail and triage guidance live
16
+ * on the arguments below. See PERF_MASTER_PLAN P2 #4a.
17
+ */
18
+ export function buildAccessAnomalyToolDescription(vendor) {
19
+ return `
20
+ Scans ${vendor} access events over a time window and flags anomalous badge activity. Use for "find unusual badge activity", "anything suspicious in access this week", or proactive access review.
21
+
22
+ Runs deterministic rules — a lost/suspended badge was used, access granted but no entry made (possible tailgating), off-hours entry, impossible travel between areas, and first-ever entry to an area — and returns ranked findings (high severity first) with cardholderName, the rule, severity, datetime, area, the camera deviceUuid, a plain-language rationale, and clip/still hints.
23
+ `;
24
+ }
11
25
  export const TOOL_ARGS = {
12
26
  area: z.string().nullable().describe("Optional: restrict analysis to events entering this area (full-text)."),
13
27
  locationUuids: z
@@ -19,7 +33,8 @@ export const TOOL_ARGS = {
19
33
  .string()
20
34
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
21
35
  .nullable()
22
- .describe("Start of the window to analyze (inclusive). " + ISOTimestampFormatDescription),
36
+ .describe('Start of the window to analyze (inclusive). Resolve relative phrasing like "this week" with time-tool first, then pass ISO 8601 here. ' +
37
+ ISOTimestampFormatDescription),
23
38
  endTime: z
24
39
  .string()
25
40
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
@@ -28,7 +43,12 @@ export const TOOL_ARGS = {
28
43
  rules: z
29
44
  .array(z.enum(ANOMALY_RULES))
30
45
  .nullable()
31
- .describe("Which rules to run; default all. Options: lost_or_inactive_badge, entry_not_made, off_hours, impossible_travel, area_novelty."),
46
+ .describe("Which rules to run; default all. Options: lost_or_inactive_badge (a non-active badge — lost or suspended — was used), " +
47
+ "entry_not_made (access granted but no entry made, possible tailgating), off_hours (entry outside business hours, configurable), " +
48
+ "impossible_travel (one cardholder at two different areas seconds apart), area_novelty (a cardholder's first-ever entry to an area vs their prior history; needs a baseline window). " +
49
+ "This tool is a triage aid: present findings grouped by severity, and for the notable ones call camera-tool " +
50
+ '(requestType "image", cameraUuid = finding.deviceUuid, timestampISO = the finding\'s time — convert finding.timestampMs with time-conversion-tool) ' +
51
+ 'and/or clips-tool ("createClip" using finding.clipHint) IN PARALLEL so a human can confirm. Do not assert wrongdoing; surface the evidence.'),
32
52
  baselineDays: z
33
53
  .number()
34
54
  .nullable()
@@ -4,12 +4,34 @@ export var AccessControlRequestType;
4
4
  (function (AccessControlRequestType) {
5
5
  AccessControlRequestType["UNLOCK_DOOR"] = "unlock-door";
6
6
  AccessControlRequestType["GET_GROUPS"] = "get-groups";
7
+ AccessControlRequestType["CREATE_GROUP"] = "create-group";
8
+ AccessControlRequestType["UPDATE_GROUP"] = "update-group";
9
+ AccessControlRequestType["DELETE_GROUP"] = "delete-group";
10
+ AccessControlRequestType["ADD_USERS_TO_GROUP"] = "add-users-to-group";
11
+ AccessControlRequestType["REMOVE_USERS_FROM_GROUP"] = "remove-users-from-group";
7
12
  AccessControlRequestType["GET_CREDENTIALS_BY_USER"] = "get-credentials-by-user";
13
+ AccessControlRequestType["ASSIGN_CREDENTIAL"] = "assign-credential";
14
+ AccessControlRequestType["REVOKE_CREDENTIAL"] = "revoke-credential";
15
+ AccessControlRequestType["SUSPEND_CREDENTIAL"] = "suspend-credential";
16
+ AccessControlRequestType["UNSUSPEND_CREDENTIAL"] = "unsuspend-credential";
17
+ AccessControlRequestType["DELETE_CREDENTIAL"] = "delete-credential";
18
+ AccessControlRequestType["UPDATE_CREDENTIAL_NOTE"] = "update-credential-note";
8
19
  AccessControlRequestType["GET_LOCKDOWN_PLANS"] = "get-lockdown-plans";
20
+ AccessControlRequestType["RENAME_LOCKDOWN_PLAN"] = "rename-lockdown-plan";
21
+ AccessControlRequestType["DELETE_LOCKDOWN_PLAN"] = "delete-lockdown-plan";
9
22
  AccessControlRequestType["ACTIVATE_LOCKDOWN"] = "activate-lockdown";
10
23
  AccessControlRequestType["DEACTIVATE_LOCKDOWN"] = "deactivate-lockdown";
24
+ /**
25
+ * Reads door schedule EXCEPTIONS, not schedules — the name is retained
26
+ * because it is the published request type. Prefer door-schedule-exception-tool
27
+ * for anything beyond a location-scoped list, and schedule-tool for schedules
28
+ * themselves.
29
+ */
11
30
  AccessControlRequestType["GET_DOOR_SCHEDULES"] = "get-door-schedules";
12
31
  AccessControlRequestType["GET_ACCESS_GRANTS"] = "get-access-grants";
32
+ AccessControlRequestType["CREATE_ACCESS_GRANT"] = "create-access-grant";
33
+ AccessControlRequestType["UPDATE_ACCESS_GRANT"] = "update-access-grant";
34
+ AccessControlRequestType["DELETE_ACCESS_GRANT"] = "delete-access-grant";
13
35
  AccessControlRequestType["GET_REMOTE_UNLOCK_USERS"] = "get-remote-unlock-users";
14
36
  })(AccessControlRequestType || (AccessControlRequestType = {}));
15
37
  export const TOOL_ARGS = {
@@ -29,7 +51,59 @@ export const TOOL_ARGS = {
29
51
  lockdownPlanUuid: z
30
52
  .string()
31
53
  .nullable()
32
- .describe("The UUID of the lockdown plan. Required for 'activate-lockdown' and 'deactivate-lockdown'."),
54
+ .describe("The UUID of the lockdown plan. Required for 'activate-lockdown', 'deactivate-lockdown', 'rename-lockdown-plan' and 'delete-lockdown-plan'."),
55
+ groupUuid: z
56
+ .string()
57
+ .nullable()
58
+ .describe("The UUID of the access control group. Required for 'update-group', 'delete-group', 'add-users-to-group' and 'remove-users-from-group'. Get it from 'get-groups' — do not guess one."),
59
+ groupName: z
60
+ .string()
61
+ .nullable()
62
+ .describe("Name of the access control group. Required for 'create-group'; for 'update-group' it is the new name (omit to leave it unchanged). Also the new name for 'rename-lockdown-plan'."),
63
+ groupDescription: z
64
+ .string()
65
+ .nullable()
66
+ .describe("Description of the access control group. Optional for 'create-group' and 'update-group'."),
67
+ userUuids: z
68
+ .array(z.string())
69
+ .nullable()
70
+ .describe("User UUIDs. Required for 'add-users-to-group' and 'remove-users-from-group'; optional for 'create-group' to seed the initial members. Resolve names and emails to UUIDs with user-tool first — never guess a user UUID, because granting the wrong person physical door access is not something the user can see from the response."),
71
+ credentialUuid: z
72
+ .string()
73
+ .nullable()
74
+ .describe("The UUID of the access control credential. Required for 'revoke-credential', 'suspend-credential', 'unsuspend-credential', 'delete-credential' and 'update-credential-note'. Get it from 'get-credentials-by-user'."),
75
+ credentialHexValue: z
76
+ .string()
77
+ .nullable()
78
+ .describe("The hex value printed on / encoded in a physical badge or fob. Required for 'assign-credential'. The credential must already exist in the org as an unassigned credential; this attaches it to a user."),
79
+ credentialNote: z
80
+ .string()
81
+ .nullable()
82
+ .describe("Free-text note to store on the credential. Required for 'update-credential-note'."),
83
+ accessGrantUuid: z
84
+ .string()
85
+ .nullable()
86
+ .describe("The UUID of the location access grant. Required for 'update-access-grant' and 'delete-access-grant'. Get it from 'get-access-grants'."),
87
+ accessGrantName: z
88
+ .string()
89
+ .nullable()
90
+ .describe("Name for the access grant. Required for 'create-access-grant'; for 'update-access-grant' it is the new name (omit to leave it unchanged)."),
91
+ doorUuids: z
92
+ .array(z.string())
93
+ .nullable()
94
+ .describe("Access controlled door UUIDs the grant provides access to. Required for 'create-access-grant'. For 'update-access-grant' this REPLACES the grant's current door list rather than adding to it, so include the doors being kept; omit to leave the doors unchanged. Get UUIDs from get-entity-tool with entityType ACCESS_CONTROL_DOOR."),
95
+ groupUuids: z
96
+ .array(z.string())
97
+ .nullable()
98
+ .describe("Access control group UUIDs the grant applies to. For 'update-access-grant' this REPLACES the grant's current group list; omit to leave it unchanged."),
99
+ scheduleUuid: z
100
+ .string()
101
+ .nullable()
102
+ .describe("UUID of the schedule limiting when the grant applies. Optional. Resolve schedule names to UUIDs with schedule-tool — never invent one."),
103
+ confirmDelete: z
104
+ .boolean()
105
+ .nullable()
106
+ .describe("Required to be true for 'delete-group', 'delete-credential', 'delete-access-grant' and 'delete-lockdown-plan'. All four remove physical access or the ability to restore it, so they refuse without explicit confirmation from the user."),
33
107
  includeFields: INCLUDE_FIELDS_ARG,
34
108
  filterBy: FILTER_BY_ARG,
35
109
  };
@@ -116,5 +190,46 @@ export const OUTPUT_SCHEMA = z.object({
116
190
  })
117
191
  .optional()
118
192
  .describe("Users who can remotely unlock doors at a location, grouped by permission group. Always present the COMPLETE list of all users to the end user."),
193
+ created: z
194
+ .object({
195
+ success: z.boolean().optional(),
196
+ uuid: z.string().optional(),
197
+ memberCount: z.number().optional(),
198
+ })
199
+ .optional()
200
+ .describe("Result of a create request."),
201
+ updated: z
202
+ .object({
203
+ success: z.boolean().optional(),
204
+ uuid: z.string().optional(),
205
+ userCount: z.number().optional(),
206
+ })
207
+ .optional()
208
+ .describe("Result of an update / membership-change request."),
209
+ deleted: z
210
+ .object({ success: z.boolean().optional(), uuid: z.string().optional() })
211
+ .optional()
212
+ .describe("Result of a delete request."),
213
+ credentialChange: z
214
+ .object({
215
+ success: z.boolean().optional(),
216
+ uuid: z.string().optional(),
217
+ userUuid: z.string().optional(),
218
+ action: z.string().optional(),
219
+ })
220
+ .optional()
221
+ .describe("Result of a credential state change."),
222
+ doorsWithoutLicense: z
223
+ .array(z.string())
224
+ .optional()
225
+ .describe("Doors named in an access grant whose access-control license is expired or unassigned. The grant saved, but these doors grant NOBODY access until their license is fixed — always report them."),
226
+ note: z
227
+ .string()
228
+ .optional()
229
+ .describe("A caveat about this result that the user needs to be told."),
230
+ warningMsg: z
231
+ .string()
232
+ .optional()
233
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
119
234
  error: z.string().optional().describe("An error message if the request failed."),
120
235
  });
@@ -5,6 +5,11 @@ export var AlarmMonitoringRequestType;
5
5
  AlarmMonitoringRequestType["ORG_STATUS"] = "org-status";
6
6
  AlarmMonitoringRequestType["GET_THREAT_CASES"] = "get-threat-cases";
7
7
  AlarmMonitoringRequestType["LOCATION_STATUS"] = "location-status";
8
+ AlarmMonitoringRequestType["ENABLE_MONITORING"] = "enable-monitoring";
9
+ AlarmMonitoringRequestType["DISABLE_MONITORING"] = "disable-monitoring";
10
+ AlarmMonitoringRequestType["DISMISS_THREAT_CASE"] = "dismiss-threat-case";
11
+ AlarmMonitoringRequestType["CANCEL_THREAT_CASE"] = "cancel-threat-case";
12
+ AlarmMonitoringRequestType["ESCALATE_THREAT_CASE"] = "escalate-threat-case";
8
13
  })(AlarmMonitoringRequestType || (AlarmMonitoringRequestType = {}));
9
14
  export const TOOL_ARGS = {
10
15
  requestType: z
@@ -13,7 +18,19 @@ export const TOOL_ARGS = {
13
18
  locationUuid: z
14
19
  .string()
15
20
  .nullable()
16
- .describe("Location UUID. Required for 'location-status'."),
21
+ .describe("Location UUID. Required for 'location-status', 'enable-monitoring' and 'disable-monitoring'."),
22
+ pin: z
23
+ .string()
24
+ .nullable()
25
+ .describe("The location's alarm PIN, required for 'enable-monitoring' and 'disable-monitoring'. Ask the user for it; never guess one and never repeat it back in your response."),
26
+ threatCaseUuid: z
27
+ .string()
28
+ .nullable()
29
+ .describe("UUID of a threat case. Required for 'dismiss-threat-case', 'cancel-threat-case' and 'escalate-threat-case'. Get it from 'get-threat-cases' — do not guess one."),
30
+ confirmAction: z
31
+ .boolean()
32
+ .nullable()
33
+ .describe("Required to be true for 'disable-monitoring' and 'escalate-threat-case'. Disabling monitoring stops a location being watched, and escalating dispatches a real alarm response, so both refuse without explicit confirmation from the user."),
17
34
  startTimeMs: z
18
35
  .number()
19
36
  .nullable()
@@ -56,5 +73,29 @@ export const OUTPUT_SCHEMA = z.object({
56
73
  .optional()
57
74
  .describe("List of alarm monitoring threat cases"),
58
75
  locationDetail: LocationMonitoringStatusSchema.optional().describe("Detailed alarm monitoring status for a single location"),
76
+ monitoringChange: z
77
+ .object({
78
+ success: z.boolean().optional(),
79
+ locationUuid: z.string().optional(),
80
+ enabled: z.boolean().optional(),
81
+ })
82
+ .optional()
83
+ .describe("Result of 'enable-monitoring' / 'disable-monitoring'."),
84
+ threatCaseChange: z
85
+ .object({
86
+ success: z.boolean().optional(),
87
+ uuid: z.string().optional(),
88
+ action: z.string().optional(),
89
+ })
90
+ .optional()
91
+ .describe("Result of a threat case state change."),
92
+ note: z
93
+ .string()
94
+ .optional()
95
+ .describe("A caveat about this result that the user needs to be told."),
96
+ warningMsg: z
97
+ .string()
98
+ .optional()
99
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
59
100
  error: z.string().optional().describe("An error message if the request failed."),
60
101
  });
@@ -1,10 +1,24 @@
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 badge-timeline 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; the how-to-present guidance lives on
9
+ * the arguments below. See PERF_MASTER_PLAN P2 #4a.
10
+ */
11
+ export function buildBadgeTimelineToolDescription(vendor) {
12
+ return `
13
+ Reconstructs one person's movements through a building from their ${vendor} badge taps. Use this for incident reconstruction / "follow the badge" requests, e.g. "reconstruct Eve's movements yesterday" or "where did this cardholder go".
14
+
15
+ Returns the cardholder's badge taps in CHRONOLOGICAL order (oldest first) — datetime, the area entered, deviceUuid of the camera at that door, clip/still hints, and gapToNextSeconds (a large gap means unobserved movement between doors) — plus a "path" array summarizing the areas traversed in order.
16
+ `;
17
+ }
4
18
  export const TOOL_ARGS = {
5
19
  cardholderQuery: z
6
20
  .string()
7
- .describe('The cardholder (person) to reconstruct, full-text name match, e.g. "Eve" or "Eve Adams".'),
21
+ .describe('The cardholder (person) to reconstruct, full-text name match, e.g. "Eve" or "Eve Adams". If the response contains "ambiguousCardholders" the query matched more than one person — ask the user which one before trusting the timeline.'),
8
22
  locationUuids: z
9
23
  .array(createUuidSchema())
10
24
  .nullable()
@@ -13,7 +27,8 @@ export const TOOL_ARGS = {
13
27
  .string()
14
28
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
15
29
  .nullable()
16
- .describe("Start of the window (inclusive). " + ISOTimestampFormatDescription),
30
+ .describe('Start of the window (inclusive). Resolve relative phrasing like "yesterday" with time-tool first, then pass ISO 8601 here. ' +
31
+ ISOTimestampFormatDescription),
17
32
  endTime: z
18
33
  .string()
19
34
  .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
@@ -22,7 +37,7 @@ export const TOOL_ARGS = {
22
37
  clipPaddingSeconds: z
23
38
  .number()
24
39
  .nullable()
25
- .describe("Seconds of video before/after each badge tap to include in the clip hint (default 15)."),
40
+ .describe("Seconds of video before/after each badge tap to include in the clip hint (default 15). To show the movement visually after this returns: for each stop (or the key transitions) call camera-tool (requestType \"image\", cameraUuid = stop.deviceUuid, timestampISO = the stop's time — convert stop.timestampMs with time-conversion-tool) for a still, and/or clips-tool (requestType \"createClip\", using stop.clipHint) for video. Issue those per-stop media calls IN PARALLEL, then present the timeline as a chronological narrative."),
26
41
  limit: z.number().nullable().describe("Maximum badge taps to include (default 200)."),
27
42
  timeZone: z
28
43
  .string()
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+ import { SCHEDULE_CONFIGS_EXAMPLE } from "./create-camera-policy-tool-types.js";
3
+ export var CameraPolicyToolRequestType;
4
+ (function (CameraPolicyToolRequestType) {
5
+ CameraPolicyToolRequestType["LIST"] = "list";
6
+ CameraPolicyToolRequestType["GET"] = "get";
7
+ CameraPolicyToolRequestType["UPDATE"] = "update";
8
+ CameraPolicyToolRequestType["DELETE"] = "delete";
9
+ CameraPolicyToolRequestType["ASSIGN_CAMERAS"] = "assign-cameras";
10
+ CameraPolicyToolRequestType["UNASSIGN_CAMERAS"] = "unassign-cameras";
11
+ })(CameraPolicyToolRequestType || (CameraPolicyToolRequestType = {}));
12
+ export const TOOL_ARGS = {
13
+ requestType: z
14
+ .nativeEnum(CameraPolicyToolRequestType)
15
+ .describe("The type of camera policy operation to perform."),
16
+ policyUuid: z
17
+ .string()
18
+ .nullable()
19
+ .describe("The UUID of the camera policy. Required for every requestType except 'list'."),
20
+ name: z
21
+ .string()
22
+ .nullable()
23
+ .describe("A new name for the policy. Only used by 'update'; omit to leave the name unchanged."),
24
+ description: z
25
+ .string()
26
+ .nullable()
27
+ .describe("A new description for the policy. Only used by 'update'; omit to leave it unchanged."),
28
+ scheduleConfigs: z
29
+ .string()
30
+ .nullable()
31
+ .describe(`Only used by 'update'. JSON string array of {"scheduleUuid","activities"} objects — e.g. ` +
32
+ `${SCHEDULE_CONFIGS_EXAMPLE}. Resolve schedule names to UUIDs with schedule-tool first; ` +
33
+ `never invent a scheduleUuid. Passing this REPLACES all of the policy's existing ` +
34
+ `schedule triggers, so include every window the policy should keep, not just the new ` +
35
+ `one. Omit to leave the triggers unchanged.`),
36
+ cameraUuids: z
37
+ .string()
38
+ .nullable()
39
+ .describe("Comma-separated camera UUIDs. Required for 'assign-cameras' and 'unassign-cameras'. " +
40
+ "Resolve camera names to UUIDs with get-entity-tool first."),
41
+ confirmDeleteInUse: z
42
+ .boolean()
43
+ .nullable()
44
+ .describe("Only for 'delete'. Deleting a policy that cameras are still assigned to leaves those " +
45
+ "cameras with no policy, so 'delete' refuses by default and lists them. Set true to " +
46
+ "delete anyway, after telling the user which cameras are affected."),
47
+ };
48
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
49
+ const SCHEDULED_TRIGGER_OUTPUT = z.object({
50
+ scheduleUuid: z.string().optional(),
51
+ scheduleName: z.string().optional(),
52
+ scheduleWindows: z
53
+ .array(z.string())
54
+ .optional()
55
+ .describe('When the schedule is active, e.g. ["Monday 09:00 – Monday 17:00"].'),
56
+ activities: z
57
+ .array(z.string())
58
+ .optional()
59
+ .describe("The activities that trigger an alert during this schedule."),
60
+ });
61
+ const POLICY_OUTPUT = z.object({
62
+ uuid: z.string().optional(),
63
+ name: z.string().optional(),
64
+ description: z.string().optional(),
65
+ defaultActivities: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe("Activities that trigger at all times, outside any schedule."),
69
+ scheduledTriggers: z.array(SCHEDULED_TRIGGER_OUTPUT).optional(),
70
+ assignedCameraCount: z
71
+ .number()
72
+ .optional()
73
+ .describe("How many cameras this policy is assigned to. Pre-computed so the count never has to be derived by scanning assignedCameras."),
74
+ assignedCameras: z
75
+ .array(z.object({ uuid: z.string().optional(), name: z.string().optional() }))
76
+ .optional()
77
+ .describe("The cameras this policy is assigned to."),
78
+ });
79
+ export const OUTPUT_SCHEMA = z.object({
80
+ policies: z.array(POLICY_OUTPUT).optional().describe("All camera policies in the organization."),
81
+ policy: POLICY_OUTPUT.optional().describe("A single camera policy."),
82
+ updated: z
83
+ .object({ success: z.boolean().optional(), policyUuid: z.string().optional() })
84
+ .optional(),
85
+ deleted: z
86
+ .object({ success: z.boolean().optional(), policyUuid: z.string().optional() })
87
+ .optional(),
88
+ assigned: z
89
+ .object({
90
+ success: z.boolean().optional(),
91
+ policyUuid: z.string().optional(),
92
+ cameraCount: z.number().optional(),
93
+ })
94
+ .optional(),
95
+ note: z
96
+ .string()
97
+ .optional()
98
+ .describe("A caveat about this result that the user needs to be told."),
99
+ warningMsg: z
100
+ .string()
101
+ .optional()
102
+ .describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
103
+ error: z.string().optional().describe("An error message if the request failed."),
104
+ });
@@ -220,7 +220,27 @@ export const BASE_TOOL_ARGS = {
220
220
  .describe(`
221
221
  the timestamp for the image. This will default to 5 minutes before the current time. You can also call time-tool to parse the user's time description.
222
222
  ` + ISOTimestampFormatDescription),
223
- requestType: z.enum(["image", "get-settings", "get-media-uris", "get-ai-thresholds"]),
223
+ requestType: z
224
+ .enum(["image", "get-settings", "get-media-uris", "get-ai-thresholds"])
225
+ .describe(`Which camera action to run: "image" (fetch a frame), "get-settings" (current device configuration), "get-media-uris" (live-stream and VOD URLs), "get-ai-thresholds" (AI detection thresholds).
226
+
227
+ **AUTOMATIC SNAPSHOT FOR IMAGE QUALITY ISSUES** — when a user mentions camera image quality (darkness, brightness, blur, washed out, "doesn't look great", "fix the image", etc.), you MUST IMMEDIATELY:
228
+ 1. Call camera-tool with requestType "image" to capture a snapshot WITHOUT asking first.
229
+ 2. Analyze the image to identify quality issues.
230
+ 3. Call camera-tool with requestType "get-settings" to check current camera settings.
231
+ 4. Propose specific setting changes based on your analysis (store the exact values you plan to change, e.g. img_brightness, wdr_strength).
232
+ 5. When the user confirms ("yes", "confirm", "fix it", "apply", "go ahead", "ok", etc.), call update-tool with those stored settings — see update-tool's description for the confirmation flow. NEVER skip the update-tool call.
233
+
234
+ **VISUAL-FEATURE CAMERA FILTERING** — when the user asks for cameras filtered by what they can see (indoors/outdoors, "facing the street", "with a view of X", parking lot, entrance), you MUST:
235
+ 1. First get the camera list via get-entity-tool or location-tool.
236
+ 2. Then call camera-tool with requestType "image" for EACH candidate camera (in PARALLEL).
237
+ 3. Analyze each image to determine if it meets the user's criteria.
238
+ 4. Return only the cameras that match.`),
239
+ detail: z
240
+ .enum(["core", "full"])
241
+ .nullish()
242
+ .transform((v) => v ?? "core")
243
+ .describe("For requestType 'get-settings' only. 'core' (default) elides bulky geometry/table sub-configs (metering tables, ROI polygons, PTZ/motor config) and oversized values, keeping the settings you can read and change (image/video/exposure/storage). Pass 'full' for the complete raw config."),
224
244
  cropX: z
225
245
  .number()
226
246
  .min(0)
@@ -257,3 +277,62 @@ export const BASE_TOOL_ARGS = {
257
277
  .describe("For requestType 'image' only. Ratio to shrink the image pixels by (default 10). Use a smaller value (e.g. 1-3) when zooming into a crop to keep detail."),
258
278
  };
259
279
  const BASE_TOOL_ARGS_SCHEMA = z.object(BASE_TOOL_ARGS);
280
+ // Output schema — registered so the filtering proxy can project results and
281
+ // generate the includeFields path catalog. All fields optional: which subset
282
+ // appears depends on requestType. Every handler return must carry
283
+ // structuredContent (or isError: true) once this is registered — the SDK
284
+ // otherwise replaces the real message with an opaque -32602 (see CLAUDE.md).
285
+ export const OUTPUT_SCHEMA = {
286
+ // asked for a camera without saying which
287
+ needUserInput: z.boolean().optional(),
288
+ commandForUser: z.string().optional(),
289
+ // requestType: "image"
290
+ success: z.boolean().optional(),
291
+ status: z.string().optional(),
292
+ cameraUuid: z.string().optional(),
293
+ timestampMs: z.number().optional(),
294
+ cropApplied: z
295
+ .object({
296
+ x: z.number().nullable().optional(),
297
+ y: z.number().nullable().optional(),
298
+ width: z.number().nullable().optional(),
299
+ height: z.number().nullable().optional(),
300
+ })
301
+ .nullable()
302
+ .optional()
303
+ .describe("The crop actually applied to the returned frame, if any"),
304
+ // requestType: "get-settings" — the faceted config is free-form (~150 fields
305
+ // per facet), so it is an open record; project with paths like
306
+ // "config.videoFacetSettings".
307
+ config: z.record(z.string(), z.unknown()).optional(),
308
+ daysInCloud: z.number().optional(),
309
+ daysOnCamera: z.number().optional(),
310
+ cloudArchiveDays: z.number().nullable().optional(),
311
+ // requestType: "get-media-uris" (subset of Camera_GetMediaUrisWSResponse)
312
+ lanCheckUrls: z.array(z.string().nullable()).nullable().optional(),
313
+ lanLiveH264Uris: z.array(z.string().nullable()).nullable().optional(),
314
+ lanLiveM3u8Uris: z.array(z.string().nullable()).nullable().optional(),
315
+ lanLiveMpdUris: z.array(z.string().nullable()).nullable().optional(),
316
+ lanLiveOpusUris: z.array(z.string().nullable()).nullable().optional(),
317
+ lanVodM3u8UrisTemplates: z.array(z.string().nullable()).nullable().optional(),
318
+ lanVodMpdUrisTemplates: z.array(z.string().nullable()).nullable().optional(),
319
+ wanLiveH264Uri: z.string().nullable().optional(),
320
+ wanLiveM3u8Uri: z.string().nullable().optional(),
321
+ wanLiveMpdUri: z.string().nullable().optional(),
322
+ wanLiveOpusUri: z.string().nullable().optional(),
323
+ wanVodH264UriTemplate: z.string().nullable().optional(),
324
+ wanVodM3u8UriTemplate: z.string().nullable().optional(),
325
+ wanVodMpdUriTemplate: z.string().nullable().optional(),
326
+ // requestType: "get-ai-thresholds" (Camera_GetCameraAIThresholdsWSResponse)
327
+ consecutiveHumanFilter: z.number().nullable().optional(),
328
+ consecutiveVehicleFilter: z.number().nullable().optional(),
329
+ faceConfidenceThreshold: z.number().nullable().optional(),
330
+ faceMatchConfidenceThreshold: z.number().nullable().optional(),
331
+ humanConfidenceThreshold: z.number().nullable().optional(),
332
+ lprConfidenceThreshold: z.number().nullable().optional(),
333
+ vehicleConfidenceThreshold: z.number().nullable().optional(),
334
+ maxEventDurationMs: z.number().nullable().optional(),
335
+ // api2's success-with-caveat channel; in ALWAYS_PROTECTED_FIELDS so a
336
+ // projection cannot strip it
337
+ warningMsg: z.string().nullable().optional(),
338
+ };