rhombus-node-mcp 0.1.44 → 0.1.45

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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Keystone correlation helper for the OnGuard feature set.
3
+ *
4
+ * Given a badge-event-like entry (a camera `deviceUuid` + a `timestampMs`), produce the media hints
5
+ * the agent needs to *show* the moment: a still (camera-tool `image`) and a short clip window
6
+ * (clips-tool `createClip`). Pure and reusable — the follow-the-badge timeline, anomaly review, and
7
+ * lost-badge tracking all turn badge events into "here's the picture/video" the same way.
8
+ *
9
+ * Kept deliberately small and side-effect-free. Richer correlation (co-located face events,
10
+ * people-count in the window) layers on top of this when those features land.
11
+ */
12
+ export const DEFAULT_CLIP_PADDING_SECONDS = 15;
13
+ /**
14
+ * Build still + clip hints for a single badge event. Returns empty hints (no clip/still) when the
15
+ * event lacks a camera or timestamp, so callers can render those events without media gracefully.
16
+ */
17
+ export function buildMediaHints(event, clipPaddingSeconds = DEFAULT_CLIP_PADDING_SECONDS) {
18
+ if (!event.deviceUuid || event.timestampMs == null) {
19
+ return {};
20
+ }
21
+ const padMs = Math.max(0, clipPaddingSeconds) * 1000;
22
+ return {
23
+ clipHint: {
24
+ deviceUuid: event.deviceUuid,
25
+ startTimeMs: event.timestampMs - padMs,
26
+ endTimeMs: event.timestampMs + padMs,
27
+ },
28
+ stillHint: {
29
+ deviceUuid: event.deviceUuid,
30
+ timestampMs: event.timestampMs,
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,56 @@
1
+ import { buildMediaHints } from "./badge-correlation.js";
2
+ import { searchOnGuardEvents } from "./onguard-tool-api.js";
3
+ /**
4
+ * Reconstructs a single cardholder's movements as a chronological timeline of OnGuard badge taps,
5
+ * each with the still/clip hints needed to show what happened. Pure orchestration over
6
+ * searchOnGuardEvents + the shared correlation helper.
7
+ */
8
+ export async function getBadgeTimeline(args, timeZone, requestModifiers, sessionId) {
9
+ const { events } = await searchOnGuardEvents({
10
+ cardholderQuery: args.cardholderQuery,
11
+ locationUuids: args.locationUuids,
12
+ afterMs: args.afterMs,
13
+ beforeMs: args.beforeMs,
14
+ limit: args.limit ?? 200,
15
+ }, timeZone, requestModifiers, sessionId);
16
+ // searchOnGuardEvents returns newest-first; a timeline reads chronologically.
17
+ const ordered = events
18
+ .filter((e) => e.timestampMs != null)
19
+ .sort((a, b) => (a.timestampMs ?? 0) - (b.timestampMs ?? 0));
20
+ const stops = ordered.map((e, i) => {
21
+ const next = ordered[i + 1];
22
+ const gapToNextSeconds = next?.timestampMs != null && e.timestampMs != null
23
+ ? Math.round((next.timestampMs - e.timestampMs) / 1000)
24
+ : undefined;
25
+ const { clipHint, stillHint } = buildMediaHints({ deviceUuid: e.deviceUuid, timestampMs: e.timestampMs }, args.clipPaddingSeconds);
26
+ return {
27
+ timestampMs: e.timestampMs,
28
+ datetime: e.datetime,
29
+ deviceUuid: e.deviceUuid,
30
+ area: e.areaEntering ?? e.areaExiting,
31
+ label: e.label,
32
+ isAnomaly: e.isAnomaly,
33
+ clipHint,
34
+ stillHint,
35
+ gapToNextSeconds,
36
+ };
37
+ });
38
+ // The sequence of areas traversed, collapsing consecutive repeats.
39
+ const path = [];
40
+ for (const stop of stops) {
41
+ if (stop.area && stop.area !== path[path.length - 1]) {
42
+ path.push(stop.area);
43
+ }
44
+ }
45
+ // cardholderQuery is full-text, so it can match more than one person — surface that so the agent
46
+ // can disambiguate rather than silently merge two people's movements.
47
+ const distinctCardholders = [
48
+ ...new Set(ordered.map((e) => e.cardholderName).filter((n) => !!n)),
49
+ ];
50
+ return {
51
+ cardholderName: distinctCardholders[0],
52
+ ambiguousCardholders: distinctCardholders.length > 1 ? distinctCardholders : undefined,
53
+ stops,
54
+ path,
55
+ };
56
+ }
@@ -0,0 +1,50 @@
1
+ import { getBadgeTimeline } from "../api/badge-timeline-tool-api.js";
2
+ import { OUTPUT_SCHEMA, TOOL_ARGS } from "../types/badge-timeline-tool-types.js";
3
+ import { createToolStructuredContent, extractFromToolExtra } from "../util.js";
4
+ const TOOL_NAME = "badge-timeline-tool";
5
+ const TOOL_DESCRIPTION = `
6
+ Reconstructs one person's movements through a building from their Honeywell OnGuard (Lenel) badge taps.
7
+ Use this for incident reconstruction / "follow the badge" requests, e.g. "reconstruct Eve's movements
8
+ yesterday" or "where did this cardholder go".
9
+
10
+ Returns the cardholder's badge taps in CHRONOLOGICAL order (oldest first), each with:
11
+ - datetime / timestampMs and the area entered
12
+ - deviceUuid: the camera at that door
13
+ - clipHint (camera + start/end window) and stillHint (camera + timestamp)
14
+ - gapToNextSeconds: time until the next tap (a large gap = unobserved movement between doors)
15
+ plus a "path" array summarizing the areas traversed in order.
16
+
17
+ Resolve relative times like "yesterday" to ISO 8601 first (use the timestamp tool), then pass
18
+ startTime/endTime. cardholderQuery is a full-text name match; if "ambiguousCardholders" is returned the
19
+ query matched more than one person — ask the user which one before trusting the timeline.
20
+
21
+ IMPORTANT — to show the movement visually: for each stop (or the key transitions), call the camera-tool
22
+ (requestType "image", cameraUuid = stop.deviceUuid, timestamp = stop.timestampMs) for a still you can see,
23
+ and/or the clips-tool (requestType "createClip", using stop.clipHint) for video. Issue those per-stop
24
+ media calls in PARALLEL, then present the timeline as a chronological narrative.
25
+ `;
26
+ const TOOL_HANDLER = async (args, _extra) => {
27
+ const { requestModifiers, sessionId } = extractFromToolExtra(_extra);
28
+ try {
29
+ const result = await getBadgeTimeline({
30
+ cardholderQuery: args.cardholderQuery,
31
+ locationUuids: args.locationUuids ?? undefined,
32
+ afterMs: args.startTime ? new Date(args.startTime).getTime() : undefined,
33
+ beforeMs: args.endTime ? new Date(args.endTime).getTime() : undefined,
34
+ clipPaddingSeconds: args.clipPaddingSeconds ?? undefined,
35
+ limit: args.limit ?? undefined,
36
+ }, args.timeZone ?? "UTC", requestModifiers, sessionId);
37
+ return createToolStructuredContent(result);
38
+ }
39
+ catch (error) {
40
+ const message = error instanceof Error ? error.message : "Unknown error";
41
+ return createToolStructuredContent({ error: message });
42
+ }
43
+ };
44
+ export function createTool(server) {
45
+ server.registerTool(TOOL_NAME, {
46
+ description: TOOL_DESCRIPTION,
47
+ inputSchema: TOOL_ARGS,
48
+ outputSchema: OUTPUT_SCHEMA.shape,
49
+ }, TOOL_HANDLER);
50
+ }
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { createUuidSchema } from "../types.js";
3
+ import { ISOTimestampFormatDescription } from "../utils/timestampInput.js";
4
+ export const TOOL_ARGS = {
5
+ cardholderQuery: z
6
+ .string()
7
+ .describe('The cardholder (person) to reconstruct, full-text name match, e.g. "Eve" or "Eve Adams".'),
8
+ locationUuids: z
9
+ .array(createUuidSchema())
10
+ .nullable()
11
+ .describe("Optional: restrict to these Rhombus location UUIDs. Use the location-tool to resolve names."),
12
+ startTime: z
13
+ .string()
14
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
15
+ .nullable()
16
+ .describe("Start of the window (inclusive). " + ISOTimestampFormatDescription),
17
+ endTime: z
18
+ .string()
19
+ .datetime({ message: "Invalid datetime string. Expected ISO 8601 format.", offset: true })
20
+ .nullable()
21
+ .describe("End of the window (inclusive). " + ISOTimestampFormatDescription),
22
+ clipPaddingSeconds: z
23
+ .number()
24
+ .nullable()
25
+ .describe("Seconds of video before/after each badge tap to include in the clip hint (default 15)."),
26
+ limit: z.number().nullable().describe("Maximum badge taps to include (default 200)."),
27
+ timeZone: z
28
+ .string()
29
+ .nullable()
30
+ .describe("IANA timezone used to format times, e.g. America/New_York. Defaults to UTC."),
31
+ };
32
+ const TOOL_ARGS_SCHEMA = z.object(TOOL_ARGS);
33
+ const ClipHintSchema = z
34
+ .object({
35
+ deviceUuid: z.string(),
36
+ startTimeMs: z.number(),
37
+ endTimeMs: z.number(),
38
+ })
39
+ .describe("Pass to clips-tool createClip to get video of this tap.");
40
+ const StillHintSchema = z
41
+ .object({
42
+ deviceUuid: z.string(),
43
+ timestampMs: z.number(),
44
+ })
45
+ .describe("Pass to camera-tool (requestType image) to get a still of this tap.");
46
+ export const BadgeTimelineStopSchema = z.object({
47
+ timestampMs: z.number().optional(),
48
+ datetime: z.string().optional().describe("Human-readable tap time in the requested timezone."),
49
+ deviceUuid: z
50
+ .string()
51
+ .optional()
52
+ .describe("The camera at this door. Pass to camera-tool (image) or clips-tool (createClip)."),
53
+ area: z.string().optional().describe("The area entered (or exited) at this tap."),
54
+ label: z.string().optional(),
55
+ isAnomaly: z.boolean().optional().describe("True if this tap was an alerting/anomalous event."),
56
+ clipHint: ClipHintSchema.optional(),
57
+ stillHint: StillHintSchema.optional(),
58
+ gapToNextSeconds: z
59
+ .number()
60
+ .optional()
61
+ .describe("Seconds until the next tap — large gaps are unobserved movement between doors."),
62
+ });
63
+ export const OUTPUT_SCHEMA = z.object({
64
+ cardholderName: z.string().optional().describe("The resolved cardholder name."),
65
+ ambiguousCardholders: z
66
+ .array(z.string())
67
+ .optional()
68
+ .describe("Set when the query matched more than one person — disambiguate with the user before trusting the timeline."),
69
+ stops: z
70
+ .array(BadgeTimelineStopSchema)
71
+ .optional()
72
+ .describe("The cardholder's badge taps in chronological order (oldest first)."),
73
+ path: z.array(z.string()).optional().describe("Areas traversed, in order (consecutive repeats collapsed)."),
74
+ error: z.string().optional(),
75
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhombus-node-mcp",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "description": "MCP server for Rhombus API",
5
5
  "keywords": [
6
6
  "ai",