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.
- package/dist/api/access-control-tool-api.js +231 -32
- package/dist/api/alarm-monitoring-tool-api.js +50 -1
- package/dist/api/automated-prompts-tool-api.js +9 -17
- package/dist/api/camera-policy-tool-api.js +122 -0
- package/dist/api/camera-tool-api.js +3 -5
- package/dist/api/camera-uptime-tool-api.js +62 -25
- package/dist/api/clips-tool-api.js +45 -9
- package/dist/api/create-tool-api.js +55 -7
- package/dist/api/door-tool-api.js +74 -13
- package/dist/api/entity-lookup-tool-api.js +8 -1
- package/dist/api/events-tool-api.js +79 -6
- package/dist/api/faces-tool-api.js +112 -7
- package/dist/api/get-accessible-apps.js +68 -6
- package/dist/api/get-entity-tool-api.js +12 -12
- package/dist/api/get-org-information-tool-api.js +2 -2
- package/dist/api/guest-management-tool-api.js +75 -1
- package/dist/api/location-tool-api.js +120 -28
- package/dist/api/lpr-tool-api.js +71 -16
- package/dist/api/policy-alerts-tool-api.js +7 -3
- package/dist/api/report-tool-api.js +4 -1
- package/dist/api/rules-tool-api.js +6 -16
- package/dist/api/schedule-tool-api.js +107 -0
- package/dist/api/search-tool-api.js +5 -13
- package/dist/api/update-tool-api.js +96 -1
- package/dist/api/user-tool-api.js +58 -13
- package/dist/createServer.js +18 -6
- package/dist/filtering-utils.js +421 -29
- package/dist/index.js +1 -1
- package/dist/network/network.js +122 -10
- package/dist/network/org-reference-cache.js +74 -0
- package/dist/telemetry/tracingProxy.js +4 -0
- package/dist/tools/entity-lookup-tool.js +8 -4
- package/dist/tools/get-entity-tool.js +46 -21
- package/dist/tools/get-org-information-tool.js +36 -3
- package/dist/tools/time-conversion-tool.js +65 -37
- package/dist/tools/time-tool.js +18 -4
- package/dist/tools/user-tool.js +101 -1
- package/dist/tools-console/access-anomaly-tool.js +4 -19
- package/dist/tools-console/access-control-tool.js +330 -2
- package/dist/tools-console/alarm-monitoring-tool.js +73 -1
- package/dist/tools-console/analytics-tool.js +4 -2
- package/dist/tools-console/automated-prompts-tool.js +2 -0
- package/dist/tools-console/badge-timeline-tool.js +4 -21
- package/dist/tools-console/camera-policy-tool.js +252 -0
- package/dist/tools-console/camera-tool.js +113 -89
- package/dist/tools-console/camera-uptime-tool.js +2 -0
- package/dist/tools-console/clips-tool.js +23 -31
- package/dist/tools-console/count-tool.js +19 -6
- package/dist/tools-console/create-camera-policy-tool.js +304 -122
- package/dist/tools-console/door-schedule-exception-tool.js +4 -33
- package/dist/tools-console/door-tool.js +131 -1
- package/dist/tools-console/elements-access-anomaly-tool.js +4 -19
- package/dist/tools-console/elements-badge-timeline-tool.js +4 -21
- package/dist/tools-console/elements-lost-badge-tool.js +4 -17
- package/dist/tools-console/elements-tool.js +4 -29
- package/dist/tools-console/events-tool.js +24 -101
- package/dist/tools-console/faces-tool.js +255 -27
- package/dist/tools-console/guest-management-tool.js +128 -1
- package/dist/tools-console/location-tool.js +161 -35
- package/dist/tools-console/lost-badge-tool.js +4 -17
- package/dist/tools-console/lpr-tool.js +90 -22
- package/dist/tools-console/netbox-access-anomaly-tool.js +4 -19
- package/dist/tools-console/netbox-badge-timeline-tool.js +4 -21
- package/dist/tools-console/netbox-lost-badge-tool.js +4 -17
- package/dist/tools-console/netbox-tool.js +4 -29
- package/dist/tools-console/onguard-tool.js +4 -30
- package/dist/tools-console/person-tracking-tool.js +4 -2
- package/dist/tools-console/policy-alerts-tool.js +6 -28
- package/dist/tools-console/reboot-cameras-tool.js +23 -3
- package/dist/tools-console/report-tool.js +12 -40
- package/dist/tools-console/rules-tool.js +2 -0
- package/dist/tools-console/schedule-tool.js +190 -0
- package/dist/tools-console/search-tool.js +2 -0
- package/dist/tools-console/update-tool.js +300 -168
- package/dist/tools-console/user-access-trail-tool.js +2 -0
- package/dist/tools-console/user-audit-tool.js +2 -0
- package/dist/tools-console/video-walls-tool.js +94 -20
- package/dist/types/access-anomaly-tool-types.js +22 -2
- package/dist/types/access-control-tool-types.js +116 -1
- package/dist/types/alarm-monitoring-tool-types.js +42 -1
- package/dist/types/badge-timeline-tool-types.js +18 -3
- package/dist/types/camera-policy-tool-types.js +104 -0
- package/dist/types/camera-tool-types.js +80 -1
- package/dist/types/clips-tool-types.js +27 -3
- package/dist/types/create-camera-policy-tool-types.js +275 -26
- package/dist/types/door-schedule-exception-tool-types.js +20 -6
- package/dist/types/door-tool-types.js +36 -4
- package/dist/types/entity-lookup-tool-types.js +6 -1
- package/dist/types/events-tools-types.js +24 -16
- package/dist/types/faces-tools-types.js +106 -3
- package/dist/types/get-entity-tool-types.js +76 -12
- package/dist/types/guest-management-tool-types.js +103 -1
- package/dist/types/location-tool-types.js +100 -14
- package/dist/types/lost-badge-tool-types.js +17 -2
- package/dist/types/lpr-tool-types.js +47 -8
- package/dist/types/onguard-tool-types.js +21 -1
- package/dist/types/policy-alerts-tool-types.js +1 -1
- package/dist/types/report-tool-types.js +29 -1
- package/dist/types/schedule-tool-types.js +370 -0
- package/dist/types/schema.js +479 -9
- package/dist/types/time-conversion-tool-types.js +3 -4
- package/dist/types/update-tool-types.js +40 -18
- package/dist/types/user-tool-types.js +44 -1
- package/dist/types/video-walls-tool-types.js +30 -2
- package/dist/types/zod-schemas.js +5128 -996
- package/dist/util.js +62 -3
- package/dist/utils/entity-name-match.js +11 -0
- package/package.json +3 -1
|
@@ -3,8 +3,8 @@ import { createUuidSchema } from "../types.js";
|
|
|
3
3
|
import { createEpochSchema, ISOTimestampFormatDescription, } from "../utils/timestampInput.js";
|
|
4
4
|
export const TOOL_ARGS = {
|
|
5
5
|
requestType: z
|
|
6
|
-
.enum(["saved", "expiringSoon", "sharedLiveStreams", "timelapseClips", "clipGroups", "sharedClips", "createClip", "deleteClip"])
|
|
7
|
-
.describe('The type of data to retrieve.
|
|
6
|
+
.enum(["saved", "expiringSoon", "sharedLiveStreams", "timelapseClips", "clipGroups", "sharedClips", "createClip", "updateClip", "deleteClip"])
|
|
7
|
+
.describe('The type of data to retrieve or action to perform. "saved" = regular saved clips; "expiringSoon" = clips nearing expiration; "sharedLiveStreams" = all shared live video streams; "timelapseClips" = all timelapse clips; "clipGroups" = clip groups in the org; "sharedClips" = shared clip groups; "createClip" = save a new clip from a camera (requires spliceRequest); "updateClip" = retitle or re-describe an existing saved clip (requires clipUuid plus clipTitle and/or clipDescription); "deleteClip" = permanently delete a saved clip (requires clipUuid; destructive).'),
|
|
8
8
|
deviceUuidFilters: z
|
|
9
9
|
.array(createUuidSchema())
|
|
10
10
|
.nullable()
|
|
@@ -35,7 +35,18 @@ export const TOOL_ARGS = {
|
|
|
35
35
|
.nullable()
|
|
36
36
|
.describe("The end of the time range for which to retrieve clips. Only clips that occurred BEFORE this timestamp will be returned. Required when requestType is saved or expiringSoon." +
|
|
37
37
|
ISOTimestampFormatDescription),
|
|
38
|
-
clipUuid: z
|
|
38
|
+
clipUuid: z
|
|
39
|
+
.string()
|
|
40
|
+
.nullable()
|
|
41
|
+
.describe("UUID of a saved clip. Required for 'deleteClip' and 'updateClip'."),
|
|
42
|
+
clipTitle: z
|
|
43
|
+
.string()
|
|
44
|
+
.nullable()
|
|
45
|
+
.describe("Only for 'updateClip': a new title for the clip. Omit to leave the title unchanged."),
|
|
46
|
+
clipDescription: z
|
|
47
|
+
.string()
|
|
48
|
+
.nullable()
|
|
49
|
+
.describe("Only for 'updateClip': a new description for the clip. Omit to leave the description unchanged."),
|
|
39
50
|
spliceRequest: z.object({
|
|
40
51
|
cameraUuid: z.string().describe("Camera UUID to create clip from"),
|
|
41
52
|
startTimeMs: z.number().describe("Start time in milliseconds"),
|
|
@@ -49,6 +60,7 @@ export const ApiPayloadSchema = TOOL_ARGS_SCHEMA.transform((args) => {
|
|
|
49
60
|
args.requestType === "clipGroups" ||
|
|
50
61
|
args.requestType === "sharedClips" ||
|
|
51
62
|
args.requestType === "createClip" ||
|
|
63
|
+
args.requestType === "updateClip" ||
|
|
52
64
|
args.requestType === "deleteClip") {
|
|
53
65
|
return {};
|
|
54
66
|
}
|
|
@@ -87,4 +99,16 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
87
99
|
deleteResult: z.object({
|
|
88
100
|
success: z.boolean().optional(),
|
|
89
101
|
}).optional().describe("Result of deleting a clip"),
|
|
102
|
+
updateResult: z.object({
|
|
103
|
+
success: z.boolean().optional(),
|
|
104
|
+
clipUuid: z.string().optional(),
|
|
105
|
+
}).optional().describe("Result of updating a clip's title or description"),
|
|
106
|
+
note: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("A caveat about this result that the user needs to be told."),
|
|
110
|
+
warningMsg: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
|
|
90
114
|
});
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { schemas } from "./zod-schemas.js";
|
|
3
|
-
|
|
4
|
-
export const PolicyInfoSchema = z
|
|
5
|
-
.object({
|
|
6
|
-
name: z.string().min(1, "Policy name is required"),
|
|
7
|
-
description: z.string().optional(),
|
|
8
|
-
orgUuid: z.string().optional(),
|
|
9
|
-
})
|
|
10
|
-
.describe("Camera policy information");
|
|
11
|
-
export const TOOL_ARGS = {};
|
|
12
|
-
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
3
|
+
import { ActivityEnum } from "./schema.js";
|
|
13
4
|
export const ApiPayloadSchema = schemas.Policy_CreateCameraPolicyWSRequest;
|
|
14
5
|
export const OUTPUT_SCHEMA = z.object({
|
|
15
6
|
needUserInput: z.boolean().optional(),
|
|
@@ -17,14 +8,6 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
17
8
|
.string()
|
|
18
9
|
.optional()
|
|
19
10
|
.describe("The message for this stage in the policy creation process. This message will be displayed to the user."),
|
|
20
|
-
requestType: z
|
|
21
|
-
.enum(["policy-creation-form", "schedule-trigger-configuration", "camera-assignment"])
|
|
22
|
-
.optional()
|
|
23
|
-
.describe("The type of form to display to the user as the next step in the policy creation process"),
|
|
24
|
-
submitAction: z
|
|
25
|
-
.string()
|
|
26
|
-
.optional()
|
|
27
|
-
.describe("The action to take when the user completes the form, corresponds to the tool name to interact with next"),
|
|
28
11
|
policyUuid: z
|
|
29
12
|
.string()
|
|
30
13
|
.optional()
|
|
@@ -33,12 +16,278 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
33
16
|
.string()
|
|
34
17
|
.optional()
|
|
35
18
|
.describe("The name of the policy that was created during this workflow"),
|
|
36
|
-
scheduleData: z
|
|
37
|
-
.array(z.object({
|
|
38
|
-
uuid: z.string(),
|
|
39
|
-
name: z.string(),
|
|
40
|
-
}))
|
|
41
|
-
.nullable()
|
|
42
|
-
.optional()
|
|
43
|
-
.describe("The schedules that were created during this workflow, can be null or undefined if user has not created them yet with schedule-trigger-configuration"),
|
|
44
19
|
});
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// scheduleConfigs / cameraUuids parsing
|
|
22
|
+
//
|
|
23
|
+
// Both are free-form STRING params, so nothing machine-enforces their shape:
|
|
24
|
+
// tools are adapted for OpenAI with `strict: false`, and even under `strict:
|
|
25
|
+
// true` a string param's *contents* are never constrained. Every check has to
|
|
26
|
+
// live here, and every message has to name the field, the expected shape, and
|
|
27
|
+
// what was actually received — otherwise the model has nothing to correct and
|
|
28
|
+
// reports the whole workflow as broken (prod 2026-08-04).
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* The activities the camera-policy workflow offers. This is a curated, camera-
|
|
32
|
+
* relevant subset of the API's org-wide `ActivityEnum` — it is what we suggest,
|
|
33
|
+
* NOT what we accept (validation goes against the full enum below, so a
|
|
34
|
+
* legitimate activity the playbook happens not to list still works).
|
|
35
|
+
*
|
|
36
|
+
* Keep in sync with the option list in the private MCP's
|
|
37
|
+
* `workflows/create-camera-policy.ts` playbook.
|
|
38
|
+
*/
|
|
39
|
+
export const SUGGESTED_CAMERA_ACTIVITIES = [
|
|
40
|
+
"MOTION",
|
|
41
|
+
"MOTION_HUMAN",
|
|
42
|
+
"MOTION_CAR",
|
|
43
|
+
"MOTION_ANIMAL",
|
|
44
|
+
"FACE_ALERT",
|
|
45
|
+
"FACE_UNIDENTIFIED",
|
|
46
|
+
"LICENSEPLATE_ALERT",
|
|
47
|
+
"HUMAN_ENTER",
|
|
48
|
+
"HUMAN_EXIT",
|
|
49
|
+
"CAR_ENTER",
|
|
50
|
+
"CAR_EXIT",
|
|
51
|
+
"POSE_ANOMALOUS",
|
|
52
|
+
"POSE_FALL",
|
|
53
|
+
"HELMET_MISSING",
|
|
54
|
+
"MASK_MISSING",
|
|
55
|
+
"GLOVES_MISSING",
|
|
56
|
+
"VISUAL_TAMPER",
|
|
57
|
+
"TAMPER",
|
|
58
|
+
"PEOPLECOUNT_HIGH",
|
|
59
|
+
];
|
|
60
|
+
const VALID_ACTIVITIES = new Set(Object.values(ActivityEnum));
|
|
61
|
+
export const SCHEDULE_CONFIGS_EXAMPLE = '[{"scheduleUuid":"Wq1nR0kBS9y_2hZpVvGkAA","activities":["MOTION_HUMAN","MOTION_CAR"]}]';
|
|
62
|
+
const SHAPE_HINT = `Expected a JSON array string of objects, each with a "scheduleUuid" string and an "activities" ` +
|
|
63
|
+
`array of activity strings — e.g. ${SCHEDULE_CONFIGS_EXAMPLE}. "activities" must be a real JSON ` +
|
|
64
|
+
`array, NOT a string containing one.`;
|
|
65
|
+
// Rhombus uuids are 22-char base64url. Deliberately loose — this only has to
|
|
66
|
+
// catch the model passing a human-readable NAME ("Business Hours", "Front Door")
|
|
67
|
+
// where a uuid belongs, which otherwise reaches the API and fails there, after
|
|
68
|
+
// the policy has already been created.
|
|
69
|
+
const UUID_LIKE = /^[A-Za-z0-9_-]{10,64}$/;
|
|
70
|
+
function quote(value) {
|
|
71
|
+
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
72
|
+
const text = serialized ?? String(value);
|
|
73
|
+
return text.length > 80 ? `${text.slice(0, 80)}…` : text;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Accept every unambiguous spelling of "a list of activities" the model
|
|
77
|
+
* produces, and return null only when the value cannot be read as one.
|
|
78
|
+
* Tolerated: a real array; a JSON-encoded array (the playbook tells the model to
|
|
79
|
+
* JSON-encode structured values into the form context, and it then re-embeds the
|
|
80
|
+
* encoded string); a comma-separated string; the API's own
|
|
81
|
+
* `[{activity: "..."}]` trigger shape.
|
|
82
|
+
*/
|
|
83
|
+
function coerceActivities(raw) {
|
|
84
|
+
if (typeof raw === "string") {
|
|
85
|
+
const trimmed = raw.trim();
|
|
86
|
+
if (!trimmed)
|
|
87
|
+
return [];
|
|
88
|
+
if (trimmed.startsWith("[")) {
|
|
89
|
+
try {
|
|
90
|
+
return coerceActivities(JSON.parse(trimmed));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return trimmed
|
|
97
|
+
.split(",")
|
|
98
|
+
.map(part => part.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(raw)) {
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const item of raw) {
|
|
104
|
+
if (typeof item === "string") {
|
|
105
|
+
if (item.trim())
|
|
106
|
+
out.push(item.trim());
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (item && typeof item === "object") {
|
|
110
|
+
const activity = item.activity;
|
|
111
|
+
if (typeof activity === "string" && activity.trim()) {
|
|
112
|
+
out.push(activity.trim());
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function normalizeActivity(activity) {
|
|
123
|
+
return activity.trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Parse the `scheduleConfigs` JSON string into the trigger configs. Runs BEFORE
|
|
127
|
+
* any mutation so every failure here is safely retryable.
|
|
128
|
+
*/
|
|
129
|
+
export function parseScheduleConfigs(raw) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
message: `scheduleConfigs is not valid JSON (${error instanceof Error ? error.message : "parse error"}). ` +
|
|
138
|
+
SHAPE_HINT,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Unwrap double (or triple) encoding rather than failing on it.
|
|
142
|
+
let unwrapAttempts = 0;
|
|
143
|
+
while (typeof parsed === "string" && unwrapAttempts < 3) {
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(parsed);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
message: `scheduleConfigs decoded to a plain string ("${quote(parsed)}") rather than an array. ${SHAPE_HINT}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
unwrapAttempts++;
|
|
154
|
+
}
|
|
155
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
message: `scheduleConfigs must be a JSON array; received ${parsed === null ? "null" : `a ${typeof parsed}`} (${quote(raw)}). ${SHAPE_HINT}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// A single config object instead of a one-element array is unambiguous.
|
|
162
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
163
|
+
const configs = [];
|
|
164
|
+
const issues = [];
|
|
165
|
+
entries.forEach((entry, index) => {
|
|
166
|
+
const label = `scheduleConfigs[${index}]`;
|
|
167
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
168
|
+
issues.push(`${label} is not an object (received ${quote(entry)})`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const fields = entry;
|
|
172
|
+
const scheduleUuidRaw = fields.scheduleUuid ?? fields.uuid ?? fields.schedule;
|
|
173
|
+
if (typeof scheduleUuidRaw !== "string" || !scheduleUuidRaw.trim()) {
|
|
174
|
+
issues.push(`${label}.scheduleUuid is missing or not a string (received ${quote(scheduleUuidRaw)})`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const scheduleUuid = scheduleUuidRaw.trim();
|
|
178
|
+
if (!UUID_LIKE.test(scheduleUuid)) {
|
|
179
|
+
issues.push(`${label}.scheduleUuid "${scheduleUuid}" is not a UUID — pass the schedule's uuid, not its name`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const activitiesRaw = fields.activities ?? fields.activity ?? fields.triggerSet;
|
|
183
|
+
const activities = coerceActivities(activitiesRaw);
|
|
184
|
+
if (activities === null) {
|
|
185
|
+
issues.push(`${label}.activities must be an array of activity strings (received ${quote(activitiesRaw)})`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (activities.length === 0) {
|
|
189
|
+
issues.push(`${label}.activities is empty — include at least one activity`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const normalized = activities.map(normalizeActivity);
|
|
193
|
+
const unsupported = normalized.filter(activity => !VALID_ACTIVITIES.has(activity));
|
|
194
|
+
if (unsupported.length > 0) {
|
|
195
|
+
issues.push(`${label}.activities contains ${unsupported.length === 1 ? "an unsupported value" : "unsupported values"} ` +
|
|
196
|
+
`${unsupported.map(value => `"${value}"`).join(", ")} — supported camera activities are ` +
|
|
197
|
+
`${SUGGESTED_CAMERA_ACTIVITIES.join(", ")}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
configs.push({ scheduleUuid, activities: normalized });
|
|
201
|
+
});
|
|
202
|
+
if (issues.length > 0) {
|
|
203
|
+
return { ok: false, message: `Invalid scheduleConfigs: ${issues.join("; ")}.` };
|
|
204
|
+
}
|
|
205
|
+
return { ok: true, value: configs };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* True when `scheduleConfigs` is present but carries no configs — the legacy
|
|
209
|
+
* step-1 call passes an empty list, and the old `!== "[]"` string compare missed
|
|
210
|
+
* every other spelling of empty ("[ ]", "[\n]", "\"[]\""), which fell through
|
|
211
|
+
* into policy creation and could create a DUPLICATE policy.
|
|
212
|
+
*/
|
|
213
|
+
export function isEmptyScheduleConfigs(raw) {
|
|
214
|
+
let parsed;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(raw);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
let unwrapAttempts = 0;
|
|
222
|
+
while (typeof parsed === "string" && unwrapAttempts < 3) {
|
|
223
|
+
try {
|
|
224
|
+
parsed = JSON.parse(parsed);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
unwrapAttempts++;
|
|
230
|
+
}
|
|
231
|
+
return Array.isArray(parsed) && parsed.length === 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Parse `cameraUuids` (comma-separated, or a JSON array the model encoded
|
|
235
|
+
* instead) into a uuid list. Runs BEFORE any mutation, for the same reason as
|
|
236
|
+
* the schedule configs: an unparsed camera NAME reaches the API and fails only
|
|
237
|
+
* after the policy exists, leaving a half-built policy behind.
|
|
238
|
+
*/
|
|
239
|
+
export function parseCameraUuids(raw) {
|
|
240
|
+
let values;
|
|
241
|
+
const trimmed = raw.trim();
|
|
242
|
+
if (trimmed.startsWith("[") || trimmed.startsWith('"[')) {
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(trimmed);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
message: `cameraUuids looks like a JSON array but is not valid JSON (${quote(raw)}). Pass a comma-separated list of camera uuids, e.g. "uuidA,uuidB".`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
let unwrapAttempts = 0;
|
|
254
|
+
while (typeof parsed === "string" && unwrapAttempts < 3) {
|
|
255
|
+
try {
|
|
256
|
+
parsed = JSON.parse(parsed);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
unwrapAttempts++;
|
|
262
|
+
}
|
|
263
|
+
if (!Array.isArray(parsed) || parsed.some(item => typeof item !== "string")) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
message: `cameraUuids must be a comma-separated list of camera uuids, e.g. "uuidA,uuidB" (received ${quote(raw)}).`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
values = parsed.map(value => value.trim()).filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
values = trimmed
|
|
273
|
+
.split(",")
|
|
274
|
+
.map(value => value.trim())
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
}
|
|
277
|
+
if (values.length === 0) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
message: `cameraUuids contained no camera uuids (received ${quote(raw)}). Pass a comma-separated list of camera uuids, e.g. "uuidA,uuidB".`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const invalid = values.filter(value => !UUID_LIKE.test(value));
|
|
284
|
+
if (invalid.length > 0) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
message: `cameraUuids contains ${invalid.length === 1 ? "a value that is not a uuid" : "values that are not uuids"}: ` +
|
|
288
|
+
`${invalid.map(value => `"${value}"`).join(", ")} — pass camera uuids, not camera names. ` +
|
|
289
|
+
`Resolve names to uuids with an entity lookup first.`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return { ok: true, value: values };
|
|
293
|
+
}
|
|
@@ -107,7 +107,15 @@ export const DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA = z.union([
|
|
|
107
107
|
export const TOOL_ARGS = {
|
|
108
108
|
requestType: z
|
|
109
109
|
.nativeEnum(DoorScheduleExceptionRequestType)
|
|
110
|
-
.describe(
|
|
110
|
+
.describe(`The type of door schedule exception request to make.
|
|
111
|
+
- create-exception: create an exception. Requires 'exception'. If locationUuid is missing but doorUuids are provided, the tool resolves the location automatically.
|
|
112
|
+
- update-exception: update an exception. Requires 'exception' (with uuid). If intervals are omitted but defaultState and date range are provided, the tool generates a full-day interval.
|
|
113
|
+
- delete-exception: requires 'exceptionUuid'.
|
|
114
|
+
- get-exception: requires 'exceptionUuid'.
|
|
115
|
+
- find-exceptions: find exceptions across the organization, optionally filtered by date range.
|
|
116
|
+
- find-exceptions-for-location: requires 'locationUuid'; optional date range filters.
|
|
117
|
+
- find-exceptions-for-door: requires 'doorUuid'; optional date range filters.
|
|
118
|
+
An exception whose scheduled date is in the past is EXPIRED. The web console lets users toggle whether expired exceptions are shown — mirror that behavior when responding (mention expired ones separately or note they are hidden by default).`),
|
|
111
119
|
exceptionUuid: z
|
|
112
120
|
.string()
|
|
113
121
|
.nullable()
|
|
@@ -120,27 +128,33 @@ export const TOOL_ARGS = {
|
|
|
120
128
|
.string()
|
|
121
129
|
.nullable()
|
|
122
130
|
.describe("Door UUID. Required for 'find-exceptions-for-door'."),
|
|
123
|
-
exception: DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA.nullable().describe(
|
|
131
|
+
exception: DOOR_SCHEDULE_EXCEPTION_INPUT_SCHEMA.nullable().describe(`DoorScheduleExceptionType object. Required for 'create-exception' and 'update-exception'.
|
|
132
|
+
|
|
133
|
+
Mutating doorUuids on an existing exception (add/remove/replace doors): 'update-exception' REPLACES doorUuids with whatever you pass — it is not a delta operation. To safely add or remove doors while preserving the others:
|
|
134
|
+
1. Call 'find-exceptions' (or 'get-exception') to fetch the exception — the response includes the FULL doorUuids array; that IS the current door list.
|
|
135
|
+
2. Compute the new array yourself: remove = filter out the UUIDs to drop; add = append the new UUIDs (deduped); replace = use the new set.
|
|
136
|
+
3. Call 'update-exception' with exception.uuid and exception.doorUuids set to your computed array. Other fields (name, dates, intervals, defaultState) are optional — omit to leave unchanged.
|
|
137
|
+
You already have the current door list in the find-exceptions response. Do not ask the user for it, do not claim you need additional lookups, and do not refuse the mutation citing missing context.`),
|
|
124
138
|
localStartDateRangeStart: z.iso
|
|
125
139
|
.datetime({ offset: true })
|
|
126
140
|
.optional()
|
|
127
141
|
.nullable()
|
|
128
|
-
.describe("Optional date range filter (inclusive) for local start date beginning
|
|
142
|
+
.describe("Optional date range filter (inclusive) for local start date beginning. ISO 8601 datetime with offset, e.g. 2026-07-20T00:00:00Z."),
|
|
129
143
|
localStartDateRangeEnd: z.iso
|
|
130
144
|
.datetime({ offset: true })
|
|
131
145
|
.optional()
|
|
132
146
|
.nullable()
|
|
133
|
-
.describe("Optional date range filter (inclusive) for local start date end
|
|
147
|
+
.describe("Optional date range filter (inclusive) for local start date end. ISO 8601 datetime with offset, e.g. 2026-07-20T23:59:59Z."),
|
|
134
148
|
localEndDateRangeStart: z.iso
|
|
135
149
|
.datetime({ offset: true })
|
|
136
150
|
.optional()
|
|
137
151
|
.nullable()
|
|
138
|
-
.describe("Optional date range filter (inclusive) for local end date beginning
|
|
152
|
+
.describe("Optional date range filter (inclusive) for local end date beginning. ISO 8601 datetime with offset, e.g. 2026-07-20T00:00:00Z."),
|
|
139
153
|
localEndDateRangeEnd: z.iso
|
|
140
154
|
.datetime({ offset: true })
|
|
141
155
|
.optional()
|
|
142
156
|
.nullable()
|
|
143
|
-
.describe("Optional date range filter (inclusive) for local end date end
|
|
157
|
+
.describe("Optional date range filter (inclusive) for local end date end. ISO 8601 datetime with offset, e.g. 2026-07-20T23:59:59Z."),
|
|
144
158
|
includeFields: INCLUDE_FIELDS_ARG,
|
|
145
159
|
filterBy: FILTER_BY_ARG,
|
|
146
160
|
};
|
|
@@ -3,27 +3,43 @@ export var DoorToolRequestType;
|
|
|
3
3
|
(function (DoorToolRequestType) {
|
|
4
4
|
DoorToolRequestType["GET_DOOR_CONTROLLER_RULES"] = "get-door-controller-rules";
|
|
5
5
|
DoorToolRequestType["CREATE_DOOR_CONTROLLER_RULE"] = "create-door-controller-rule";
|
|
6
|
+
DoorToolRequestType["UPDATE_DOOR_CONTROLLER_RULE"] = "update-door-controller-rule";
|
|
7
|
+
DoorToolRequestType["DELETE_DOOR_CONTROLLER_RULE"] = "delete-door-controller-rule";
|
|
6
8
|
DoorToolRequestType["GET_DOOR_POLICIES"] = "get-door-policies";
|
|
7
9
|
DoorToolRequestType["CREATE_DOOR_POLICY"] = "create-door-policy";
|
|
10
|
+
DoorToolRequestType["UPDATE_DOOR_POLICY"] = "update-door-policy";
|
|
11
|
+
DoorToolRequestType["DELETE_DOOR_POLICY"] = "delete-door-policy";
|
|
8
12
|
})(DoorToolRequestType || (DoorToolRequestType = {}));
|
|
9
13
|
export const TOOL_ARGS = {
|
|
10
14
|
requestType: z.nativeEnum(DoorToolRequestType).describe("The type of door operation to perform."),
|
|
11
15
|
doorControllerUuid: z
|
|
12
16
|
.string()
|
|
13
17
|
.nullable()
|
|
14
|
-
.describe("The UUID of the door controller device. Required for '
|
|
18
|
+
.describe("The UUID of the door controller device. Required for every 'door-controller-rule' operation — a rule is addressed by controller AND rule uuid, never by rule uuid alone."),
|
|
19
|
+
ruleUuid: z
|
|
20
|
+
.string()
|
|
21
|
+
.nullable()
|
|
22
|
+
.describe("The UUID of the door controller rule. Required for 'update-door-controller-rule' and 'delete-door-controller-rule'. Get it from 'get-door-controller-rules' — do not guess one."),
|
|
15
23
|
ruleConfig: z
|
|
16
24
|
.string()
|
|
17
25
|
.nullable()
|
|
18
|
-
.describe("JSON string of the door controller rule configuration. Required for 'create-door-controller-rule'."),
|
|
26
|
+
.describe("JSON string of the door controller rule configuration. Required for 'create-door-controller-rule' and 'update-door-controller-rule'. For an update this is MERGED over the rule's current configuration, so send only the fields being changed."),
|
|
27
|
+
policyUuid: z
|
|
28
|
+
.string()
|
|
29
|
+
.nullable()
|
|
30
|
+
.describe("The UUID of the door policy. Required for 'update-door-policy' and 'delete-door-policy'. Get it from 'get-door-policies' — do not guess one."),
|
|
19
31
|
policyName: z
|
|
20
32
|
.string()
|
|
21
33
|
.nullable()
|
|
22
|
-
.describe("The name for the
|
|
34
|
+
.describe("The name for the door policy. Required for 'create-door-policy'; for 'update-door-policy' it is the new name (omit to leave the name unchanged)."),
|
|
23
35
|
policyConfig: z
|
|
24
36
|
.string()
|
|
25
37
|
.nullable()
|
|
26
|
-
.describe("JSON string of door policy configuration. Required for 'create-door-policy'."),
|
|
38
|
+
.describe("JSON string of door policy configuration. Required for 'create-door-policy'; optional for 'update-door-policy', where it is MERGED over the policy's current configuration so only the fields being changed need sending."),
|
|
39
|
+
confirmDelete: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.nullable()
|
|
42
|
+
.describe("Required to be true for 'delete-door-policy' and 'delete-door-controller-rule'. Both are irreversible and change how doors behave, so they refuse without it. Confirm with the user first."),
|
|
27
43
|
};
|
|
28
44
|
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
29
45
|
export const OUTPUT_SCHEMA = z.object({
|
|
@@ -60,5 +76,21 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
60
76
|
})
|
|
61
77
|
.optional()
|
|
62
78
|
.describe("Result of creating a door policy"),
|
|
79
|
+
updated: z
|
|
80
|
+
.object({ success: z.boolean().optional(), uuid: z.string().optional() })
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("Result of an update request"),
|
|
83
|
+
deleted: z
|
|
84
|
+
.object({ success: z.boolean().optional(), uuid: z.string().optional() })
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Result of a delete request"),
|
|
87
|
+
note: z
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("A caveat about this result that the user needs to be told."),
|
|
91
|
+
warningMsg: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("A warning from the Rhombus API — the call succeeded, but with a caveat."),
|
|
63
95
|
error: z.string().optional().describe("An error message if the request failed."),
|
|
64
96
|
});
|
|
@@ -2,13 +2,18 @@ import { z } from "zod";
|
|
|
2
2
|
import { createUuidSchema } from "../types.js";
|
|
3
3
|
import { TempUnit } from "../utils/temp.js";
|
|
4
4
|
export const TOOL_ARGS = {
|
|
5
|
-
deviceUuids: z
|
|
5
|
+
deviceUuids: z
|
|
6
|
+
.array(createUuidSchema())
|
|
7
|
+
.min(1)
|
|
8
|
+
.nullable()
|
|
9
|
+
.describe("The UUIDs of the devices to look up. Pass null to return ALL entities of every type (no UUID filter) and narrow with filterBy/includeFields instead. NEVER invent placeholder UUIDs — an unknown UUID matches nothing and returns empty results."),
|
|
6
10
|
timeZone: z
|
|
7
11
|
.string()
|
|
8
12
|
.describe("The timezone for formatting timestamps. This is necessary for the tool to produce accurate formatted timestamps."),
|
|
9
13
|
tempUnit: z
|
|
10
14
|
.nativeEnum(TempUnit)
|
|
11
15
|
.nullable()
|
|
16
|
+
.default(null)
|
|
12
17
|
.describe("The unit of temperature to return, if applicable. Default is Celsius."),
|
|
13
18
|
};
|
|
14
19
|
const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
|
|
@@ -19,21 +19,21 @@ export var EventsToolRequestType;
|
|
|
19
19
|
export const TOOL_ARGS = {
|
|
20
20
|
eventType: z
|
|
21
21
|
.nativeEnum(EventsToolRequestType)
|
|
22
|
-
.describe("The type of events to retrieve. " +
|
|
23
|
-
"access-control: Access control events
|
|
24
|
-
"brivo-access-control: Badge/credential events from Brivo-integrated doors. Does not require door UUIDs — automatically
|
|
25
|
-
"environmental-gateway: Environmental gateway events
|
|
26
|
-
"climate-sensor: Climate sensor events
|
|
27
|
-
"component-events: All
|
|
28
|
-
"camera: Footage seekpoints for
|
|
29
|
-
"button-press: Button press events from
|
|
30
|
-
"occupancy: Occupancy sensor events with people count
|
|
31
|
-
"proximity: Proximity tag events with RSSI readings
|
|
32
|
-
"doorbell: Doorbell camera events."),
|
|
22
|
+
.describe("The type of events to retrieve. Every mode takes startTime and endTime (ISO 8601); the per-mode UUID argument is named below and in that argument's own description.\n\n" +
|
|
23
|
+
"access-control: Access control events (arrivals, badge ins, credentials, unlocks) for the doors in accessControlledDoorUuids. Can return a lot of data — use a narrow time range. The returned `credSource` field says how the event was triggered: REMOTE = Rhombus Key app remote unlock; 'REMOTE (Admin)' = unlock via the Rhombus console or browser/mobile app; BLE_WAVE = user waved a hand over the reader; NFC = user tapped a badge or phone on the reader.\n" +
|
|
24
|
+
"brivo-access-control: Badge/credential events from Brivo-integrated doors. Does not require door UUIDs — automatically fetches the Brivo integration configuration to determine which locations have Brivo doors mapped. Returns integrationEnabled, brivoDoorsConfigured, the brivoDoors list (Brivo IDs, names, Rhombus location UUIDs), and credential-received events newest first. Events are fetched at the LOCATION level, so results may include events from all access-controlled doors at locations where Brivo is configured.\n" +
|
|
25
|
+
"environmental-gateway: Environmental gateway events (sensor readings and derived values) for deviceUuid.\n" +
|
|
26
|
+
"climate-sensor: Climate sensor events (temperature, humidity, air quality, vape/THC detection, battery) for sensorUuid. Cap the row count with limit (default 1000).\n" +
|
|
27
|
+
"component-events: All component event types for locationUuid — the most flexible option. Narrow it with componentEventTypes (see that argument for the full list; omit it for every type).\n" +
|
|
28
|
+
"camera: Footage seekpoints for cameraUuid over a window of `duration` seconds starting at startTime — all timeline activity types that camera recorded (human motion, vehicle motion, etc., depending on device/analytics), each with an activity string and timestamp; plate/vehicle/face fields appear when the API provides them. For org LPR saved vehicles, labels, and plate search APIs, use lpr-tool.\n" +
|
|
29
|
+
"button-press: Button press events from the sensor in buttonSensorUuid.\n" +
|
|
30
|
+
"occupancy: Occupancy sensor events with people count, for occupancySensorUuid.\n" +
|
|
31
|
+
"proximity: Proximity tag events with RSSI readings, for proximityTagUuids.\n" +
|
|
32
|
+
"doorbell: Doorbell camera events for doorbellCameraUuid."),
|
|
33
33
|
startTime: z
|
|
34
34
|
.string()
|
|
35
35
|
.datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
|
|
36
|
-
.describe("A timestamp representing when to start the search for access control events." +
|
|
36
|
+
.describe("A timestamp representing when to start the search for access control events. Keep the window as narrow as the question allows (e.g. the last 24 hours) — access-control results are returned newest-first and capped at the newest 500 events per door, so an over-wide window only slows the search without returning more." +
|
|
37
37
|
ISOTimestampFormatDescription),
|
|
38
38
|
endTime: z
|
|
39
39
|
.string()
|
|
@@ -51,7 +51,7 @@ export const TOOL_ARGS = {
|
|
|
51
51
|
sensorUuid: z
|
|
52
52
|
.string()
|
|
53
53
|
.nullable()
|
|
54
|
-
.describe("The UUID of the climate sensor. Required when eventType is 'climate-sensor'. Can be obtained from the get-entity-tool for
|
|
54
|
+
.describe("The UUID of the climate sensor. Required when eventType is 'climate-sensor'. Can be obtained from the get-entity-tool for ENVIRONMENTAL_SENSOR."),
|
|
55
55
|
limit: z
|
|
56
56
|
.number()
|
|
57
57
|
.int()
|
|
@@ -61,7 +61,11 @@ export const TOOL_ARGS = {
|
|
|
61
61
|
locationUuid: z
|
|
62
62
|
.string()
|
|
63
63
|
.nullable()
|
|
64
|
-
.describe("The UUID of the location. Required when eventType is 'component-events'."
|
|
64
|
+
.describe("The UUID of the location. Required when eventType is 'component-events'. Must be a UUID returned by a " +
|
|
65
|
+
"prior tool call in this conversation (get-entity-tool for LOCATION, or the locationUuid on an " +
|
|
66
|
+
"access-control-door) — never guess or reuse a UUID from memory. Scoping to a location that has no " +
|
|
67
|
+
"access-controlled doors silently returns zero door events; to review door activity org-wide, query " +
|
|
68
|
+
"each location that actually has doors."),
|
|
65
69
|
componentEventTypes: z
|
|
66
70
|
.array(z.nativeEnum(ComponentEventEnumType))
|
|
67
71
|
.nullable()
|
|
@@ -76,7 +80,7 @@ export const TOOL_ARGS = {
|
|
|
76
80
|
"AperioGatewayConnectionStateChangeEvent, AperioDtcEvent, AperioTamperStateEvent."),
|
|
77
81
|
timeZone: z
|
|
78
82
|
.string()
|
|
79
|
-
.describe("The timezone of the requested locations or devices. This is necessary for the tool to produce accurate formatted timestamps."),
|
|
83
|
+
.describe("The timezone of the requested locations or devices. This is necessary for the tool to produce accurate formatted timestamps. Formatted event timestamps come back in the device / sensor / location timezone, not necessarily UTC."),
|
|
80
84
|
cameraUuid: z
|
|
81
85
|
.string()
|
|
82
86
|
.nullable()
|
|
@@ -164,7 +168,7 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
164
168
|
.describe("Number of Brivo doors configured in the integration"),
|
|
165
169
|
brivoDoors: z
|
|
166
170
|
.array(z.object({
|
|
167
|
-
|
|
171
|
+
brivoDoorId: z.string().describe("Brivo's door ID"),
|
|
168
172
|
doorName: z.string().optional().describe("Brivo door name"),
|
|
169
173
|
locationUuid: z.string().optional().describe("Rhombus location UUID for this door"),
|
|
170
174
|
}))
|
|
@@ -371,4 +375,8 @@ export const OUTPUT_SCHEMA = z.object({
|
|
|
371
375
|
.describe("Doorbell camera events"),
|
|
372
376
|
needUserInput: z.boolean().optional(),
|
|
373
377
|
commandForUser: z.string().optional(),
|
|
378
|
+
note: z
|
|
379
|
+
.string()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("Diagnostic note about why a result set may be empty or incomplete. Read it before concluding that no events occurred."),
|
|
374
382
|
});
|