rhombus-node-mcp 0.1.15 → 0.1.17

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 (80) hide show
  1. package/README.md +9 -1
  2. package/dist/api/camera-tool-api.js +161 -0
  3. package/dist/api/clips-tool-api.js +36 -0
  4. package/dist/api/create-camera-policy-tool-api.js +9 -0
  5. package/dist/api/create-tool-api.js +53 -0
  6. package/dist/api/entity-lookup-tool-api.js +65 -0
  7. package/dist/api/events-tool-api.js +320 -0
  8. package/dist/api/faces-tool-api.js +110 -0
  9. package/dist/api/get-entity-tool-api.js +176 -0
  10. package/dist/api/get-org-information-tool-api.js +9 -0
  11. package/dist/api/location-tool-api.js +9 -0
  12. package/dist/api/lpr-tool-api.js +68 -0
  13. package/dist/api/policy-alerts-tool-api.js +42 -0
  14. package/dist/api/reboot-cameras-tool-api.js +34 -0
  15. package/dist/api/report-tool-api.js +426 -0
  16. package/dist/api/time-tool-api.js +90 -0
  17. package/dist/api/update-tool-api.js +148 -0
  18. package/dist/createServer.js +7 -1
  19. package/dist/disabled-tools/endpoint-to-keys-tool.js +84 -0
  20. package/dist/disabled-tools/semantic-search-tool.js +90 -0
  21. package/dist/index.js +3 -5
  22. package/dist/logger.js +4 -3
  23. package/dist/network.js +42 -15
  24. package/dist/resources/routes.json.js +1 -1
  25. package/dist/services/embedding-service.js +153 -0
  26. package/dist/services/faiss-search-service.js +261 -0
  27. package/dist/tools/camera-tool.js +100 -0
  28. package/dist/tools/clips-tool.js +23 -37
  29. package/dist/tools/count-tool.js +25 -0
  30. package/dist/tools/create-camera-policy-tool.js +214 -0
  31. package/dist/tools/create-tool.js +25 -74
  32. package/dist/tools/entity-lookup-tool.js +36 -0
  33. package/dist/tools/events-tool.js +188 -92
  34. package/dist/tools/faces-tool.js +59 -132
  35. package/dist/tools/get-entity-tool.js +78 -0
  36. package/dist/tools/get-org-information-tool.js +18 -0
  37. package/dist/tools/location-tool.js +24 -31
  38. package/dist/tools/lpr-tool.js +79 -0
  39. package/dist/tools/policy-alerts-tool.js +34 -42
  40. package/dist/tools/reboot-cameras-tool.js +34 -0
  41. package/dist/tools/report-tool.js +190 -0
  42. package/dist/tools/time-conversion-tool.js +43 -0
  43. package/dist/tools/time-tool.js +17 -57
  44. package/dist/tools/update-tool.js +262 -0
  45. package/dist/transports/streamable-http.js +160 -57
  46. package/dist/{tools/devices/camera-tool/types.js → types/camera-tool-types.js} +52 -0
  47. package/dist/types/clips-tool-types.js +41 -0
  48. package/dist/types/create-camera-policy-tool-types.js +44 -0
  49. package/dist/types/create-tool-types.js +8 -0
  50. package/dist/types/deviceType.js +1 -0
  51. package/dist/types/endpoint-to-keys-tool-types.js +7 -0
  52. package/dist/types/entity-lookup-tool-types.js +70 -0
  53. package/dist/types/events-tools-types.js +257 -0
  54. package/dist/types/faces-tools-types.js +143 -0
  55. package/dist/types/get-entity-tool-types.js +25 -0
  56. package/dist/types/get-org-information-tool-types.js +3 -0
  57. package/dist/types/location-tool-types.js +11 -0
  58. package/dist/types/lpr-tool-types.js +97 -0
  59. package/dist/types/policy-alerts-tool-types.js +74 -0
  60. package/dist/types/reboot-cameras-tool-types.js +8 -0
  61. package/dist/types/report-tool-types.js +268 -0
  62. package/dist/types/schema-components.js +7093 -0
  63. package/dist/types/schema.js +1 -0
  64. package/dist/types/semantic-search-tool-types.js +5 -0
  65. package/dist/types/time-conversion-tool-types.js +8 -0
  66. package/dist/types/time-tool-types.js +11 -0
  67. package/dist/types/update-tool-types.js +186 -0
  68. package/dist/types/zod-schemas.js +21315 -0
  69. package/dist/types.js +17 -7
  70. package/dist/util.js +94 -2
  71. package/dist/utils/confirmation.js +1 -1
  72. package/dist/utils/reduce-output.js +35 -0
  73. package/dist/utils/remove-nulls.js +28 -0
  74. package/dist/utils/temp.js +8 -0
  75. package/dist/utils/timestampInput.js +12 -0
  76. package/package.json +23 -3
  77. package/dist/tools/devices/camera-tool/camera-tool.js +0 -218
  78. package/dist/tools/devices/get-entity-tool.js +0 -118
  79. package/dist/tools/get-org-information.js +0 -17
  80. package/dist/tools/reboot-cameras.js +0 -62
@@ -0,0 +1,426 @@
1
+ import { getLogger } from "../logger.js";
2
+ import { postApi } from "../network.js";
3
+ import { formatTimestamp } from "../util.js";
4
+ import { GetCountReportV2WSRequestTypesEnum } from "../types/schema-components.js";
5
+ import { DateTime } from "luxon";
6
+ const REPORT_TYPES_THAT_RETURN_UTC = new Set([
7
+ GetCountReportV2WSRequestTypesEnum.BANDWIDTH,
8
+ GetCountReportV2WSRequestTypesEnum.VEHICLES,
9
+ GetCountReportV2WSRequestTypesEnum.MOTION,
10
+ GetCountReportV2WSRequestTypesEnum.DWELL,
11
+ ]);
12
+ const logger = getLogger();
13
+ function getUtcTime(datetime, reportType, timeZone) {
14
+ if (REPORT_TYPES_THAT_RETURN_UTC.has(reportType)) {
15
+ return datetime;
16
+ }
17
+ else {
18
+ logger.info("timeZone", datetime);
19
+ return DateTime.fromISO(datetime, { zone: timeZone }).toUTC().toISO();
20
+ }
21
+ }
22
+ export async function getOccupancyCountReport(deviceUuid, startTimeMs, endTimeMs, interval, requestModifiers, sessionId) {
23
+ const body = {
24
+ deviceUuid,
25
+ startTimeMs,
26
+ endTimeMs,
27
+ interval,
28
+ };
29
+ const response = await postApi({
30
+ route: "/report/getOccupancyCounts",
31
+ body,
32
+ modifiers: requestModifiers,
33
+ sessionId,
34
+ });
35
+ response.timeSeriesDataPoints = (response.timeSeriesDataPoints || []).map(dataPoint => {
36
+ const dateLocalMs = new Date(dataPoint.dateLocal || "").getTime();
37
+ const dateUtcMs = new Date(dataPoint.dateUtc || "").getTime();
38
+ logger.info("timeSeriesDataPoints dataPoint:", JSON.stringify(dataPoint));
39
+ return {
40
+ ...dataPoint,
41
+ dateLocalString: formatTimestamp(dateLocalMs),
42
+ dateUtcString: formatTimestamp(dateUtcMs),
43
+ };
44
+ });
45
+ return response;
46
+ }
47
+ export async function getSummaryCountReport(interval, scope, types, uuid, endTimeMs, startTimeMs, requestModifiers, sessionId, timeZone) {
48
+ logger.info("📊 Getting summary count report", JSON.stringify({
49
+ interval,
50
+ scope,
51
+ types,
52
+ endTimeMs,
53
+ startTimeMs,
54
+ uuid,
55
+ }));
56
+ const body = {
57
+ endTimeMs,
58
+ interval,
59
+ scope,
60
+ startTimeMs,
61
+ types,
62
+ ...(uuid ? { uuid } : {}),
63
+ };
64
+ const response = await postApi({
65
+ route: "/report/getCountReportV2",
66
+ body,
67
+ modifiers: requestModifiers,
68
+ sessionId,
69
+ });
70
+ // Process response to convert date strings to UTC milliseconds timestamps
71
+ let newTimeSeriesDataPoints = undefined;
72
+ if (response && response.timeSeriesDataPoints && Array.isArray(response.timeSeriesDataPoints)) {
73
+ newTimeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint) => {
74
+ const sanitizedDataPoint = {
75
+ eventCountMap: dataPoint.eventCountMap,
76
+ dateUtcString: getUtcTime(dataPoint.dateLocal, dataPoint.type, timeZone),
77
+ };
78
+ return sanitizedDataPoint;
79
+ });
80
+ }
81
+ return {
82
+ error: response.error,
83
+ errorMsg: response.errorMsg ?? undefined,
84
+ timeSeriesDataPoints: newTimeSeriesDataPoints,
85
+ };
86
+ }
87
+ export async function getOccupancyEnabledCameras(requestModifiers, sessionId) {
88
+ logger.info("📷 Getting occupancy enabled cameras");
89
+ const body = {};
90
+ const response = await postApi({
91
+ route: "/camera/getOccupancyEnabledCameras",
92
+ body,
93
+ modifiers: requestModifiers,
94
+ sessionId,
95
+ });
96
+ // Transform the response to handle null values
97
+ const cameras = response.cameras
98
+ ? response.cameras.map(camera => ({
99
+ uuid: camera.uuid ?? undefined,
100
+ deviceUuid: camera.deviceUuid ?? undefined,
101
+ name: camera.name ?? undefined,
102
+ serialNumber: camera.serialNumber ?? undefined,
103
+ locationUuid: camera.locationUuid ?? undefined,
104
+ facetNameMap: camera.facetNameMap ?? undefined,
105
+ deleted: camera.deleted ?? undefined,
106
+ pending: camera.pending ?? undefined,
107
+ mummified: camera.mummified ?? undefined,
108
+ }))
109
+ : undefined;
110
+ return {
111
+ error: response.error ?? undefined,
112
+ errorMsg: response.errorMsg ?? undefined,
113
+ cameras,
114
+ };
115
+ }
116
+ export async function getLineCrossingEnabledCameras(locationUuid, requestModifiers, sessionId) {
117
+ logger.info("🚶 Getting line crossing enabled cameras for location", locationUuid);
118
+ const body = {
119
+ locationUuid,
120
+ };
121
+ const response = await postApi({
122
+ route: "/camera/getLineCrossingEnabledCamerasForLocation",
123
+ body,
124
+ modifiers: requestModifiers,
125
+ sessionId,
126
+ });
127
+ return {
128
+ error: response.error ?? undefined,
129
+ errorMsg: response.errorMsg ?? undefined,
130
+ camerasToConfigs: response.camerasToConfigs ?? undefined,
131
+ };
132
+ }
133
+ export async function getThresholdCrossingCountReport(deviceUuid, startTimeMs, endTimeMs, bucketSize, crossingObject, dedupe, requestModifiers, sessionId) {
134
+ logger.info("🚪 Getting threshold crossing count report", JSON.stringify({
135
+ deviceUuid,
136
+ startTimeMs,
137
+ endTimeMs,
138
+ bucketSize,
139
+ crossingObject,
140
+ dedupe,
141
+ }));
142
+ const body = {
143
+ deviceUuid,
144
+ startTimeMs,
145
+ endTimeMs,
146
+ bucketSize,
147
+ crossingObject,
148
+ dedupe,
149
+ };
150
+ const response = await postApi({
151
+ route: "/report/getThresholdCrossingCountReport",
152
+ body,
153
+ modifiers: requestModifiers,
154
+ sessionId,
155
+ });
156
+ // Transform the response to handle null values
157
+ const crossingCounts = response.crossingCounts
158
+ ? response.crossingCounts.map(count => ({
159
+ timestampMs: count.timestampMs ?? undefined,
160
+ ingressCount: count.ingressCount ?? undefined,
161
+ egressCount: count.egressCount ?? undefined,
162
+ }))
163
+ : undefined;
164
+ // Calculate metrics if we have crossing counts and the bucket size is appropriate
165
+ let metrics = undefined;
166
+ if (crossingCounts && crossingCounts.length > 0) {
167
+ // Calculate total entries and exits
168
+ let totalEntries = 0;
169
+ let totalExits = 0;
170
+ let maxEntries = 0;
171
+ let maxExits = 0;
172
+ let maxTotal = 0;
173
+ let maxEntriesTimestamp;
174
+ let maxExitsTimestamp;
175
+ let maxTotalTimestamp;
176
+ let maxTotalEntries = 0;
177
+ let maxTotalExits = 0;
178
+ crossingCounts.forEach(count => {
179
+ const entries = count.ingressCount || 0;
180
+ const exits = count.egressCount || 0;
181
+ const total = entries + exits;
182
+ totalEntries += entries;
183
+ totalExits += exits;
184
+ // Track max entries
185
+ if (entries > maxEntries) {
186
+ maxEntries = entries;
187
+ maxEntriesTimestamp = count.timestampMs;
188
+ }
189
+ // Track max exits
190
+ if (exits > maxExits) {
191
+ maxExits = exits;
192
+ maxExitsTimestamp = count.timestampMs;
193
+ }
194
+ // Track busiest hour (max total)
195
+ if (total > maxTotal) {
196
+ maxTotal = total;
197
+ maxTotalTimestamp = count.timestampMs;
198
+ maxTotalEntries = entries;
199
+ maxTotalExits = exits;
200
+ }
201
+ });
202
+ // Calculate hours based on bucket size
203
+ let hoursInPeriod = 1;
204
+ const periodMs = endTimeMs - startTimeMs;
205
+ if (bucketSize === "HOUR") {
206
+ hoursInPeriod = periodMs / (1000 * 60 * 60);
207
+ }
208
+ else if (bucketSize === "QUARTER_HOUR") {
209
+ // For quarter hour buckets, we need to aggregate to hourly
210
+ hoursInPeriod = periodMs / (1000 * 60 * 60);
211
+ // Note: For more accurate hourly calculations with quarter-hour buckets,
212
+ // we'd need to group by hour, but for now we'll use the total period
213
+ }
214
+ else if (bucketSize === "DAY") {
215
+ hoursInPeriod = periodMs / (1000 * 60 * 60);
216
+ }
217
+ else if (bucketSize === "WEEK") {
218
+ hoursInPeriod = periodMs / (1000 * 60 * 60);
219
+ }
220
+ // Helper function to format hour labels
221
+ const formatHourLabel = (timestampMs) => {
222
+ if (!timestampMs)
223
+ return "Unknown";
224
+ const date = DateTime.fromMillis(timestampMs);
225
+ if (bucketSize === "HOUR") {
226
+ const endHour = date.plus({ hours: 1 });
227
+ return `${date.toFormat("h:mm a")} - ${endHour.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`;
228
+ }
229
+ else if (bucketSize === "QUARTER_HOUR") {
230
+ const endTime = date.plus({ minutes: 15 });
231
+ return `${date.toFormat("h:mm a")} - ${endTime.toFormat("h:mm a")} (${date.toFormat("MMM d, yyyy")})`;
232
+ }
233
+ else if (bucketSize === "DAY") {
234
+ return date.toFormat("MMM d, yyyy");
235
+ }
236
+ else if (bucketSize === "WEEK") {
237
+ const endWeek = date.plus({ weeks: 1 }).minus({ days: 1 });
238
+ return `Week of ${date.toFormat("MMM d")} - ${endWeek.toFormat("MMM d, yyyy")}`;
239
+ }
240
+ return date.toISO() || "Unknown";
241
+ };
242
+ metrics = {
243
+ averageEntriesPerHour: totalEntries / hoursInPeriod,
244
+ averageExitsPerHour: totalExits / hoursInPeriod,
245
+ mostEntriesInHour: {
246
+ count: maxEntries,
247
+ timestamp: maxEntriesTimestamp ? DateTime.fromMillis(maxEntriesTimestamp).toISO() : "",
248
+ hourLabel: formatHourLabel(maxEntriesTimestamp),
249
+ },
250
+ mostExitsInHour: {
251
+ count: maxExits,
252
+ timestamp: maxExitsTimestamp ? DateTime.fromMillis(maxExitsTimestamp).toISO() : "",
253
+ hourLabel: formatHourLabel(maxExitsTimestamp),
254
+ },
255
+ busiestHour: {
256
+ totalCount: maxTotal,
257
+ timestamp: maxTotalTimestamp ? DateTime.fromMillis(maxTotalTimestamp).toISO() : "",
258
+ hourLabel: formatHourLabel(maxTotalTimestamp),
259
+ entries: maxTotalEntries,
260
+ exits: maxTotalExits,
261
+ },
262
+ };
263
+ }
264
+ return {
265
+ error: response.error ?? undefined,
266
+ errorMsg: response.errorMsg ?? undefined,
267
+ crossingCounts,
268
+ metrics,
269
+ };
270
+ }
271
+ export async function findPromptConfigurations(requestModifiers, sessionId) {
272
+ logger.info("🔍 Finding custom event prompt configurations");
273
+ const body = {};
274
+ const response = await postApi({
275
+ route: "/scenequery/findPromptConfigurations",
276
+ body,
277
+ modifiers: requestModifiers,
278
+ sessionId,
279
+ });
280
+ // Transform the response to handle null values
281
+ const promptConfigurations = response.promptConfigurations
282
+ ? response.promptConfigurations.map(config => ({
283
+ active: config.active ?? undefined,
284
+ cameraConfigurations: config.cameraConfigurations ?? undefined,
285
+ checkCondition: config.checkCondition ?? undefined,
286
+ description: config.description ?? undefined,
287
+ name: config.name ?? undefined,
288
+ orgUuid: config.orgUuid ?? undefined,
289
+ prompt: config.prompt ?? undefined,
290
+ promptType: config.promptType ?? undefined,
291
+ scheduleUuid: config.scheduleUuid ?? undefined,
292
+ shortName: config.shortName ?? undefined,
293
+ uuid: config.uuid ?? undefined,
294
+ }))
295
+ : undefined;
296
+ return {
297
+ error: response.error ?? undefined,
298
+ errorMsg: response.errorMsg ?? undefined,
299
+ promptConfigurations,
300
+ };
301
+ }
302
+ export async function getCustomLLMReport(promptUuid, promptType, startTimeMs, endTimeMs, interval, requestModifiers, sessionId) {
303
+ logger.info("📊 Getting custom LLM report", JSON.stringify({
304
+ promptUuid,
305
+ promptType,
306
+ startTimeMs,
307
+ endTimeMs,
308
+ interval,
309
+ }));
310
+ const body = {
311
+ promptUuid,
312
+ startTimeMs,
313
+ endTimeMs,
314
+ interval,
315
+ };
316
+ // Determine the correct endpoint based on promptType
317
+ let route;
318
+ switch (promptType) {
319
+ case "COUNT":
320
+ route = "/report/getCustomLLMNumericCounts";
321
+ break;
322
+ case "PERCENT":
323
+ route = "/report/getCustomLLMReport";
324
+ break;
325
+ case "BOOLEAN":
326
+ route = "/report/getCustomLLMBinaryCounts";
327
+ break;
328
+ default:
329
+ throw new Error(`Unknown prompt type: ${promptType}`);
330
+ }
331
+ const response = await postApi({
332
+ route,
333
+ body,
334
+ modifiers: requestModifiers,
335
+ sessionId,
336
+ });
337
+ logger.info("📊 Custom LLM API response:", JSON.stringify({
338
+ promptType,
339
+ hasReports: !!response.reports,
340
+ hasTimeSeriesDataPoints: !!response.timeSeriesDataPoints,
341
+ error: response.error,
342
+ }));
343
+ // Different prompt types return different response structures
344
+ let timeSeriesDataPoints;
345
+ if ((promptType === "BOOLEAN" || promptType === "PERCENT") && response.reports) {
346
+ // BOOLEAN and PERCENT types return a reports object with device UUIDs as keys
347
+ const reportEntries = Object.entries(response.reports);
348
+ logger.info(`📊 ${promptType} response structure:`, JSON.stringify({ reportEntries: reportEntries.length }));
349
+ // Aggregate data from all devices
350
+ const aggregatedData = {};
351
+ reportEntries.forEach(([deviceId, reportData]) => {
352
+ if (Array.isArray(reportData)) {
353
+ reportData.forEach((item) => {
354
+ const dateKey = item.localDate || new Date().toISOString();
355
+ if (!aggregatedData[dateKey]) {
356
+ aggregatedData[dateKey] = {
357
+ dateLocal: item.localDate,
358
+ eventCount: 0,
359
+ true: 0,
360
+ false: 0,
361
+ // For PERCENT type, store percentage values
362
+ percentages: {},
363
+ };
364
+ }
365
+ if (promptType === "BOOLEAN") {
366
+ aggregatedData[dateKey].eventCount += item.eventCount || 0;
367
+ aggregatedData[dateKey].true += item.true || 0;
368
+ aggregatedData[dateKey].false += item.false || 0;
369
+ }
370
+ else if (promptType === "PERCENT") {
371
+ // For PERCENT type, aggregate percentage data
372
+ aggregatedData[dateKey].eventCount += item.eventCount || 0;
373
+ // Store any percentage-related fields from the response
374
+ Object.entries(item).forEach(([key, value]) => {
375
+ if (key !== "localDate" && key !== "eventCount") {
376
+ // Store any numeric values as percentages (could be number or string percentage)
377
+ aggregatedData[dateKey].percentages[key] = value;
378
+ }
379
+ });
380
+ }
381
+ });
382
+ }
383
+ });
384
+ // Convert aggregated data to timeSeriesDataPoints format
385
+ timeSeriesDataPoints = Object.entries(aggregatedData).map(([date, data]) => ({
386
+ dateLocal: data.dateLocal ?? undefined,
387
+ dateUtc: data.dateLocal ?? undefined, // Convert to UTC if needed
388
+ eventCountMap: promptType === "BOOLEAN"
389
+ ? {
390
+ total: data.eventCount,
391
+ true: data.true,
392
+ false: data.false,
393
+ }
394
+ : {
395
+ total: data.eventCount,
396
+ ...data.percentages,
397
+ },
398
+ }));
399
+ }
400
+ else if (response.timeSeriesDataPoints) {
401
+ // COUNT type uses the standard timeSeriesDataPoints format
402
+ timeSeriesDataPoints = response.timeSeriesDataPoints.map((dataPoint) => ({
403
+ dateLocal: dataPoint.dateLocal ?? undefined,
404
+ dateUtc: dataPoint.dateUtc ?? undefined,
405
+ eventCountMap: dataPoint.eventCountMap
406
+ ? Object.entries(dataPoint.eventCountMap).reduce((acc, [key, value]) => {
407
+ if (value !== null && value !== undefined) {
408
+ acc[key] = value;
409
+ }
410
+ return acc;
411
+ }, {})
412
+ : undefined,
413
+ }));
414
+ }
415
+ const result = {
416
+ error: response.error ?? undefined,
417
+ errorMsg: response.errorMsg ?? undefined,
418
+ timeSeriesDataPoints,
419
+ };
420
+ logger.info("📊 Custom LLM report result:", JSON.stringify({
421
+ hasError: !!result.error,
422
+ dataPointsCount: timeSeriesDataPoints?.length ?? 0,
423
+ firstDataPoint: timeSeriesDataPoints?.[0],
424
+ }));
425
+ return result;
426
+ }
@@ -0,0 +1,90 @@
1
+ import { parse } from "chrono-node";
2
+ import { DateTime } from "luxon";
3
+ import { logger } from "../logger.js";
4
+ export function nullToUndefined(value) {
5
+ return value === null ? undefined : value;
6
+ }
7
+ function normalizeTimeDescription(description) {
8
+ const normalized = description.toLowerCase().trim();
9
+ if (normalized.includes("current time")) {
10
+ return "now";
11
+ }
12
+ // Handle plain day references as start of day
13
+ if (normalized === "today") {
14
+ return "today at 00:00";
15
+ }
16
+ if (normalized === "yesterday") {
17
+ return "yesterday at 00:00";
18
+ }
19
+ if (normalized === "tomorrow") {
20
+ return "tomorrow at 00:00";
21
+ }
22
+ // Handle "this" time periods - always refer to today
23
+ if (normalized === "this morning") {
24
+ return "today at 06:00";
25
+ }
26
+ if (normalized === "this afternoon") {
27
+ return "today at 12:00";
28
+ }
29
+ if (normalized === "this evening") {
30
+ return "today at 18:00";
31
+ }
32
+ if (normalized === "this night" || normalized === "tonight") {
33
+ return "today at 20:00";
34
+ }
35
+ if (normalized.includes("start of today") || normalized.includes("beginning of today")) {
36
+ return "today at 00:00";
37
+ }
38
+ if (normalized.includes("start of yesterday") || normalized.includes("beginning of yesterday")) {
39
+ return "yesterday at 00:00";
40
+ }
41
+ if (normalized.includes("start of tomorrow") || normalized.includes("beginning of tomorrow")) {
42
+ return "tomorrow at 00:00";
43
+ }
44
+ if (normalized.includes("end of today")) {
45
+ return "today at 23:59:59";
46
+ }
47
+ if (normalized.includes("end of yesterday")) {
48
+ return "yesterday at 23:59:59";
49
+ }
50
+ if (normalized.includes("end of tomorrow")) {
51
+ return "tomorrow at 23:59:59";
52
+ }
53
+ return description;
54
+ }
55
+ export function parseTimeDescription(time_description, timezone, extra) {
56
+ logger.info("EXTRA", extra);
57
+ const now = new Date(DateTime.now().setZone(timezone || "America/Los_Angeles").toISO({ includeOffset: false }));
58
+ const normalizedDescription = normalizeTimeDescription(time_description);
59
+ logger.info(`TIME TOOL ${timezone}: Normalized "${time_description}" to "${normalizedDescription}"`);
60
+ // Use the timezone-adjusted date as the reference date for chrono-node
61
+ const parsed = parse(normalizedDescription, now);
62
+ if (!parsed || parsed.length === 0) {
63
+ throw new Error(`Could not parse time description: ${time_description}`);
64
+ }
65
+ logger.info(`TIME TOOLPARSED ${time_description}`, JSON.stringify(parsed));
66
+ const dateComponents = parsed[0].start;
67
+ if (!dateComponents) {
68
+ throw new Error("Parsed time has no start component");
69
+ }
70
+ const dt = DateTime.fromObject({
71
+ year: nullToUndefined(dateComponents.get("year")),
72
+ month: nullToUndefined(dateComponents.get("month")),
73
+ day: nullToUndefined(dateComponents.get("day")),
74
+ hour: nullToUndefined(dateComponents.get("hour")),
75
+ minute: nullToUndefined(dateComponents.get("minute")),
76
+ second: nullToUndefined(dateComponents.get("second")),
77
+ millisecond: 0,
78
+ }, {
79
+ zone: timezone || "local",
80
+ });
81
+ if (!dt.isValid) {
82
+ throw new Error(`Could not construct valid DateTime: ${dt.invalidReason}`);
83
+ }
84
+ const timestamp = dt.toMillis();
85
+ return {
86
+ timestamp,
87
+ iso: dt.toISO(),
88
+ timezone: dt.zoneName,
89
+ };
90
+ }
@@ -0,0 +1,148 @@
1
+ import { postApi } from "../network.js";
2
+ /**
3
+ * Updates camera configuration using the faceted config API
4
+ */
5
+ export async function updateCameraConfig(payload, requestModifiers, sessionId) {
6
+ try {
7
+ const result = await postApi({
8
+ route: "/camera/updateFacetedConfig",
9
+ body: payload,
10
+ modifiers: requestModifiers,
11
+ sessionId,
12
+ });
13
+ if (result.error) {
14
+ return {
15
+ success: false,
16
+ error: result.errorMsg || "Failed to update camera configuration",
17
+ };
18
+ }
19
+ return {
20
+ success: true,
21
+ updatedSettings: payload.configUpdate,
22
+ };
23
+ }
24
+ catch (error) {
25
+ return {
26
+ success: false,
27
+ error: error instanceof Error ? error.message : "Unknown error occurred",
28
+ };
29
+ }
30
+ }
31
+ /**
32
+ * Gets current camera configuration
33
+ */
34
+ export async function getCameraDetails(cameraUuid, requestModifiers, sessionId) {
35
+ try {
36
+ const result = await postApi({
37
+ route: "/camera/getDetailsV2",
38
+ body: {
39
+ uuid: cameraUuid,
40
+ },
41
+ modifiers: requestModifiers,
42
+ sessionId,
43
+ });
44
+ if (result.error) {
45
+ return {
46
+ success: false,
47
+ error: result.errorMsg || "Failed to get camera details",
48
+ };
49
+ }
50
+ return {
51
+ success: true,
52
+ data: result.camera,
53
+ };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ success: false,
58
+ error: error instanceof Error ? error.message : "Unknown error occurred",
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * Helper function to remove null/undefined fields from update payload
64
+ */
65
+ export function cleanUpdatePayload(payload) {
66
+ const cleaned = {};
67
+ for (const [key, value] of Object.entries(payload)) {
68
+ if (value !== null && value !== undefined) {
69
+ if (typeof value === "object" && !Array.isArray(value)) {
70
+ const cleanedValue = cleanUpdatePayload(value);
71
+ if (Object.keys(cleanedValue).length > 0) {
72
+ cleaned[key] = cleanedValue;
73
+ }
74
+ }
75
+ else {
76
+ cleaned[key] = value;
77
+ }
78
+ }
79
+ }
80
+ return cleaned;
81
+ }
82
+ /**
83
+ * Formats camera settings for display
84
+ */
85
+ export function formatCameraSettings(settings) {
86
+ const sections = [];
87
+ if (settings.videoFacetSettings) {
88
+ const videoSettings = Object.values(settings.videoFacetSettings)[0];
89
+ if (videoSettings) {
90
+ const videoItems = [];
91
+ if (videoSettings.resolution)
92
+ videoItems.push(`Resolution: ${videoSettings.resolution.width}x${videoSettings.resolution.height}`);
93
+ if (videoSettings.img_brightness !== undefined)
94
+ videoItems.push(`Brightness: ${videoSettings.img_brightness}`);
95
+ if (videoSettings.img_contrast !== undefined)
96
+ videoItems.push(`Contrast: ${videoSettings.img_contrast}`);
97
+ if (videoSettings.img_saturation !== undefined)
98
+ videoItems.push(`Saturation: ${videoSettings.img_saturation}`);
99
+ if (videoSettings.img_sharpness !== undefined)
100
+ videoItems.push(`Sharpness: ${videoSettings.img_sharpness}`);
101
+ if (videoSettings.hdr_enabled !== undefined)
102
+ videoItems.push(`HDR: ${videoSettings.hdr_enabled ? "On" : "Off"}`);
103
+ if (videoSettings.wdr_enabled !== undefined)
104
+ videoItems.push(`WDR: ${videoSettings.wdr_enabled ? "On" : "Off"}`);
105
+ if (videoSettings.wdr_strength !== undefined)
106
+ videoItems.push(`WDR Strength: ${videoSettings.wdr_strength}`);
107
+ if (videoSettings.video_persist_disabled !== undefined)
108
+ videoItems.push(`Video Persist: ${videoSettings.video_persist_disabled ? "Disabled" : "Enabled"}`);
109
+ if (videoSettings.zero_motion_video_bitrate_percent !== undefined)
110
+ videoItems.push(`Zero Motion Bitrate: ${videoSettings.zero_motion_video_bitrate_percent}%`);
111
+ if (videoItems.length > 0) {
112
+ sections.push("**Video Settings:**\n" + videoItems.map(item => `• ${item}`).join("\n"));
113
+ }
114
+ }
115
+ }
116
+ if (settings.audioFacetSettings) {
117
+ const audioSettings = Object.values(settings.audioFacetSettings)[0];
118
+ if (audioSettings) {
119
+ const audioItems = [];
120
+ if (audioSettings.audio_record !== undefined)
121
+ audioItems.push(`Recording: ${audioSettings.audio_record ? "On" : "Off"}`);
122
+ if (audioSettings.device_mic_enabled !== undefined)
123
+ audioItems.push(`Microphone: ${audioSettings.device_mic_enabled ? "Enabled" : "Disabled"}`);
124
+ if (audioSettings.device_speaker_enabled !== undefined)
125
+ audioItems.push(`Speaker: ${audioSettings.device_speaker_enabled ? "Enabled" : "Disabled"}`);
126
+ if (audioItems.length > 0) {
127
+ sections.push("**Audio Settings:**\n" + audioItems.map(item => `• ${item}`).join("\n"));
128
+ }
129
+ }
130
+ }
131
+ if (settings.deviceSettings) {
132
+ const deviceItems = [];
133
+ if (settings.deviceSettings.camera_name)
134
+ deviceItems.push(`Name: ${settings.deviceSettings.camera_name}`);
135
+ if (settings.deviceSettings.camera_timezone)
136
+ deviceItems.push(`Timezone: ${settings.deviceSettings.camera_timezone}`);
137
+ if (settings.deviceSettings.led_mode)
138
+ deviceItems.push(`LED Mode: ${settings.deviceSettings.led_mode}`);
139
+ if (settings.deviceSettings.led_intensity !== undefined)
140
+ deviceItems.push(`LED Intensity: ${settings.deviceSettings.led_intensity}`);
141
+ if (settings.deviceSettings.led_stealth_mode !== undefined)
142
+ deviceItems.push(`LED Stealth Mode: ${settings.deviceSettings.led_stealth_mode ? "On" : "Off"}`);
143
+ if (deviceItems.length > 0) {
144
+ sections.push("**Device Settings:**\n" + deviceItems.map(item => `• ${item}`).join("\n"));
145
+ }
146
+ }
147
+ return sections.join("\n\n");
148
+ }
@@ -36,7 +36,13 @@ export default async function createServer() {
36
36
  }
37
37
  logger.info(`🛠️ Registered ${resources.length} resources`);
38
38
  for (const tool of tools) {
39
- tool.create(server);
39
+ try {
40
+ await tool.create(server);
41
+ }
42
+ catch (error) {
43
+ logger.error(`Failed to register tool ${tool.name}:`, error);
44
+ // Continue with other tools instead of failing completely
45
+ }
40
46
  }
41
47
  logger.info(`🛠️ Registered ${tools.length} tools`);
42
48
  logger.info(`✅ Server created`);