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.
- package/README.md +70 -0
- package/dist/_shared/schemas/agent.schema.js +2 -1
- package/dist/config/env.js +6 -0
- package/dist/mcp/places/geocode-place.js +33 -18
- package/dist/mcp/places/places-format-utils.js +23 -1
- package/dist/public/assets/{architectureDiagram-3BPJPVTR-BACYknch.js → architectureDiagram-3BPJPVTR-DDgVnuoo.js} +1 -1
- package/dist/public/assets/{chunk-727SXJPM-BCyrHTp7.js → chunk-727SXJPM-PDiAD8Du.js} +1 -1
- package/dist/public/assets/{chunk-AQP2D5EJ-DQW6WpUt.js → chunk-AQP2D5EJ-wy5_L253.js} +1 -1
- package/dist/public/assets/{classDiagram-v2-Q7XG4LA2-vP76aZOW.js → classDiagram-4FO5ZUOK-BYJ5bFf6.js} +1 -1
- package/dist/public/assets/{classDiagram-4FO5ZUOK-vP76aZOW.js → classDiagram-v2-Q7XG4LA2-BYJ5bFf6.js} +1 -1
- package/dist/public/assets/{dagre-BM42HDAG-cX3IYed4.js → dagre-BM42HDAG-CjoXczSR.js} +1 -1
- package/dist/public/assets/{diagram-2AECGRRQ-BdnHDBWj.js → diagram-2AECGRRQ-CCKmxsdo.js} +1 -1
- package/dist/public/assets/{diagram-5GNKFQAL-BEF52y8F.js → diagram-5GNKFQAL-yKZCpt4W.js} +1 -1
- package/dist/public/assets/{diagram-LMA3HP47-CkJFMgD9.js → diagram-LMA3HP47-DneZvlxQ.js} +1 -1
- package/dist/public/assets/{diagram-OG6HWLK6-CzOfEubM.js → diagram-OG6HWLK6-Ud_EdkZh.js} +1 -1
- package/dist/public/assets/{erDiagram-TEJ5UH35-B6X8wID6.js → erDiagram-TEJ5UH35-B-vZXvBd.js} +1 -1
- package/dist/public/assets/{flowDiagram-I6XJVG4X-Bx048skw.js → flowDiagram-I6XJVG4X-rVn14Wbt.js} +1 -1
- package/dist/public/assets/{index-BuB5ROMJ.js → index-BTKdu_X_.js} +5 -5
- package/dist/public/assets/{infoDiagram-5YYISTIA-BfOovwvr.js → infoDiagram-5YYISTIA-BbX08lDC.js} +1 -1
- package/dist/public/assets/{ishikawaDiagram-YF4QCWOH-Bq2pdr7u.js → ishikawaDiagram-YF4QCWOH-CYs43POM.js} +1 -1
- package/dist/public/assets/{kanban-definition-UN3LZRKU-BYNOT0q9.js → kanban-definition-UN3LZRKU--Cb13wva.js} +1 -1
- package/dist/public/assets/{mindmap-definition-RKZ34NQL-ZqyFQHUp.js → mindmap-definition-RKZ34NQL-CQJGU9F-.js} +1 -1
- package/dist/public/assets/{pieDiagram-4H26LBE5-adI7z4xv.js → pieDiagram-4H26LBE5-DMNxEuMG.js} +1 -1
- package/dist/public/assets/{requirementDiagram-4Y6WPE33-DSQpGrFx.js → requirementDiagram-4Y6WPE33-DVzHOXTK.js} +1 -1
- package/dist/public/assets/{sequenceDiagram-3UESZ5HK-CedIlDfK.js → sequenceDiagram-3UESZ5HK-CrIiBex5.js} +1 -1
- package/dist/public/assets/{stateDiagram-AJRCARHV-Dyff9k9-.js → stateDiagram-AJRCARHV-LVBo72aS.js} +1 -1
- package/dist/public/assets/{stateDiagram-v2-BHNVJYJU-BYjnQQxJ.js → stateDiagram-v2-BHNVJYJU-pc1U14yi.js} +1 -1
- package/dist/public/assets/{timeline-definition-PNZ67QCA-BXAcpPyH.js → timeline-definition-PNZ67QCA-COcJo8Ga.js} +1 -1
- package/dist/public/assets/{vennDiagram-CIIHVFJN-CmL9UiW5.js → vennDiagram-CIIHVFJN-qOZm-QC4.js} +1 -1
- package/dist/public/assets/{wardleyDiagram-YWT4CUSO-rPbjnaXo.js → wardleyDiagram-YWT4CUSO-idQAwlbA.js} +1 -1
- package/dist/public/assets/{xychartDiagram-2RQKCTM6-BTJUCOrb.js → xychartDiagram-2RQKCTM6-Caywd0sW.js} +1 -1
- package/dist/public/index.html +1 -1
- package/dist/sensors/geolocation-sensor.js +2 -2
- package/dist/services/agent-registry.js +4 -1
- package/dist/services/google/google-request.js +5 -1
- package/dist/services/google/google-request.types.js +11 -1
- package/dist/services/places/google/google-address-utils.js +51 -0
- package/dist/services/places/google/google-opening-hours-parser.js +105 -0
- package/dist/services/places/google/google-place-mapper.js +149 -0
- package/dist/services/places/google/google-place-type-mapping.js +70 -0
- package/dist/services/places/google/google-places-adapter.js +105 -0
- package/dist/services/places/google/google-places-adapter.types.js +94 -0
- package/dist/services/places/google/google-places-client.js +141 -0
- package/dist/services/places/google/google-request-utils.js +22 -0
- package/dist/services/places/google/google-search-area.js +31 -0
- package/dist/services/places/google/google-transit-mapping.js +43 -0
- package/dist/services/places/places-manager.js +19 -1
- package/dist/services/places/places-manager.types.js +27 -0
- package/dist/utils/number-utils.js +5 -0
- 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
|
|
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(),
|
package/dist/config/env.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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')
|
|
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
|
-
|
|
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(["
|
|
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"];
|