crow-central-agency 0.26.7 → 0.26.8

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/README.md +70 -0
  2. package/dist/_shared/schemas/agent.schema.js +2 -1
  3. package/dist/config/env.js +6 -0
  4. package/dist/mcp/places/geocode-place.js +33 -18
  5. package/dist/mcp/places/places-format-utils.js +23 -1
  6. package/dist/public/assets/{architectureDiagram-3BPJPVTR-BACYknch.js → architectureDiagram-3BPJPVTR-DDgVnuoo.js} +1 -1
  7. package/dist/public/assets/{chunk-727SXJPM-BCyrHTp7.js → chunk-727SXJPM-PDiAD8Du.js} +1 -1
  8. package/dist/public/assets/{chunk-AQP2D5EJ-DQW6WpUt.js → chunk-AQP2D5EJ-wy5_L253.js} +1 -1
  9. package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-vP76aZOW.js → classDiagram-4FO5ZUOK-BYJ5bFf6.js} +1 -1
  10. package/dist/public/assets/{classDiagram-4FO5ZUOK-vP76aZOW.js → classDiagram-v2-Q7XG4LA2-BYJ5bFf6.js} +1 -1
  11. package/dist/public/assets/{dagre-BM42HDAG-cX3IYed4.js → dagre-BM42HDAG-CjoXczSR.js} +1 -1
  12. package/dist/public/assets/{diagram-2AECGRRQ-BdnHDBWj.js → diagram-2AECGRRQ-CCKmxsdo.js} +1 -1
  13. package/dist/public/assets/{diagram-5GNKFQAL-BEF52y8F.js → diagram-5GNKFQAL-yKZCpt4W.js} +1 -1
  14. package/dist/public/assets/{diagram-LMA3HP47-CkJFMgD9.js → diagram-LMA3HP47-DneZvlxQ.js} +1 -1
  15. package/dist/public/assets/{diagram-OG6HWLK6-CzOfEubM.js → diagram-OG6HWLK6-Ud_EdkZh.js} +1 -1
  16. package/dist/public/assets/{erDiagram-TEJ5UH35-B6X8wID6.js → erDiagram-TEJ5UH35-B-vZXvBd.js} +1 -1
  17. package/dist/public/assets/{flowDiagram-I6XJVG4X-Bx048skw.js → flowDiagram-I6XJVG4X-rVn14Wbt.js} +1 -1
  18. package/dist/public/assets/{index-BuB5ROMJ.js → index-BTKdu_X_.js} +5 -5
  19. package/dist/public/assets/{infoDiagram-5YYISTIA-BfOovwvr.js → infoDiagram-5YYISTIA-BbX08lDC.js} +1 -1
  20. package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-Bq2pdr7u.js → ishikawaDiagram-YF4QCWOH-CYs43POM.js} +1 -1
  21. package/dist/public/assets/{kanban-definition-UN3LZRKU-BYNOT0q9.js → kanban-definition-UN3LZRKU--Cb13wva.js} +1 -1
  22. package/dist/public/assets/{mindmap-definition-RKZ34NQL-ZqyFQHUp.js → mindmap-definition-RKZ34NQL-CQJGU9F-.js} +1 -1
  23. package/dist/public/assets/{pieDiagram-4H26LBE5-adI7z4xv.js → pieDiagram-4H26LBE5-DMNxEuMG.js} +1 -1
  24. package/dist/public/assets/{requirementDiagram-4Y6WPE33-DSQpGrFx.js → requirementDiagram-4Y6WPE33-DVzHOXTK.js} +1 -1
  25. package/dist/public/assets/{sequenceDiagram-3UESZ5HK-CedIlDfK.js → sequenceDiagram-3UESZ5HK-CrIiBex5.js} +1 -1
  26. package/dist/public/assets/{stateDiagram-AJRCARHV-Dyff9k9-.js → stateDiagram-AJRCARHV-LVBo72aS.js} +1 -1
  27. package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-BYjnQQxJ.js → stateDiagram-v2-BHNVJYJU-pc1U14yi.js} +1 -1
  28. package/dist/public/assets/{timeline-definition-PNZ67QCA-BXAcpPyH.js → timeline-definition-PNZ67QCA-COcJo8Ga.js} +1 -1
  29. package/dist/public/assets/{vennDiagram-CIIHVFJN-CmL9UiW5.js → vennDiagram-CIIHVFJN-qOZm-QC4.js} +1 -1
  30. package/dist/public/assets/{wardleyDiagram-YWT4CUSO-rPbjnaXo.js → wardleyDiagram-YWT4CUSO-idQAwlbA.js} +1 -1
  31. package/dist/public/assets/{xychartDiagram-2RQKCTM6-BTJUCOrb.js → xychartDiagram-2RQKCTM6-Caywd0sW.js} +1 -1
  32. package/dist/public/index.html +1 -1
  33. package/dist/sensors/geolocation-sensor.js +2 -2
  34. package/dist/services/agent-registry.js +4 -1
  35. package/dist/services/google/google-request.js +5 -1
  36. package/dist/services/google/google-request.types.js +11 -1
  37. package/dist/services/places/google/google-address-utils.js +51 -0
  38. package/dist/services/places/google/google-opening-hours-parser.js +105 -0
  39. package/dist/services/places/google/google-place-mapper.js +149 -0
  40. package/dist/services/places/google/google-place-type-mapping.js +70 -0
  41. package/dist/services/places/google/google-places-adapter.js +105 -0
  42. package/dist/services/places/google/google-places-adapter.types.js +94 -0
  43. package/dist/services/places/google/google-places-client.js +141 -0
  44. package/dist/services/places/google/google-request-utils.js +22 -0
  45. package/dist/services/places/google/google-search-area.js +31 -0
  46. package/dist/services/places/google/google-transit-mapping.js +43 -0
  47. package/dist/services/places/places-manager.js +19 -1
  48. package/dist/services/places/places-manager.types.js +27 -0
  49. package/dist/utils/number-utils.js +5 -0
  50. package/package.json +2 -2
package/README.md CHANGED
@@ -107,6 +107,9 @@ See `.env.example` for the full list, including:
107
107
  - `FEED_TEXT_GENERATION_*` — optional OpenAI-compatible endpoint used by the feed manager to summarize feed items into a consistent length for better agent consumption.
108
108
  - `FEED_MAX_SUMMARIZATION_ITEMS` — cap on items summarized per refresh (default: `50`). Items left unsummarized become retry candidates on the next refresh; when the retry backlog exceeds the cap, retries are skipped so a feed isn't permanently blocked by hundreds or thousands of stuck unsummarized items.
109
109
  - `AUDIO_GENERATION_*` — optional Gemini TTS configuration that powers the play-message button on the agent console. See [Audio generation](#audio-generation) below.
110
+ - `PLACES_DEFAULT_SOURCE` — default provider for the Places tools when a lookup doesn't request one explicitly: `OSM` (default, no key needed) or `GOOGLE`. `GOOGLE` requires `GOOGLE_PLACES_API_KEY`; without it, lookups fall back to OSM. See [Places](#places) below.
111
+ - `GOOGLE_PLACES_API_KEY` — Google API key that registers the Google Places adapter alongside the default OSM source. See [Places](#places) below.
112
+ - `PHOTON_API_URL` / `OVERPASS_INTERPRETER_URL` — override the geocoding / nearby-search endpoints the OSM places source calls (default to the public Photon and Overpass instances).
110
113
  - `GOOGLE_CONNECTOR_CLIENT_ID` / `GOOGLE_CONNECTOR_CLIENT_SECRET` / `CONNECTOR_CALLBACK_URL` — OAuth credentials for the Google connector. Required if you want agents to access Gmail / Calendar / Contacts. See [Connectors](#connectors) below.
111
114
  - `OAUTH_PENDING_STATE_TTL_MS` — how long an unfinished OAuth flow stays valid before being swept (default: `600_000`, i.e. 10 minutes).
112
115
  - `OTEL_*` — optional OpenTelemetry export.
@@ -139,6 +142,73 @@ The `AUDIO_GENERATION_API_KEY` must be a Google API key with access to the
139
142
  You can use the same key as `TEXT_GENERATION_API_KEY` if that is also a
140
143
  Gemini-backed configuration, but the two are read independently.
141
144
 
145
+ ## Places
146
+
147
+ The **Places** MCP server gives agents location tools — `geocode_place`,
148
+ `search_nearby_places`, and `get_place_details`. It is enabled per-agent from
149
+ the **MCP Servers** section of the agent editor.
150
+
151
+ Two providers back these tools:
152
+
153
+ - **OSM** (default) — uses the public OpenStreetMap-based services (Photon for
154
+ geocoding, Overpass for nearby search). No API key required, so the Places
155
+ tools work out of the box.
156
+ - **GOOGLE** — uses the Google Places API (New) and Geocoding API for richer
157
+ results. Opt in by setting `GOOGLE_PLACES_API_KEY`.
158
+
159
+ `PLACES_DEFAULT_SOURCE` selects which provider serves lookups that don't name a
160
+ source explicitly (`OSM` or `GOOGLE`, defaulting to `OSM`). When
161
+ `GOOGLE_PLACES_API_KEY` is set, the Google adapter is registered alongside OSM;
162
+ if `PLACES_DEFAULT_SOURCE=GOOGLE` is set without a key, lookups fall back to OSM.
163
+
164
+ ### Configuration
165
+
166
+ ```bash
167
+ PLACES_DEFAULT_SOURCE=GOOGLE
168
+ GOOGLE_PLACES_API_KEY=<your Google API key>
169
+ ```
170
+
171
+ ### Getting a Google Places API key
172
+
173
+ The key is a plain Google API key (not an OAuth client — unrelated to the
174
+ Google connector setup below).
175
+
176
+ **1. Sign in to Google Cloud Console.**
177
+
178
+ Go to [console.cloud.google.com](https://console.cloud.google.com/) and select
179
+ (or create) a project. Unlike the OAuth connector, the Places and Geocoding
180
+ APIs are **paid** and require a billing account on the project. Google provides
181
+ a recurring monthly free allotment, but you must still enable billing to use
182
+ the key.
183
+
184
+ **2. Enable the required APIs.**
185
+
186
+ Open **APIs & Services → Library** and enable both:
187
+
188
+ - **Places API (New)** — powers `search_nearby_places` and `get_place_details`.
189
+ - **Geocoding API** — powers reverse geocoding for `geocode_place`.
190
+
191
+ Click each result, then click **Enable**.
192
+
193
+ **3. Create the API key.**
194
+
195
+ Open **APIs & Services → Credentials → Create Credentials → API key**. Copy the
196
+ generated key.
197
+
198
+ > [!NOTE]
199
+ > Restrict the key to just the **Places API (New)** and **Geocoding API** under
200
+ > the key's **API restrictions** so a leaked key can't be used against other
201
+ > Google services.
202
+
203
+ **4. Add the key to your `.env` and restart Crow.**
204
+
205
+ ```bash
206
+ GOOGLE_PLACES_API_KEY=<paste the API key>
207
+ PLACES_DEFAULT_SOURCE=GOOGLE
208
+ ```
209
+
210
+ Restart the server so it picks up the new env vars.
211
+
142
212
  ## Connectors
143
213
 
144
214
  Connectors are a framework-level capability - they are not tools agents call
@@ -317,7 +317,8 @@ export const UpdateAgentInputSchema = z.object({
317
317
  workspace: z.string().optional(),
318
318
  persona: z.string().optional(),
319
319
  model: z.string().optional(),
320
- effort: ReasoningEffortSchema.optional(),
320
+ // null explicitly clears the effort (back to provider default); undefined leaves it unchanged.
321
+ effort: ReasoningEffortSchema.nullish(),
321
322
  permissionMode: PermissionModeSchema.optional(),
322
323
  settingSources: z.array(SettingSourceSchema).optional(),
323
324
  settingSourceConfig: SettingSourceConfigInputSchema.optional(),
@@ -2,6 +2,7 @@ import os from "node:os";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { expandPath } from "../utils/fs-utils.js";
5
+ import { PLACES_SOURCE } from "../services/places/places-manager.types.js";
5
6
  const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
6
7
  const DIST_DIR = path.resolve(CONFIG_DIR, "..");
7
8
  const DEFAULT_STATIC_DIR = path.join(DIST_DIR, "public");
@@ -15,6 +16,9 @@ function getBoolean(key) {
15
16
  const value = process.env[key]?.trim().toLowerCase();
16
17
  return value === "true" || value === "1";
17
18
  }
19
+ function resolvePlacesSource(key) {
20
+ return process.env[key]?.trim().toUpperCase() === PLACES_SOURCE.GOOGLE ? PLACES_SOURCE.GOOGLE : PLACES_SOURCE.OSM;
21
+ }
18
22
  function readRequired(key) {
19
23
  const value = process.env[key]?.trim();
20
24
  if (!value) {
@@ -74,6 +78,8 @@ export const env = {
74
78
  GMAIL_CHECK_INTERVAL_IN_MINUTES: getOptionalNumber("GMAIL_CHECK_INTERVAL_IN_MINUTES"),
75
79
  PHOTON_API_URL: getOptional("PHOTON_API_URL") ?? "https://photon.komoot.io",
76
80
  OVERPASS_INTERPRETER_URL: getOptional("OVERPASS_INTERPRETER_URL") ?? "https://overpass-api.de/api/interpreter",
81
+ PLACES_DEFAULT_SOURCE: resolvePlacesSource("PLACES_DEFAULT_SOURCE"),
82
+ GOOGLE_PLACES_API_KEY: getOptional("GOOGLE_PLACES_API_KEY"),
77
83
  };
78
84
  /**
79
85
  * Eagerly validate all required environment variables. Call once during boot
@@ -1,20 +1,40 @@
1
1
  import { z } from "zod";
2
+ import { PLACES_SOURCE } from "../../services/places/places-manager.types.js";
2
3
  import { getErrorToolResult, textToolResult } from "../tool-utils.js";
3
4
  import { formatPlaceSummary } from "./places-format-utils.js";
4
5
  const DEFAULT_GEOCODE_LIMIT = 10;
5
6
  const MAX_GEOCODE_LIMIT = 25;
6
7
  export const GEOCODE_PLACE_TOOL_NAME = "geocode_place";
8
+ const NEUTRAL_DESCRIPTION = "Resolve a free-text query - a place name, full address, or landmark - to matching places. " +
9
+ "Use get_place_details on a returned id for opening hours, contact info, and other attributes.";
10
+ const NEUTRAL_RESULT_GUIDANCE = "Refine with a more specific name, address, or the city/country disambiguators if needed.";
11
+ const DESCRIPTION_BY_SOURCE = {
12
+ [PLACES_SOURCE.GOOGLE]: "Resolve a free-text query to matching places. Accepts a place name, a full address, a landmark, or a " +
13
+ "natural-language / descriptive POI search (e.g. 'rooftop bars in Shibuya'). " +
14
+ "Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
15
+ [PLACES_SOURCE.OSM]: "Resolve a free-text query to matching places. Best results come from a precise name or full address; " +
16
+ "descriptive or category-style queries degrade - for category discovery use search_nearby_places. " +
17
+ "Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
18
+ };
19
+ const RESULT_GUIDANCE_BY_SOURCE = {
20
+ [PLACES_SOURCE.GOOGLE]: "Phrase queries in natural language (names, full addresses, or descriptive POI searches) for stronger matches.",
21
+ [PLACES_SOURCE.OSM]: "Use a precise name or full address; descriptive or category queries return weak matches.",
22
+ };
23
+ function selectForSource(map, source, fallback) {
24
+ return map[source] ?? fallback;
25
+ }
7
26
  export function getGeocodePlaceToolConfig(placesManager) {
27
+ const defaultSource = placesManager.getDefaultSource();
28
+ const description = selectForSource(DESCRIPTION_BY_SOURCE, defaultSource, NEUTRAL_DESCRIPTION);
29
+ const resultGuidance = selectForSource(RESULT_GUIDANCE_BY_SOURCE, defaultSource, NEUTRAL_RESULT_GUIDANCE);
8
30
  const inputSchema = {
9
- name: z.string().min(1).optional().describe("Proper-noun name of a specific place or business."),
10
- street: z.string().min(1).optional().describe("Street address, including house number when known."),
11
- city: z.string().min(1).optional().describe("City, town, or locality. Example: 'London', 'Paris'."),
12
- postcode: z.string().min(1).optional().describe("Postal / ZIP code."),
31
+ query: z.string().min(1).describe("A place name, a full address, or a landmark (e.g. 'Eiffel Tower')."),
32
+ city: z.string().min(1).optional().describe("City, town, or locality to disambiguate the query."),
13
33
  country: z
14
34
  .string()
15
35
  .min(1)
16
36
  .optional()
17
- .describe("Country name or ISO 3166-1 alpha-2 code (e.g. 'France' or 'FR'). Use to disambiguate same-named places across countries."),
37
+ .describe("Country name or ISO 3166-1 alpha-2 code (e.g. 'France' or 'FR') to disambiguate same-named places."),
18
38
  nearLatitude: z
19
39
  .number()
20
40
  .min(-90)
@@ -31,14 +51,14 @@ export function getGeocodePlaceToolConfig(placesManager) {
31
51
  .describe(`Max results to return (default ${DEFAULT_GEOCODE_LIMIT}, max ${MAX_GEOCODE_LIMIT}).`),
32
52
  };
33
53
  const handler = async (args) => {
34
- const queryParts = [args.name, args.street, args.city, args.postcode, args.country]
54
+ if ((args.nearLatitude === undefined) !== (args.nearLongitude === undefined)) {
55
+ return textToolResult(["nearLatitude and nearLongitude must both be provided or both omitted."], true);
56
+ }
57
+ const queryParts = [args.query, args.city, args.country]
35
58
  .map((part) => part?.trim())
36
59
  .filter((part) => part !== undefined && part.length > 0);
37
60
  if (queryParts.length === 0) {
38
- return textToolResult(["At least one of name, street, city, postcode, or country must be provided."], true);
39
- }
40
- if ((args.nearLatitude === undefined) !== (args.nearLongitude === undefined)) {
41
- return textToolResult(["nearLatitude and nearLongitude must both be provided or both omitted."], true);
61
+ return textToolResult(["query must contain non-whitespace text."], true);
42
62
  }
43
63
  const queryText = queryParts.join(", ");
44
64
  try {
@@ -51,11 +71,11 @@ export function getGeocodePlaceToolConfig(placesManager) {
51
71
  limit: args.limit ?? DEFAULT_GEOCODE_LIMIT,
52
72
  });
53
73
  if (places.length === 0) {
54
- return textToolResult([`No places found matching "${queryText}".`]);
74
+ return textToolResult([`No places found matching "${queryText}".`, resultGuidance]);
55
75
  }
56
76
  const header = `[Geocode results: ${places.length} for "${queryText}"]`;
57
77
  const body = places.map(formatPlaceSummary).join("\n");
58
- return textToolResult([header, body]);
78
+ return textToolResult([header, body, resultGuidance]);
59
79
  }
60
80
  catch (error) {
61
81
  return getErrorToolResult(error, "Failed to geocode place.");
@@ -63,12 +83,7 @@ export function getGeocodePlaceToolConfig(placesManager) {
63
83
  };
64
84
  const config = {
65
85
  name: GEOCODE_PLACE_TOOL_NAME,
66
- description: [
67
- "Resolve a known place to coordinates: a named landmark, business, or specific street address. ",
68
- "Provide whichever structured fields you have - the more you fill, the more precise the match. ",
69
- "Not a discovery tool: for 'cafes near me', 'good restaurants', or any category / subjective query, use search_nearby_places instead. ",
70
- "Use get_place_details on a returned id for opening hours, contact info, and other attributes.",
71
- ].join(""),
86
+ description,
72
87
  inputSchema,
73
88
  handler,
74
89
  };
@@ -1,4 +1,4 @@
1
- import { WEEKDAY, } from "../../services/places/places-manager.types.js";
1
+ import { BUSINESS_STATUS, WEEKDAY, } from "../../services/places/places-manager.types.js";
2
2
  const WEEKDAY_LABEL = {
3
3
  [WEEKDAY.MONDAY]: "Monday",
4
4
  [WEEKDAY.TUESDAY]: "Tuesday",
@@ -19,6 +19,12 @@ export function formatPlaceSummary(place) {
19
19
  if (place.address !== undefined && place.address.length > 0) {
20
20
  lines.push(` - Address: ${place.address}`);
21
21
  }
22
+ if (place.businessStatus !== undefined && place.businessStatus !== BUSINESS_STATUS.OPERATIONAL) {
23
+ lines.push(` - Business status: ${place.businessStatus}`);
24
+ }
25
+ if (place.mapsUrl !== undefined && place.mapsUrl.length > 0) {
26
+ lines.push(` - Map: ${place.mapsUrl}`);
27
+ }
22
28
  return lines.join("\n");
23
29
  }
24
30
  /** Full-detail block returned by `places_get_details`. Layers attributes on top of the summary. */
@@ -48,6 +54,12 @@ export function formatPlaceDetails(details) {
48
54
  if (details.wheelchairAccess !== undefined) {
49
55
  lines.push(` - Wheelchair access: ${details.wheelchairAccess}`);
50
56
  }
57
+ if (details.transit !== undefined && details.transit.lines.length > 0) {
58
+ lines.push(" - Transit lines:");
59
+ for (const line of details.transit.lines) {
60
+ lines.push(` ${formatTransitLine(line)}`);
61
+ }
62
+ }
51
63
  if (details.description !== undefined && details.description.length > 0) {
52
64
  lines.push(" - Description:");
53
65
  for (const descriptionLine of details.description.split(/\r?\n/)) {
@@ -56,6 +68,16 @@ export function formatPlaceDetails(details) {
56
68
  }
57
69
  return lines.join("\n");
58
70
  }
71
+ function formatTransitLine(line) {
72
+ let formatted = `${line.vehicleType} ${line.shortName ?? line.name}`;
73
+ if (line.shortName !== undefined && line.shortName !== line.name) {
74
+ formatted += ` (${line.name})`;
75
+ }
76
+ if (line.operator !== undefined) {
77
+ formatted += ` — ${line.operator}`;
78
+ }
79
+ return formatted;
80
+ }
59
81
  function formatOpeningHoursLines(hours) {
60
82
  if (hours.alwaysOpen) {
61
83
  return ["Open 24/7"];