rhombus-node-mcp 0.1.44 → 0.1.46
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/badge-correlation.js +33 -0
- package/dist/api/badge-timeline-tool-api.js +56 -0
- package/dist/api/camera-tool-api.js +42 -47
- package/dist/tools-console/badge-timeline-tool.js +50 -0
- package/dist/tools-console/camera-tool.js +14 -6
- package/dist/types/badge-timeline-tool-types.js +75 -0
- package/dist/types/camera-tool-types.js +34 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -1,70 +1,65 @@
|
|
|
1
1
|
import { getLogger } from "../logger.js";
|
|
2
|
-
import {
|
|
2
|
+
import { postApi } from "../network/network.js";
|
|
3
3
|
import { removeNullFields } from "../util.js";
|
|
4
4
|
const logger = getLogger("camera-tool");
|
|
5
5
|
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
6
|
-
|
|
6
|
+
/** Convert a 0-100 percentage to permyriad (0-10000), clamped to the valid range. */
|
|
7
|
+
function pctToPermyriad(pct) {
|
|
8
|
+
return Math.round(Math.max(0, Math.min(100, pct)) * 100);
|
|
9
|
+
}
|
|
10
|
+
export async function getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId, options) {
|
|
11
|
+
// biome-ignore lint/suspicious/noExplicitAny: request body is a loose JSON shape
|
|
7
12
|
const body = {
|
|
8
|
-
cameraUuid
|
|
9
|
-
|
|
13
|
+
cameraUuid,
|
|
14
|
+
timestampMs,
|
|
15
|
+
downscaleFactor: options?.downscaleFactor ?? 10,
|
|
10
16
|
jpgQuality: 70,
|
|
11
|
-
timestampMs: timestampMs,
|
|
12
17
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const crop = options?.crop;
|
|
19
|
+
const hasCrop = crop != null &&
|
|
20
|
+
(crop.x != null || crop.y != null || crop.width != null || crop.height != null);
|
|
21
|
+
const resolvedCrop = hasCrop
|
|
22
|
+
? {
|
|
23
|
+
x: crop.x ?? 0,
|
|
24
|
+
y: crop.y ?? 0,
|
|
25
|
+
width: crop.width ?? 100,
|
|
26
|
+
height: crop.height ?? 100,
|
|
27
|
+
}
|
|
28
|
+
: null;
|
|
29
|
+
if (resolvedCrop) {
|
|
30
|
+
body.permyriadCropX = pctToPermyriad(resolvedCrop.x);
|
|
31
|
+
body.permyriadCropY = pctToPermyriad(resolvedCrop.y);
|
|
32
|
+
body.permyriadCropWidth = pctToPermyriad(resolvedCrop.width);
|
|
33
|
+
body.permyriadCropHeight = pctToPermyriad(resolvedCrop.height);
|
|
34
|
+
}
|
|
35
|
+
logger.debug(`Getting exact frame data for UUID: ${cameraUuid} at timestampMs: ${timestampMs}` +
|
|
36
|
+
(hasCrop ? ` (cropped)` : ``));
|
|
37
|
+
const res = await postApi({
|
|
38
|
+
route: "/video/getExactFrameData",
|
|
17
39
|
body,
|
|
18
40
|
modifiers: requestModifiers,
|
|
19
41
|
sessionId,
|
|
20
|
-
}).then(async (res) => {
|
|
21
|
-
logger.debug(`Received frameUri ${res.frameUri}`);
|
|
22
|
-
if (!res.frameUri) {
|
|
23
|
-
throw new Error("No frameUri given. Maybe camera does not support it.");
|
|
24
|
-
}
|
|
25
|
-
// construct request headers
|
|
26
|
-
const { url: _frameUri, requestHeaders } = constructRequestHeaders(res.frameUri, requestModifiers, sessionId);
|
|
27
|
-
frameUri = _frameUri;
|
|
28
|
-
// remove content type
|
|
29
|
-
delete requestHeaders["Content-Type"];
|
|
30
|
-
delete requestHeaders["accept"];
|
|
31
|
-
logger.debug(`Fetching with headers\n${JSON.stringify(requestHeaders, null, 2)}`);
|
|
32
|
-
return await fetch(frameUri, {
|
|
33
|
-
method: "GET",
|
|
34
|
-
headers: requestHeaders,
|
|
35
|
-
}).then(async (res) => {
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
logger.error(`Failed to fetch image (HTTP ${res.status}): ${await res.text()}`);
|
|
38
|
-
logger.error(res);
|
|
39
|
-
return res.status === 404 ? 404 : null;
|
|
40
|
-
}
|
|
41
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
42
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
43
|
-
const base64 = buffer.toString("base64");
|
|
44
|
-
logger.debug(`Received image base64:\n ${base64}`);
|
|
45
|
-
return base64;
|
|
46
|
-
});
|
|
47
42
|
});
|
|
48
|
-
if (!
|
|
49
|
-
|
|
50
|
-
success: false,
|
|
51
|
-
status: "failed to fetch image",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (base64Image === 404) {
|
|
43
|
+
if (res.error || !res.frameData) {
|
|
44
|
+
logger.error(`getExactFrameData failed: ${JSON.stringify({ error: res.error, errorMsg: res.errorMsg, status: res.status, hasFrameData: !!res.frameData })}`);
|
|
55
45
|
return {
|
|
56
46
|
success: false,
|
|
57
47
|
status: "failed to fetch image",
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
// Prefer a structured business error from the endpoint, then any transport-level
|
|
49
|
+
// status set by postApi (e.g. permission/HTTP errors), and only fall back to the
|
|
50
|
+
// no-VOD explanation when no error detail is available.
|
|
51
|
+
message: res.errorMsg ??
|
|
52
|
+
res.status ??
|
|
53
|
+
"Camera snapshot unavailable: this camera may have no recorded video (VOD) at the requested time. " +
|
|
54
|
+
"It may not have any footage stored, or the requested timestamp may be outside its retention window.",
|
|
60
55
|
};
|
|
61
56
|
}
|
|
62
57
|
return {
|
|
63
58
|
success: true,
|
|
64
59
|
status: "successfully fetched image",
|
|
65
|
-
frameUri: frameUri ?? "",
|
|
66
60
|
imageType: "base64",
|
|
67
|
-
imageData:
|
|
61
|
+
imageData: res.frameData,
|
|
62
|
+
crop: resolvedCrop,
|
|
68
63
|
};
|
|
69
64
|
}
|
|
70
65
|
async function getCameraStorageData(cameraUuid, requestModifiers, sessionId) {
|
|
@@ -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
|
+
}
|
|
@@ -21,8 +21,8 @@ This tool captures and returns a real-time snapshot from a designated security c
|
|
|
21
21
|
The image reflects the current scene in the camera's field of view and serves as a contextual
|
|
22
22
|
input source for downstream tasks such as object recognition, anomaly detection, incident investigation,
|
|
23
23
|
or situational assessment. When invoked, the tool provides the following:
|
|
24
|
-
- Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
|
|
25
|
-
-
|
|
24
|
+
- Visual Scene Capture: A high-resolution image of what the camera is actively observing, including people, vehicles, license plates, and any detectable objects.
|
|
25
|
+
- Optional zoom: pass cropX, cropY, cropWidth, cropHeight (each a percentage 0-100, origin at the top-left) to return only a sub-region of the frame so you can inspect a detail (e.g. a license plate or a doorway) more closely. Omit them for the full frame. When zooming into a small crop, pass a smaller downscaleFactor (e.g. 1-3) to preserve detail.
|
|
26
26
|
|
|
27
27
|
What follows is a description of the behavior of this tool given the requestType "get-settings"
|
|
28
28
|
|
|
@@ -56,7 +56,7 @@ Examples that REQUIRE the automatic snapshot flow:
|
|
|
56
56
|
const logger = getLogger("camera-tool");
|
|
57
57
|
const TOOL_ARGS = BASE_TOOL_ARGS;
|
|
58
58
|
const TOOL_HANDLER = async (args, extra) => {
|
|
59
|
-
const { cameraUuid, timestampISO, requestType } = args;
|
|
59
|
+
const { cameraUuid, timestampISO, requestType, cropX, cropY, cropWidth, cropHeight, downscaleFactor } = args;
|
|
60
60
|
if (!cameraUuid) {
|
|
61
61
|
return {
|
|
62
62
|
content: [
|
|
@@ -76,13 +76,21 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
76
76
|
const { requestModifiers, sessionId } = extractFromToolExtra(extra);
|
|
77
77
|
switch (requestType) {
|
|
78
78
|
case "image":
|
|
79
|
-
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId
|
|
79
|
+
response = await getImageForCameraAtTime(cameraUuid, timestampMs, requestModifiers, sessionId, {
|
|
80
|
+
crop: {
|
|
81
|
+
x: cropX ?? null,
|
|
82
|
+
y: cropY ?? null,
|
|
83
|
+
width: cropWidth ?? null,
|
|
84
|
+
height: cropHeight ?? null,
|
|
85
|
+
},
|
|
86
|
+
downscaleFactor: downscaleFactor ?? null,
|
|
87
|
+
});
|
|
80
88
|
if (!response.success || !response.imageData) {
|
|
81
89
|
return {
|
|
82
90
|
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
83
91
|
};
|
|
84
92
|
}
|
|
85
|
-
logger.debug(`Received image response
|
|
93
|
+
logger.debug(`Received image response (base64 length ${response.imageData.length})`);
|
|
86
94
|
return {
|
|
87
95
|
content: [
|
|
88
96
|
{
|
|
@@ -97,7 +105,7 @@ const TOOL_HANDLER = async (args, extra) => {
|
|
|
97
105
|
status: "image-attached",
|
|
98
106
|
cameraUuid,
|
|
99
107
|
timestampMs,
|
|
100
|
-
|
|
108
|
+
cropApplied: response.crop ?? null,
|
|
101
109
|
}),
|
|
102
110
|
},
|
|
103
111
|
],
|
|
@@ -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
|
+
});
|
|
@@ -221,5 +221,39 @@ export const BASE_TOOL_ARGS = {
|
|
|
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
223
|
requestType: z.enum(["image", "get-settings", "get-media-uris", "get-ai-thresholds"]),
|
|
224
|
+
cropX: z
|
|
225
|
+
.number()
|
|
226
|
+
.min(0)
|
|
227
|
+
.max(100)
|
|
228
|
+
.nullable()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe("For requestType 'image' only. Left edge of the crop region as a percentage (0-100) of image width, measured from the left. 0 = left edge. Omit for full frame."),
|
|
231
|
+
cropY: z
|
|
232
|
+
.number()
|
|
233
|
+
.min(0)
|
|
234
|
+
.max(100)
|
|
235
|
+
.nullable()
|
|
236
|
+
.optional()
|
|
237
|
+
.describe("For requestType 'image' only. Top edge of the crop region as a percentage (0-100) of image height, measured from the top. 0 = top edge. Omit for full frame."),
|
|
238
|
+
cropWidth: z
|
|
239
|
+
.number()
|
|
240
|
+
.min(0)
|
|
241
|
+
.max(100)
|
|
242
|
+
.nullable()
|
|
243
|
+
.optional()
|
|
244
|
+
.describe("For requestType 'image' only. Width of the crop region as a percentage (0-100) of image width. Omit for full frame (100)."),
|
|
245
|
+
cropHeight: z
|
|
246
|
+
.number()
|
|
247
|
+
.min(0)
|
|
248
|
+
.max(100)
|
|
249
|
+
.nullable()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("For requestType 'image' only. Height of the crop region as a percentage (0-100) of image height. Omit for full frame (100)."),
|
|
252
|
+
downscaleFactor: z
|
|
253
|
+
.number()
|
|
254
|
+
.min(1)
|
|
255
|
+
.nullable()
|
|
256
|
+
.optional()
|
|
257
|
+
.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."),
|
|
224
258
|
};
|
|
225
259
|
const BASE_TOOL_ARGS_SCHEMA = z.object(BASE_TOOL_ARGS);
|