crow-central-agency 0.26.8 → 0.26.9

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 (50) hide show
  1. package/dist/mcp/agents/search-workspace.js +3 -3
  2. package/dist/mcp/artifacts/list-artifacts.js +3 -3
  3. package/dist/mcp/artifacts/list-circle-artifacts.js +3 -3
  4. package/dist/mcp/claude-mcp-adapter.js +30 -2
  5. package/dist/mcp/copilot-mcp-adapter.js +10 -2
  6. package/dist/mcp/feed/get-feed-items.js +3 -3
  7. package/dist/mcp/feed/get-recent-feed-items.js +3 -3
  8. package/dist/mcp/feed/list-feeds.js +3 -3
  9. package/dist/mcp/feed/search-feed-items.js +3 -3
  10. package/dist/mcp/places/geocode-place.js +10 -1
  11. package/dist/mcp/places/places-format-utils.js +24 -0
  12. package/dist/mcp/places/places-routing-input.js +49 -0
  13. package/dist/mcp/places/search-nearby-places.js +12 -1
  14. package/dist/mcp/reminders/list-reminders.js +3 -3
  15. package/dist/mcp/tool-utils.js +27 -10
  16. package/dist/public/assets/{architectureDiagram-3BPJPVTR-DDgVnuoo.js → architectureDiagram-3BPJPVTR-DeXG4IF1.js} +1 -1
  17. package/dist/public/assets/{chunk-727SXJPM-PDiAD8Du.js → chunk-727SXJPM-ClhCvSxl.js} +1 -1
  18. package/dist/public/assets/{chunk-AQP2D5EJ-wy5_L253.js → chunk-AQP2D5EJ-S71PlnyF.js} +1 -1
  19. package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-BYJ5bFf6.js → classDiagram-4FO5ZUOK-t2EdfGSJ.js} +1 -1
  20. package/dist/public/assets/{classDiagram-4FO5ZUOK-BYJ5bFf6.js → classDiagram-v2-Q7XG4LA2-t2EdfGSJ.js} +1 -1
  21. package/dist/public/assets/{dagre-BM42HDAG-CjoXczSR.js → dagre-BM42HDAG-BUN2v9KO.js} +1 -1
  22. package/dist/public/assets/{diagram-2AECGRRQ-CCKmxsdo.js → diagram-2AECGRRQ-BsEdgIXV.js} +1 -1
  23. package/dist/public/assets/{diagram-5GNKFQAL-yKZCpt4W.js → diagram-5GNKFQAL-DkDNM-w8.js} +1 -1
  24. package/dist/public/assets/{diagram-LMA3HP47-DneZvlxQ.js → diagram-LMA3HP47-bemQvtc4.js} +1 -1
  25. package/dist/public/assets/{diagram-OG6HWLK6-Ud_EdkZh.js → diagram-OG6HWLK6-DTt1CSKj.js} +1 -1
  26. package/dist/public/assets/{erDiagram-TEJ5UH35-B-vZXvBd.js → erDiagram-TEJ5UH35-Ctez1ZUQ.js} +1 -1
  27. package/dist/public/assets/{flowDiagram-I6XJVG4X-rVn14Wbt.js → flowDiagram-I6XJVG4X-6cl55wrS.js} +1 -1
  28. package/dist/public/assets/{index-BTKdu_X_.js → index-ISMoAJEC.js} +4 -4
  29. package/dist/public/assets/{infoDiagram-5YYISTIA-BbX08lDC.js → infoDiagram-5YYISTIA-DJG5GveT.js} +1 -1
  30. package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-CYs43POM.js → ishikawaDiagram-YF4QCWOH-BiRWZQoC.js} +1 -1
  31. package/dist/public/assets/{kanban-definition-UN3LZRKU--Cb13wva.js → kanban-definition-UN3LZRKU--LGs8j1A.js} +1 -1
  32. package/dist/public/assets/{mindmap-definition-RKZ34NQL-CQJGU9F-.js → mindmap-definition-RKZ34NQL-CBTeUNeJ.js} +1 -1
  33. package/dist/public/assets/{pieDiagram-4H26LBE5-DMNxEuMG.js → pieDiagram-4H26LBE5-Bj8lCWKM.js} +1 -1
  34. package/dist/public/assets/{requirementDiagram-4Y6WPE33-DVzHOXTK.js → requirementDiagram-4Y6WPE33-DNbmiPTP.js} +1 -1
  35. package/dist/public/assets/{sequenceDiagram-3UESZ5HK-CrIiBex5.js → sequenceDiagram-3UESZ5HK-DOZcdL-V.js} +1 -1
  36. package/dist/public/assets/{stateDiagram-AJRCARHV-LVBo72aS.js → stateDiagram-AJRCARHV-BCpPGAsz.js} +1 -1
  37. package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-pc1U14yi.js → stateDiagram-v2-BHNVJYJU-hUzXDK6m.js} +1 -1
  38. package/dist/public/assets/{timeline-definition-PNZ67QCA-COcJo8Ga.js → timeline-definition-PNZ67QCA-BsNHIWNc.js} +1 -1
  39. package/dist/public/assets/{vennDiagram-CIIHVFJN-qOZm-QC4.js → vennDiagram-CIIHVFJN-DCZMeVJ8.js} +1 -1
  40. package/dist/public/assets/{wardleyDiagram-YWT4CUSO-idQAwlbA.js → wardleyDiagram-YWT4CUSO-B15lhDxS.js} +1 -1
  41. package/dist/public/assets/{xychartDiagram-2RQKCTM6-Caywd0sW.js → xychartDiagram-2RQKCTM6-u23J3wWJ.js} +1 -1
  42. package/dist/public/index.html +1 -1
  43. package/dist/routes/agent.routes.js +3 -0
  44. package/dist/routines/gmail-notification-routine.js +6 -1
  45. package/dist/services/places/google/google-place-mapper.js +31 -0
  46. package/dist/services/places/google/google-places-adapter.js +33 -9
  47. package/dist/services/places/google/google-places-adapter.types.js +11 -0
  48. package/dist/services/places/google/google-places-client.js +29 -4
  49. package/dist/services/places/places-manager.types.js +10 -0
  50. package/package.json +1 -1
@@ -21,16 +21,16 @@ export function getSearchWorkspaceToolConfig(agentId, documentSearchService, tas
21
21
  .number()
22
22
  .optional()
23
23
  .describe(`Maximum number of hits to return per page. Default ${DEFAULT_SEARCH_LIMIT}.`),
24
- skip: z.number().optional().describe("Number of hits to skip for pagination."),
24
+ offset: z.number().optional().describe("Number of hits to skip before the page starts (0-based offset)."),
25
25
  };
26
- const handler = async ({ query, sources, limit, skip }) => {
26
+ const handler = async ({ query, sources, limit, offset }) => {
27
27
  try {
28
28
  const filter = buildAccessFilter(agentId, taskManager, circleManager, sources);
29
29
  const hits = documentSearchService.search(query, { filter, limit: MAX_SEARCH_LIMIT });
30
30
  if (hits.length === 0) {
31
31
  return textToolResult([`No matches found for query "${query}".`]);
32
32
  }
33
- const pagination = applyPagination(hits, limit || DEFAULT_SEARCH_LIMIT, skip);
33
+ const pagination = applyPagination(hits, limit || DEFAULT_SEARCH_LIMIT, offset);
34
34
  const header = formatPaginationHeader(`Workspace search for query "${query}"`, pagination);
35
35
  return textToolResult(header.concat("", renderHits(pagination.items)));
36
36
  }
@@ -16,9 +16,9 @@ export function getListArtifactsToolConfig(agentId, artifactManager, registry, c
16
16
  .optional()
17
17
  .describe("Filter by tags. Only artifacts that have every specified tag are returned."),
18
18
  limit: z.number().optional().describe("Number of artifacts to return per page."),
19
- skip: z.number().optional().describe("Number of artifacts to skip for pagination."),
19
+ offset: z.number().optional().describe("Number of artifacts to skip before the page starts (0-based offset)."),
20
20
  };
21
- const handler = async ({ agent_id, type, tags, limit, skip }) => {
21
+ const handler = async ({ agent_id, type, tags, limit, offset }) => {
22
22
  const targetId = agent_id ?? agentId;
23
23
  if (agent_id) {
24
24
  try {
@@ -47,7 +47,7 @@ export function getListArtifactsToolConfig(agentId, artifactManager, registry, c
47
47
  const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
48
48
  return textToolResult([`No artifacts found for agent ${targetId}${suffix}.`]);
49
49
  }
50
- const pagination = applyPagination(artifacts, limit || DEFAULT_ARTIFACTS_LIMIT, skip);
50
+ const pagination = applyPagination(artifacts, limit || DEFAULT_ARTIFACTS_LIMIT, offset);
51
51
  const lines = pagination.items.map((artifact) => `- ${artifact.filename} (owner: ${artifact.entityId}, type: ${artifact.type}, modified: ${formatLocalDateTime(new Date(artifact.updatedTimestamp), userTimezone)})`);
52
52
  const header = formatPaginationHeader(`Artifacts for agent ${targetId}`, pagination);
53
53
  return textToolResult(header.concat("", lines));
@@ -16,9 +16,9 @@ export function getListCircleArtifactsToolConfig(agentId, artifactManager, senso
16
16
  .optional()
17
17
  .describe("Filter by tags. Only artifacts that have every specified tag are returned."),
18
18
  limit: z.number().optional().describe("Number of artifacts to return per page."),
19
- skip: z.number().optional().describe("Number of artifacts to skip for pagination."),
19
+ offset: z.number().optional().describe("Number of artifacts to skip before the page starts (0-based offset)."),
20
20
  };
21
- const handler = async ({ circle_id, type, tags, limit, skip }) => {
21
+ const handler = async ({ circle_id, type, tags, limit, offset }) => {
22
22
  if (!artifactManager.isDirectCircleMember(circle_id, agentId)) {
23
23
  return textToolResult(["You are not a direct member of this circle."], true);
24
24
  }
@@ -38,7 +38,7 @@ export function getListCircleArtifactsToolConfig(agentId, artifactManager, senso
38
38
  const suffix = filterParts.length ? ` matching ${filterParts.join(" and ")}` : "";
39
39
  return textToolResult([`No artifacts found for circle ${circle_id}${suffix}.`]);
40
40
  }
41
- const pagination = applyPagination(artifacts, limit || DEFAULT_CIRCLE_ARTIFACTS_LIMIT, skip);
41
+ const pagination = applyPagination(artifacts, limit || DEFAULT_CIRCLE_ARTIFACTS_LIMIT, offset);
42
42
  const lines = pagination.items.map((artifact) => `- ${artifact.filename} (type: ${artifact.type}, modified: ${formatLocalDateTime(new Date(artifact.updatedTimestamp), userTimezone)})`);
43
43
  const header = formatPaginationHeader(`Artifacts for circle ${circle_id}`, pagination);
44
44
  return textToolResult(header.concat("", lines));
@@ -1,13 +1,41 @@
1
1
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { MCP_CONFIG_TYPE } from "../_shared/index.js";
3
- /** Wrap a server's registered tools into an in-process Claude SDK MCP server. */
3
+ import { buildStrictToolSchema } from "./tool-utils.js";
4
+ const hasRegisteredTools = (instance) => {
5
+ if (!("_registeredTools" in instance)) {
6
+ return false;
7
+ }
8
+ const registeredTools = instance._registeredTools;
9
+ return typeof registeredTools === "object" && registeredTools !== null;
10
+ };
11
+ /**
12
+ * Swap each registered tool's input schema for a strict variant so unknown parameters are rejected instead of
13
+ * silently dropped. The raw shape is passed to tool() first so the SDK caches per-field descriptions; the strict
14
+ * object reuses the same field instances, so those descriptions survive the swap (a strict object passed directly
15
+ * to tool() would lose them). Guarded so an SDK internals change degrades to the default lenient
16
+ * behavior rather than throwing.
17
+ */
18
+ function applyStrictToolSchemas(instance, tools) {
19
+ if (!hasRegisteredTools(instance)) {
20
+ return;
21
+ }
22
+ for (const registeredTool of tools) {
23
+ const entry = instance._registeredTools[registeredTool.name];
24
+ if (entry) {
25
+ entry.inputSchema = buildStrictToolSchema(registeredTool.inputSchema);
26
+ }
27
+ }
28
+ }
29
+ /** Wrap a server's registered tools into an in-process Claude SDK MCP server with strict parameter validation. */
4
30
  export function toClaudeServer(name, tools) {
5
- return createSdkMcpServer({
31
+ const server = createSdkMcpServer({
6
32
  name,
7
33
  tools: tools.map((registeredTool) => tool(registeredTool.name, registeredTool.description, registeredTool.inputSchema, registeredTool.handler, {
8
34
  annotations: registeredTool.annotations,
9
35
  })),
10
36
  });
37
+ applyStrictToolSchemas(server.instance, tools);
38
+ return server;
11
39
  }
12
40
  /** Map a Crow MCP transport to the Claude SDK's stdio/http/sse server config. */
13
41
  export function toClaudeTransport(transport) {
@@ -1,16 +1,24 @@
1
1
  import { convertMcpCallToolResult } from "@github/copilot-sdk";
2
2
  import { z } from "zod";
3
3
  import { MCP_CONFIG_TYPE } from "../_shared/index.js";
4
+ import { buildStrictToolSchema, getValidationErrorToolResult } from "./tool-utils.js";
4
5
  /** Flatten an internal server's tools into Copilot in-process tools, prefixed to keep `mcp__server__tool` names. */
5
6
  export function toCopilotTools(server) {
6
7
  return server.tools.map((registeredTool) => toCopilotTool(registeredTool, server));
7
8
  }
8
9
  function toCopilotTool(registeredTool, server) {
10
+ const inputSchema = buildStrictToolSchema(registeredTool.inputSchema);
9
11
  return {
10
12
  name: `${server.mcpToolPrefix}${registeredTool.name}`,
11
13
  description: registeredTool.description,
12
- parameters: z.toJSONSchema(z.object(registeredTool.inputSchema)),
13
- handler: async (args, invocation) => toCopilotToolResult(await registeredTool.handler(args, invocation)),
14
+ parameters: z.toJSONSchema(inputSchema),
15
+ handler: async (args, invocation) => {
16
+ const parsed = inputSchema.safeParse(args);
17
+ if (!parsed.success) {
18
+ return toCopilotToolResult(getValidationErrorToolResult(registeredTool.name, parsed.error));
19
+ }
20
+ return toCopilotToolResult(await registeredTool.handler(parsed.data, invocation));
21
+ },
14
22
  skipPermission: server.isAutoApproved,
15
23
  };
16
24
  }
@@ -8,9 +8,9 @@ export function getFeedItemsToolConfig(agentId, registry, feedManager, sensorMan
8
8
  const inputSchema = {
9
9
  feedId: z.string().describe("The news/RSS feed ID from which to get items."),
10
10
  limit: z.number().optional().describe(`Number of items to return per page.`),
11
- skip: z.number().optional().describe("Number of items to skip for pagination."),
11
+ offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
12
12
  };
13
- const handler = async ({ feedId, limit, skip }) => {
13
+ const handler = async ({ feedId, limit, offset }) => {
14
14
  try {
15
15
  const visibleFeedIds = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
16
16
  if (!visibleFeedIds.has(feedId)) {
@@ -22,7 +22,7 @@ export function getFeedItemsToolConfig(agentId, registry, feedManager, sensorMan
22
22
  }
23
23
  const timezone = await sensorManager.getUserTimezone();
24
24
  const allItems = await feedManager.getItemsFromFeed(feedId);
25
- const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, skip);
25
+ const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
26
26
  const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feed.title, timezone));
27
27
  const header = formatPaginationHeader(`Items from feed [${feed.title}]`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
28
28
  return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
@@ -13,9 +13,9 @@ export function getRecentFeedItemsToolConfig(agentId, registry, feedManager, sen
13
13
  .optional()
14
14
  .describe(`Number of minutes to look back from now for recent items (default: ${DEFAULT_RECENCY_IN_MINUTES}).`),
15
15
  limit: z.number().optional().describe(`Number of items to return per page.`),
16
- skip: z.number().optional().describe("Number of items to skip for pagination."),
16
+ offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
17
17
  };
18
- const handler = async ({ recencyInMinutes, limit, skip }) => {
18
+ const handler = async ({ recencyInMinutes, limit, offset }) => {
19
19
  try {
20
20
  const timezone = await sensorManager.getUserTimezone();
21
21
  const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
@@ -27,7 +27,7 @@ export function getRecentFeedItemsToolConfig(agentId, registry, feedManager, sen
27
27
  if (!allItems.length) {
28
28
  return textToolResult([`No items found in the last ${effectiveRecency} minutes. Try increase the minutes.`]);
29
29
  }
30
- const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, skip);
30
+ const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
31
31
  const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feedsMap.get(item.feedId), timezone));
32
32
  const header = formatPaginationHeader(`Recent items from all feeds (last ${effectiveRecency} minutes)`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
33
33
  return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
@@ -7,14 +7,14 @@ export const LIST_FEEDS_TOOL_NAME = "list_feeds";
7
7
  export function getListFeedsToolConfig(agentId, registry, feedManager, sensorManager, systemSettingsManager) {
8
8
  const inputSchema = {
9
9
  limit: z.number().optional().describe(`Number of feeds to return per page.`),
10
- skip: z.number().optional().describe("Number of feeds to skip for pagination."),
10
+ offset: z.number().optional().describe("Number of feeds to skip before the page starts (0-based offset)."),
11
11
  };
12
- const handler = async ({ limit, skip }) => {
12
+ const handler = async ({ limit, offset }) => {
13
13
  try {
14
14
  const timezone = await sensorManager.getUserTimezone();
15
15
  const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
16
16
  const feeds = await feedManager.getFeeds(Array.from(visibleFeedIdSet));
17
- const pagination = applyPagination(feeds, limit || DEFAULT_FEEDS_LIMIT, skip);
17
+ const pagination = applyPagination(feeds, limit || DEFAULT_FEEDS_LIMIT, offset);
18
18
  const formattedFeeds = pagination.items.map((feed) => formatFeed(feed, timezone));
19
19
  const header = formatPaginationHeader("Available feeds", pagination);
20
20
  return textToolResult(header.concat("", formattedFeeds));
@@ -9,9 +9,9 @@ export function getSearchFeedItemsToolConfig(agentId, registry, feedManager, sen
9
9
  query: z.string().describe("Natural-language phrase or topic to match against feed items."),
10
10
  feedId: z.string().optional().describe("Optional feed ID to filter search results to a specific feed."),
11
11
  limit: z.number().optional().describe(`Number of items to return per page.`),
12
- skip: z.number().optional().describe("Number of items to skip for pagination."),
12
+ offset: z.number().optional().describe("Number of items to skip before the page starts (0-based offset)."),
13
13
  };
14
- const handler = async ({ query, feedId, limit, skip }) => {
14
+ const handler = async ({ query, feedId, limit, offset }) => {
15
15
  try {
16
16
  const timezone = await sensorManager.getUserTimezone();
17
17
  const visibleFeedIdSet = await resolveVisibleFeedIds(agentId, registry, systemSettingsManager);
@@ -25,7 +25,7 @@ export function getSearchFeedItemsToolConfig(agentId, registry, feedManager, sen
25
25
  if (!allItems.length) {
26
26
  return textToolResult([`No items found matching "${query}".`]);
27
27
  }
28
- const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, skip);
28
+ const pagination = applyPagination(allItems, limit || DEFAULT_FEED_ITEMS_LIMIT, offset);
29
29
  const formattedItems = pagination.items.map((item) => formatFeedItemSummary(item, feedsMap.get(item.feedId), timezone));
30
30
  const header = formatPaginationHeader(`Items matching "${query}"`, pagination, "Items show summary only — use get_feed_item_content with an item ID for full content");
31
31
  return textToolResult(header.concat("", formattedItems.flatMap((item) => [item, "---"])));
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { PLACES_SOURCE } from "../../services/places/places-manager.types.js";
3
3
  import { getErrorToolResult, textToolResult } from "../tool-utils.js";
4
4
  import { formatPlaceSummary } from "./places-format-utils.js";
5
+ import { resolveRoutingOrigin, routingDescriptionLine, routingOriginInputSchema } from "./places-routing-input.js";
5
6
  const DEFAULT_GEOCODE_LIMIT = 10;
6
7
  const MAX_GEOCODE_LIMIT = 25;
7
8
  export const GEOCODE_PLACE_TOOL_NAME = "geocode_place";
@@ -25,7 +26,9 @@ function selectForSource(map, source, fallback) {
25
26
  }
26
27
  export function getGeocodePlaceToolConfig(placesManager) {
27
28
  const defaultSource = placesManager.getDefaultSource();
28
- const description = selectForSource(DESCRIPTION_BY_SOURCE, defaultSource, NEUTRAL_DESCRIPTION);
29
+ const baseDescription = selectForSource(DESCRIPTION_BY_SOURCE, defaultSource, NEUTRAL_DESCRIPTION);
30
+ const routingLine = routingDescriptionLine(defaultSource);
31
+ const description = routingLine ? `${baseDescription} ${routingLine}` : baseDescription;
29
32
  const resultGuidance = selectForSource(RESULT_GUIDANCE_BY_SOURCE, defaultSource, NEUTRAL_RESULT_GUIDANCE);
30
33
  const inputSchema = {
31
34
  query: z.string().min(1).describe("A place name, a full address, or a landmark (e.g. 'Eiffel Tower')."),
@@ -42,6 +45,7 @@ export function getGeocodePlaceToolConfig(placesManager) {
42
45
  .optional()
43
46
  .describe("Optional latitude for ranking bias - prefers candidates near this point."),
44
47
  nearLongitude: z.number().min(-180).max(180).optional().describe("Optional longitude paired with nearLatitude."),
48
+ ...routingOriginInputSchema,
45
49
  limit: z
46
50
  .number()
47
51
  .int()
@@ -54,6 +58,10 @@ export function getGeocodePlaceToolConfig(placesManager) {
54
58
  if ((args.nearLatitude === undefined) !== (args.nearLongitude === undefined)) {
55
59
  return textToolResult(["nearLatitude and nearLongitude must both be provided or both omitted."], true);
56
60
  }
61
+ const routing = resolveRoutingOrigin(args);
62
+ if ("error" in routing) {
63
+ return textToolResult([routing.error], true);
64
+ }
57
65
  const queryParts = [args.query, args.city, args.country]
58
66
  .map((part) => part?.trim())
59
67
  .filter((part) => part !== undefined && part.length > 0);
@@ -68,6 +76,7 @@ export function getGeocodePlaceToolConfig(placesManager) {
68
76
  const places = await placesManager.geocode({
69
77
  text: queryText,
70
78
  near,
79
+ routingOrigin: routing.routingOrigin,
71
80
  limit: args.limit ?? DEFAULT_GEOCODE_LIMIT,
72
81
  });
73
82
  if (places.length === 0) {
@@ -25,8 +25,32 @@ export function formatPlaceSummary(place) {
25
25
  if (place.mapsUrl !== undefined && place.mapsUrl.length > 0) {
26
26
  lines.push(` - Map: ${place.mapsUrl}`);
27
27
  }
28
+ if (place.routing !== undefined) {
29
+ const { durationSeconds, distanceMeters, travelMode, directionsUrl } = place.routing;
30
+ lines.push(` - Travel: ${humanizeDuration(durationSeconds)} · ${humanizeDistance(distanceMeters)} (${travelMode})`);
31
+ if (directionsUrl !== undefined && directionsUrl.length > 0) {
32
+ lines.push(` - Directions: ${directionsUrl}`);
33
+ }
34
+ }
28
35
  return lines.join("\n");
29
36
  }
37
+ /** "8 min" under an hour, "1 h 12 min" (or "2 h" on the hour) above. */
38
+ function humanizeDuration(totalSeconds) {
39
+ const totalMinutes = Math.round(totalSeconds / 60);
40
+ if (totalMinutes < 60) {
41
+ return `${totalMinutes} min`;
42
+ }
43
+ const hours = Math.floor(totalMinutes / 60);
44
+ const minutes = totalMinutes % 60;
45
+ return minutes === 0 ? `${hours} h` : `${hours} h ${minutes} min`;
46
+ }
47
+ /** "850 m" under a kilometre, "3.2 km" (one decimal) above. */
48
+ function humanizeDistance(meters) {
49
+ if (meters < 1000) {
50
+ return `${Math.round(meters)} m`;
51
+ }
52
+ return `${(meters / 1000).toFixed(1)} km`;
53
+ }
30
54
  /** Full-detail block returned by `places_get_details`. Layers attributes on top of the summary. */
31
55
  export function formatPlaceDetails(details) {
32
56
  const lines = [formatPlaceSummary(details)];
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { PLACES_SOURCE, TRAVEL_MODE, } from "../../services/places/places-manager.types.js";
3
+ const TRAVEL_MODE_VALUES = Object.values(TRAVEL_MODE);
4
+ /** Optional routing-origin inputs shared by the place search and geocode tools. */
5
+ export const routingOriginInputSchema = {
6
+ originLatitude: z
7
+ .number()
8
+ .min(-90)
9
+ .max(90)
10
+ .optional()
11
+ .describe("Latitude of the travel origin; pair with originLongitude to attach travel time and distance."),
12
+ originLongitude: z
13
+ .number()
14
+ .min(-180)
15
+ .max(180)
16
+ .optional()
17
+ .describe("Longitude of the travel origin; must be paired with originLatitude."),
18
+ travelMode: z
19
+ .enum(TRAVEL_MODE_VALUES)
20
+ .optional()
21
+ .describe("Travel mode for the origin (default DRIVE). Ignored unless an origin is provided."),
22
+ };
23
+ /**
24
+ * Validate the both-or-neither origin pair and build a `RoutingOrigin`. A
25
+ * `travelMode` without an origin is a no-op rather than an error.
26
+ */
27
+ export function resolveRoutingOrigin(args) {
28
+ const { originLatitude, originLongitude, travelMode } = args;
29
+ if ((originLatitude === undefined) !== (originLongitude === undefined)) {
30
+ return { error: "originLatitude and originLongitude must both be provided or both omitted." };
31
+ }
32
+ if (originLatitude === undefined || originLongitude === undefined) {
33
+ return { routingOrigin: undefined };
34
+ }
35
+ const routingOrigin = { point: { latitude: originLatitude, longitude: originLongitude } };
36
+ if (travelMode) {
37
+ routingOrigin.travelMode = travelMode;
38
+ }
39
+ return { routingOrigin };
40
+ }
41
+ const ROUTING_DESCRIPTION_BY_SOURCE = {
42
+ [PLACES_SOURCE.GOOGLE]: "Provide originLatitude/originLongitude to attach travel time and distance to each result.",
43
+ [PLACES_SOURCE.OSM]: "Travel time and distance to results are not available on this source.",
44
+ };
45
+ /** One-line, source-aware note about routing support for a tool description. */
46
+ export function routingDescriptionLine(source) {
47
+ return ROUTING_DESCRIPTION_BY_SOURCE[source];
48
+ }
49
+ //# sourceMappingURL=places-routing-input.js.map
@@ -2,12 +2,17 @@ import { z } from "zod";
2
2
  import { PLACE_CATEGORY } from "../../services/places/places-manager.types.js";
3
3
  import { getErrorToolResult, textToolResult } from "../tool-utils.js";
4
4
  import { formatPlaceSummary } from "./places-format-utils.js";
5
+ import { resolveRoutingOrigin, routingDescriptionLine, routingOriginInputSchema } from "./places-routing-input.js";
5
6
  const DEFAULT_SEARCH_NEARBY_LIMIT = 20;
6
7
  const MAX_SEARCH_NEARBY_LIMIT = 50;
7
8
  const MAX_SEARCH_NEARBY_RADIUS_METERS = 50_000;
8
9
  const CATEGORY_VALUES = Object.values(PLACE_CATEGORY);
9
10
  export const SEARCH_NEARBY_PLACES_TOOL_NAME = "search_nearby_places";
11
+ const BASE_DESCRIPTION = "Find places of a given category within a radius around a point. Use get_place_details with a returned id " +
12
+ "for opening hours, contact info, and other attributes.";
10
13
  export function getSearchNearbyPlacesToolConfig(placesManager) {
14
+ const routingLine = routingDescriptionLine(placesManager.getDefaultSource());
15
+ const description = routingLine ? `${BASE_DESCRIPTION} ${routingLine}` : BASE_DESCRIPTION;
11
16
  const inputSchema = {
12
17
  latitude: z.number().min(-90).max(90).describe("Latitude of the search center."),
13
18
  longitude: z.number().min(-180).max(180).describe("Longitude of the search center."),
@@ -20,6 +25,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
20
25
  category: z
21
26
  .enum(CATEGORY_VALUES)
22
27
  .describe("Curated place category to filter by. Use OTHER only as a last resort - prefer a more specific value."),
28
+ ...routingOriginInputSchema,
23
29
  limit: z
24
30
  .number()
25
31
  .int()
@@ -29,6 +35,10 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
29
35
  .describe(`Max results to return (default ${DEFAULT_SEARCH_NEARBY_LIMIT}, max ${MAX_SEARCH_NEARBY_LIMIT}).`),
30
36
  };
31
37
  const handler = async (args) => {
38
+ const routing = resolveRoutingOrigin(args);
39
+ if ("error" in routing) {
40
+ return textToolResult([routing.error], true);
41
+ }
32
42
  try {
33
43
  const places = await placesManager.searchPlaces({
34
44
  area: {
@@ -37,6 +47,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
37
47
  radiusMeters: args.radiusMeters,
38
48
  },
39
49
  category: args.category,
50
+ routingOrigin: routing.routingOrigin,
40
51
  limit: args.limit ?? DEFAULT_SEARCH_NEARBY_LIMIT,
41
52
  });
42
53
  const areaLabel = `within ${args.radiusMeters}m of ${args.latitude.toFixed(6)}, ${args.longitude.toFixed(6)}`;
@@ -53,7 +64,7 @@ export function getSearchNearbyPlacesToolConfig(placesManager) {
53
64
  };
54
65
  const config = {
55
66
  name: SEARCH_NEARBY_PLACES_TOOL_NAME,
56
- description: "Find places of a given category within a radius around a point. Use get_place_details with a returned id for opening hours, contact info, and other attributes.",
67
+ description,
57
68
  inputSchema,
58
69
  handler,
59
70
  };
@@ -6,16 +6,16 @@ export const LIST_REMINDERS_TOOL_NAME = "list_reminders";
6
6
  export function getListRemindersToolConfig(agentId, scheduler, sensorManager) {
7
7
  const inputSchema = {
8
8
  limit: z.number().optional().describe("Number of reminders to return per page."),
9
- skip: z.number().optional().describe("Number of reminders to skip for pagination."),
9
+ offset: z.number().optional().describe("Number of reminders to skip before the page starts (0-based offset)."),
10
10
  };
11
- const handler = async ({ limit, skip }) => {
11
+ const handler = async ({ limit, offset }) => {
12
12
  try {
13
13
  const userTimezone = await sensorManager.getUserTimezone();
14
14
  const reminders = scheduler.listAgentReminders(agentId);
15
15
  if (reminders.length === 0) {
16
16
  return textToolResult(["You have no upcoming reminders."]);
17
17
  }
18
- const pagination = applyPagination(reminders, limit || DEFAULT_REMINDERS_LIMIT, skip);
18
+ const pagination = applyPagination(reminders, limit || DEFAULT_REMINDERS_LIMIT, offset);
19
19
  const formatted = pagination.items.map((reminder) => formatReminder(reminder, userTimezone)).join("\n---\n");
20
20
  const header = formatPaginationHeader("Your reminders", pagination);
21
21
  return textToolResult(header.concat("", formatted));
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  /**
2
3
  * Creates a tool result object with text content.
3
4
  *
@@ -29,32 +30,48 @@ export const getErrorToolResult = (error, fallbackMessage) => {
29
30
  const errorMessage = exceptionError ? exceptionError : fallbackMessage;
30
31
  return textToolResult([errorMessage], true);
31
32
  };
32
- export const applyPagination = (allItems, limit, skip) => {
33
- const effectiveSkip = skip || 0;
34
- const items = allItems.slice(effectiveSkip, effectiveSkip + limit);
33
+ /**
34
+ * Build an object schema from a tool input shape. Schemas with parameters are strict so unknown parameters are
35
+ * rejected rather than silently dropped; no-arg tools stay lenient since there is no parameter to confuse.
36
+ */
37
+ export const buildStrictToolSchema = (inputSchema) => {
38
+ const schema = z.object(inputSchema);
39
+ return Object.keys(inputSchema).length > 0 ? schema.strict() : schema;
40
+ };
41
+ /** Format a Zod validation failure as an agent-facing error result that names the offending parameters. */
42
+ export const getValidationErrorToolResult = (toolName, error) => {
43
+ const issues = error.issues.map((issue) => {
44
+ const path = issue.path.join(".");
45
+ return path ? `${path}: ${issue.message}` : issue.message;
46
+ });
47
+ return textToolResult([`Invalid arguments for ${toolName}:`, ...issues], true);
48
+ };
49
+ export const applyPagination = (allItems, limit, offset) => {
50
+ const effectiveOffset = offset || 0;
51
+ const items = allItems.slice(effectiveOffset, effectiveOffset + limit);
35
52
  return {
36
53
  items,
37
54
  totalCount: allItems.length,
38
- effectiveSkip,
39
- hasMore: effectiveSkip + items.length < allItems.length,
55
+ effectiveOffset,
56
+ hasMore: effectiveOffset + items.length < allItems.length,
40
57
  };
41
58
  };
42
59
  export const formatPaginationHeader = (description, pagination, note) => {
43
- const { items, totalCount, effectiveSkip, hasMore } = pagination;
44
- const isPaginated = effectiveSkip > 0 || hasMore;
60
+ const { items, totalCount, effectiveOffset, hasMore } = pagination;
61
+ const isPaginated = effectiveOffset > 0 || hasMore;
45
62
  const metaParts = [`Total: ${totalCount}`];
46
63
  if (isPaginated) {
47
64
  metaParts.push(`Showing: ${items.length}`);
48
65
  }
49
- if (effectiveSkip > 0) {
50
- metaParts.push(`Skipped: ${effectiveSkip}`);
66
+ if (effectiveOffset > 0) {
67
+ metaParts.push(`Offset: ${effectiveOffset}`);
51
68
  }
52
69
  const lines = [`--- ${description.toUpperCase()} ---`, `[${metaParts.join(" | ")}]`];
53
70
  if (note) {
54
71
  lines.push(`[${note}]`);
55
72
  }
56
73
  if (hasMore) {
57
- lines.push(`[More available: use skip=${effectiveSkip + items.length} for next page]`);
74
+ lines.push(`[More available: use offset=${effectiveOffset + items.length} for next page]`);
58
75
  }
59
76
  return lines;
60
77
  };