go-transit-mcp 0.0.0

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/dist/entry/http.js +78 -0
  4. package/dist/entry/http.js.map +1 -0
  5. package/dist/entry/stdio.js +16 -0
  6. package/dist/entry/stdio.js.map +1 -0
  7. package/dist/errors.js +30 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/metrolinx/cache.js +29 -0
  10. package/dist/metrolinx/cache.js.map +1 -0
  11. package/dist/metrolinx/client.js +180 -0
  12. package/dist/metrolinx/client.js.map +1 -0
  13. package/dist/metrolinx/types.js +5 -0
  14. package/dist/metrolinx/types.js.map +1 -0
  15. package/dist/normalize/fares.js +44 -0
  16. package/dist/normalize/fares.js.map +1 -0
  17. package/dist/normalize/fleet-consist.js +31 -0
  18. package/dist/normalize/fleet-consist.js.map +1 -0
  19. package/dist/normalize/journey.js +184 -0
  20. package/dist/normalize/journey.js.map +1 -0
  21. package/dist/normalize/line-schedule.js +56 -0
  22. package/dist/normalize/line-schedule.js.map +1 -0
  23. package/dist/normalize/list-lines.js +29 -0
  24. package/dist/normalize/list-lines.js.map +1 -0
  25. package/dist/normalize/next-service.js +59 -0
  26. package/dist/normalize/next-service.js.map +1 -0
  27. package/dist/normalize/search-stops.js +113 -0
  28. package/dist/normalize/search-stops.js.map +1 -0
  29. package/dist/normalize/service-alerts.js +61 -0
  30. package/dist/normalize/service-alerts.js.map +1 -0
  31. package/dist/normalize/service-exceptions.js +51 -0
  32. package/dist/normalize/service-exceptions.js.map +1 -0
  33. package/dist/normalize/service-guarantee.js +23 -0
  34. package/dist/normalize/service-guarantee.js.map +1 -0
  35. package/dist/normalize/stop-destinations.js +54 -0
  36. package/dist/normalize/stop-destinations.js.map +1 -0
  37. package/dist/normalize/stop-details.js +44 -0
  38. package/dist/normalize/stop-details.js.map +1 -0
  39. package/dist/normalize/trip-status.js +88 -0
  40. package/dist/normalize/trip-status.js.map +1 -0
  41. package/dist/normalize/trip-updates.js +113 -0
  42. package/dist/normalize/trip-updates.js.map +1 -0
  43. package/dist/normalize/union-departures.js +52 -0
  44. package/dist/normalize/union-departures.js.map +1 -0
  45. package/dist/normalize/vehicle-positions.js +67 -0
  46. package/dist/normalize/vehicle-positions.js.map +1 -0
  47. package/dist/prompts.js +96 -0
  48. package/dist/prompts.js.map +1 -0
  49. package/dist/resources.js +61 -0
  50. package/dist/resources.js.map +1 -0
  51. package/dist/schemas/fares.js +30 -0
  52. package/dist/schemas/fares.js.map +1 -0
  53. package/dist/schemas/fleet-consist.js +32 -0
  54. package/dist/schemas/fleet-consist.js.map +1 -0
  55. package/dist/schemas/journey.js +127 -0
  56. package/dist/schemas/journey.js.map +1 -0
  57. package/dist/schemas/line-schedule.js +43 -0
  58. package/dist/schemas/line-schedule.js.map +1 -0
  59. package/dist/schemas/list-lines.js +29 -0
  60. package/dist/schemas/list-lines.js.map +1 -0
  61. package/dist/schemas/next-service.js +29 -0
  62. package/dist/schemas/next-service.js.map +1 -0
  63. package/dist/schemas/search-stops.js +35 -0
  64. package/dist/schemas/search-stops.js.map +1 -0
  65. package/dist/schemas/service-alerts.js +55 -0
  66. package/dist/schemas/service-alerts.js.map +1 -0
  67. package/dist/schemas/service-exceptions.js +27 -0
  68. package/dist/schemas/service-exceptions.js.map +1 -0
  69. package/dist/schemas/service-guarantee.js +32 -0
  70. package/dist/schemas/service-guarantee.js.map +1 -0
  71. package/dist/schemas/stop-destinations.js +32 -0
  72. package/dist/schemas/stop-destinations.js.map +1 -0
  73. package/dist/schemas/stop-details.js +27 -0
  74. package/dist/schemas/stop-details.js.map +1 -0
  75. package/dist/schemas/trip-status.js +39 -0
  76. package/dist/schemas/trip-status.js.map +1 -0
  77. package/dist/schemas/trip-updates.js +51 -0
  78. package/dist/schemas/trip-updates.js.map +1 -0
  79. package/dist/schemas/union-departures.js +26 -0
  80. package/dist/schemas/union-departures.js.map +1 -0
  81. package/dist/schemas/vehicle-positions.js +48 -0
  82. package/dist/schemas/vehicle-positions.js.map +1 -0
  83. package/dist/server.js +58 -0
  84. package/dist/server.js.map +1 -0
  85. package/dist/time.js +159 -0
  86. package/dist/time.js.map +1 -0
  87. package/dist/tools/get-fares.js +26 -0
  88. package/dist/tools/get-fares.js.map +1 -0
  89. package/dist/tools/get-fleet-consist.js +34 -0
  90. package/dist/tools/get-fleet-consist.js.map +1 -0
  91. package/dist/tools/get-line-schedule.js +27 -0
  92. package/dist/tools/get-line-schedule.js.map +1 -0
  93. package/dist/tools/get-next-service.js +25 -0
  94. package/dist/tools/get-next-service.js.map +1 -0
  95. package/dist/tools/get-service-alerts.js +45 -0
  96. package/dist/tools/get-service-alerts.js.map +1 -0
  97. package/dist/tools/get-service-exceptions.js +25 -0
  98. package/dist/tools/get-service-exceptions.js.map +1 -0
  99. package/dist/tools/get-service-guarantee.js +26 -0
  100. package/dist/tools/get-service-guarantee.js.map +1 -0
  101. package/dist/tools/get-stop-destinations.js +28 -0
  102. package/dist/tools/get-stop-destinations.js.map +1 -0
  103. package/dist/tools/get-stop-details.js +25 -0
  104. package/dist/tools/get-stop-details.js.map +1 -0
  105. package/dist/tools/get-trip-status.js +31 -0
  106. package/dist/tools/get-trip-status.js.map +1 -0
  107. package/dist/tools/get-trip-updates.js +26 -0
  108. package/dist/tools/get-trip-updates.js.map +1 -0
  109. package/dist/tools/get-union-departures.js +29 -0
  110. package/dist/tools/get-union-departures.js.map +1 -0
  111. package/dist/tools/get-vehicle-positions.js +30 -0
  112. package/dist/tools/get-vehicle-positions.js.map +1 -0
  113. package/dist/tools/list-lines.js +27 -0
  114. package/dist/tools/list-lines.js.map +1 -0
  115. package/dist/tools/plan-journey.js +46 -0
  116. package/dist/tools/plan-journey.js.map +1 -0
  117. package/dist/tools/plan-trip.js +81 -0
  118. package/dist/tools/plan-trip.js.map +1 -0
  119. package/dist/tools/search-stops.js +25 -0
  120. package/dist/tools/search-stops.js.map +1 -0
  121. package/dist/tools/test-support.js +93 -0
  122. package/dist/tools/test-support.js.map +1 -0
  123. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emil Soleymani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # go-planning-mcp
2
+
3
+ An MCP server that wraps the Metrolinx GO Transit Open Data API, letting LLMs plan trips, check schedules, and query live departures through natural conversation. Self-hostable via Docker or deployable to Vercel in minutes.
4
+
5
+ ## Getting Started
6
+
7
+ > Work in progress — implementation coming soon.
8
+
9
+ ## Running your own server
10
+
11
+ Transport split: **Claude Desktop → stdio; everything else → Streamable HTTP.**
12
+
13
+ ### 1. Get a Metrolinx API key
14
+
15
+ Register at the [Metrolinx Open Data API registration form](https://api.openmetrolinx.com/OpenDataAPI/Help/Registration/en). It's free, but approval is manual and can take up to 10 business days.
16
+
17
+ ### 2. `docker run` one-liner
18
+
19
+ ```bash
20
+ docker run -e METROLINX_API_KEY=xxx -p 3000:3000 ghcr.io/emilsoleymani/go-planning-mcp
21
+ ```
22
+
23
+ This serves the MCP endpoint at `http://localhost:3000/mcp` and a liveness probe at `http://localhost:3000/health`.
24
+
25
+ ### 3. Compose quick start
26
+
27
+ ```bash
28
+ git clone https://github.com/EmilSoleymani/go-planning-mcp.git
29
+ cd go-planning-mcp
30
+ cp .env.example .env # then add your METROLINX_API_KEY
31
+ docker compose up
32
+ ```
33
+
34
+ ### 4. Claude Desktop (stdio)
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "go-transit": {
40
+ "command": "npx",
41
+ "args": ["go-transit-mcp"],
42
+ "env": {
43
+ "METROLINX_API_KEY": "your_key_here"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ From source (contributor/dev variant):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "go-transit": {
56
+ "command": "node",
57
+ "args": ["dist/entry/stdio.js"],
58
+ "env": {
59
+ "METROLINX_API_KEY": "your_key_here"
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { createServer, } from "node:http";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { cacheEnabledFromEnv } from "../metrolinx/cache.js";
5
+ import { MetrolinxHttpClient } from "../metrolinx/client.js";
6
+ import { buildServer } from "../server.js";
7
+ const apiKey = process.env.METROLINX_API_KEY;
8
+ if (!apiKey) {
9
+ console.error("METROLINX_API_KEY is required. Set it in the environment (see .env.example).");
10
+ process.exit(1);
11
+ }
12
+ const port = Number(process.env.PORT ?? 3000);
13
+ const client = new MetrolinxHttpClient({
14
+ apiKey,
15
+ cacheEnabled: cacheEnabledFromEnv(process.env.CACHE_ENABLED),
16
+ });
17
+ function methodNotAllowed(res) {
18
+ res.writeHead(405, { "content-type": "application/json" }).end(JSON.stringify({
19
+ jsonrpc: "2.0",
20
+ error: { code: -32000, message: "Method not allowed." },
21
+ id: null,
22
+ }));
23
+ }
24
+ // Pure liveness: no upstream Metrolinx call (docker-deployment spec §2).
25
+ function health(res) {
26
+ res
27
+ .writeHead(200, { "content-type": "application/json" })
28
+ .end(JSON.stringify({ status: "ok" }));
29
+ }
30
+ // One MCP server + transport per request, stateless mode
31
+ // (sessionIdGenerator: undefined) — the same pattern the SDK's stateless
32
+ // example and Vercel's mcp-handler use, so all three entry surfaces share
33
+ // buildServer() without diverging on session lifecycle.
34
+ async function handleMcp(req, res) {
35
+ if (req.method === "GET" || req.method === "DELETE") {
36
+ methodNotAllowed(res);
37
+ return;
38
+ }
39
+ const mcpServer = buildServer(client);
40
+ // Omitting sessionIdGenerator (rather than setting it to undefined) means
41
+ // stateless mode without tripping exactOptionalPropertyTypes.
42
+ const transport = new StreamableHTTPServerTransport({});
43
+ res.on("close", () => {
44
+ void transport.close();
45
+ void mcpServer.close();
46
+ });
47
+ // The SDK's own Transport getters/setters are typed `T | undefined`,
48
+ // wider than the Transport interface's `T | undefined`-less optional
49
+ // fields; exactOptionalPropertyTypes rejects the structural match even
50
+ // though the class declares `implements Transport`.
51
+ await mcpServer.connect(transport);
52
+ await transport.handleRequest(req, res);
53
+ }
54
+ const httpServer = createServer((req, res) => {
55
+ const url = new URL(req.url ?? "/", "http://localhost");
56
+ if (url.pathname === "/health") {
57
+ health(res);
58
+ return;
59
+ }
60
+ if (url.pathname === "/mcp") {
61
+ handleMcp(req, res).catch((error) => {
62
+ console.error("Error handling MCP request:", error);
63
+ if (!res.headersSent) {
64
+ res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({
65
+ jsonrpc: "2.0",
66
+ error: { code: -32603, message: "Internal server error" },
67
+ id: null,
68
+ }));
69
+ }
70
+ });
71
+ return;
72
+ }
73
+ res.writeHead(404).end();
74
+ });
75
+ httpServer.listen(port, () => {
76
+ console.error(`go-transit-mcp listening on port ${String(port)}`);
77
+ });
78
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/entry/http.ts"],"names":[],"mappings":";AACA,OAAO,EACL,YAAY,GAGb,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAGnG,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;AAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;IACZ,OAAO,CAAC,KAAK,CACX,8EAA8E,CAC/E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAC9C,MAAM,MAAM,GAAG,IAAI,mBAAmB,CAAC;IACrC,MAAM;IACN,YAAY,EAAE,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;CAC7D,CAAC,CAAC;AAEH,SAAS,gBAAgB,CAAC,GAAmB;IAC3C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,GAAG,CAC5D,IAAI,CAAC,SAAS,CAAC;QACb,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,EAAE;QACvD,EAAE,EAAE,IAAI;KACT,CAAC,CACH,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,SAAS,MAAM,CAAC,GAAmB;IACjC,GAAG;SACA,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;SACtD,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,yDAAyD;AACzD,yEAAyE;AACzE,0EAA0E;AAC1E,wDAAwD;AACxD,KAAK,UAAU,SAAS,CACtB,GAAoB,EACpB,GAAmB;IAEnB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACpD,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,0EAA0E;IAC1E,8DAA8D;IAC9D,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,EAAE,CAAC,CAAC;IACxD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACnB,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC,CAAC,CAAC;IACH,qEAAqE;IACrE,qEAAqE;IACrE,uEAAuE;IACvE,oDAAoD;IACpD,MAAM,SAAS,CAAC,OAAO,CAAC,SAAsB,CAAC,CAAC;IAChD,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAExD,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,CAAC;QACZ,OAAO;IACT,CAAC;IAED,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC5B,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YAC3C,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;YACpD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC,GAAG,CAC5D,IAAI,CAAC,SAAS,CAAC;oBACb,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE;oBACzD,EAAE,EAAE,IAAI;iBACT,CAAC,CACH,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;AAC3B,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IAC3B,OAAO,CAAC,KAAK,CAAC,oCAAoC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACpE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { cacheEnabledFromEnv } from "../metrolinx/cache.js";
4
+ import { MetrolinxHttpClient } from "../metrolinx/client.js";
5
+ import { buildServer } from "../server.js";
6
+ const apiKey = process.env.METROLINX_API_KEY;
7
+ if (!apiKey) {
8
+ console.error("METROLINX_API_KEY is required. Set it in the environment (see .env.example).");
9
+ process.exit(1);
10
+ }
11
+ const server = buildServer(new MetrolinxHttpClient({
12
+ apiKey,
13
+ cacheEnabled: cacheEnabledFromEnv(process.env.CACHE_ENABLED),
14
+ }));
15
+ await server.connect(new StdioServerTransport());
16
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/entry/stdio.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;AAC7C,IAAI,CAAC,MAAM,EAAE,CAAC;IACZ,OAAO,CAAC,KAAK,CACX,8EAA8E,CAC/E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,WAAW,CACxB,IAAI,mBAAmB,CAAC;IACtB,MAAM;IACN,YAAY,EAAE,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;CAC7D,CAAC,CACH,CAAC;AACF,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC"}
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * An operational failure surfaced to the LLM as an in-result error
3
+ * (tool-schemas spec §1.5). Messages are instructions for the model,
4
+ * not diagnostics.
5
+ */
6
+ export class MetrolinxError extends Error {
7
+ code;
8
+ retryable;
9
+ constructor(code, message, retryable) {
10
+ super(message);
11
+ this.name = "MetrolinxError";
12
+ this.code = code;
13
+ this.retryable = retryable;
14
+ }
15
+ }
16
+ export function toToolErrorResult(error) {
17
+ const payload = {
18
+ error: {
19
+ code: error.code,
20
+ message: error.message,
21
+ retryable: error.retryable,
22
+ },
23
+ };
24
+ return {
25
+ isError: true,
26
+ content: [{ type: "text", text: JSON.stringify(payload) }],
27
+ structuredContent: payload,
28
+ };
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAqB;IACzB,SAAS,CAAU;IAE5B,YAAY,IAAwB,EAAE,OAAe,EAAE,SAAkB;QACvE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;CACF;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAqB;IACrD,MAAM,OAAO,GAAG;QACd,KAAK,EAAE;YACL,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,SAAS,EAAE,KAAK,CAAC,SAAS;SAC3B;KACF,CAAC;IACF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1D,iBAAiB,EAAE,OAAO;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ // In-process, best-effort TTL cache (caching spec, ticket 005). No
2
+ // dependencies, no invalidation beyond time — full benefit on the
3
+ // long-lived Docker process, degrades to a no-op across cold Vercel
4
+ // invocations.
5
+ export class TtlCache {
6
+ enabled;
7
+ store = new Map();
8
+ constructor(options = {}) {
9
+ this.enabled = options.enabled ?? true;
10
+ }
11
+ async getOrFetch(key, ttlMs, fetcher) {
12
+ if (!this.enabled)
13
+ return fetcher();
14
+ const hit = this.store.get(key);
15
+ const now = Date.now();
16
+ if (hit && hit.expiresAt > now)
17
+ return hit.value;
18
+ const value = await fetcher();
19
+ this.store.set(key, { value, expiresAt: now + ttlMs });
20
+ return value;
21
+ }
22
+ }
23
+ /** CACHE_ENABLED env var: disabled only on an explicit "false"/"0"; default on. */
24
+ export function cacheEnabledFromEnv(value) {
25
+ if (value === undefined)
26
+ return true;
27
+ return !["false", "0"].includes(value.trim().toLowerCase());
28
+ }
29
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/metrolinx/cache.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,kEAAkE;AAClE,oEAAoE;AACpE,eAAe;AAWf,MAAM,OAAO,QAAQ;IACF,OAAO,CAAU;IACjB,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAE3D,YAAY,UAAwB,EAAE;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,UAAU,CACd,GAAW,EACX,KAAa,EACb,OAAyB;QAEzB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,OAAO,EAAE,CAAC;QAEpC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,IAAI,GAAG,CAAC,SAAS,GAAG,GAAG;YAAE,OAAO,GAAG,CAAC,KAAU,CAAC;QAEtD,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IAC3D,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,180 @@
1
+ import { MetrolinxError } from "../errors.js";
2
+ import { TtlCache } from "./cache.js";
3
+ const DEFAULT_BASE_URL = "https://api.openmetrolinx.com/OpenDataAPI/api/V1";
4
+ const MAX_RETRIES = 2;
5
+ const BACKOFF_BASE_MS = 500;
6
+ const BACKOFF_CAP_MS = 5000;
7
+ // Caching spec (ticket 005): stops are slow-changing (24h); fares and
8
+ // published schedules are effectively static once a service day is
9
+ // published (6h); next-service, destinations, and fleet consist (live
10
+ // physical makeup, research handoff §2.7) are real-time and never cached.
11
+ const STOP_ALL_TTL_MS = 24 * 60 * 60 * 1000;
12
+ const FARES_TTL_MS = 6 * 60 * 60 * 1000;
13
+ const SCHEDULE_TTL_MS = 6 * 60 * 60 * 1000;
14
+ const SERVICE_GLANCE_SEGMENT = {
15
+ train: "Trains",
16
+ bus: "Buses",
17
+ upx: "UPX",
18
+ };
19
+ export class MetrolinxHttpClient {
20
+ apiKey;
21
+ baseUrl;
22
+ sleep;
23
+ cache;
24
+ constructor(options) {
25
+ this.apiKey = options.apiKey;
26
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
27
+ this.sleep =
28
+ options.sleep ??
29
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
30
+ this.cache = new TtlCache({ enabled: options.cacheEnabled ?? true });
31
+ }
32
+ async getStopDetails(stopCode) {
33
+ return this.get(`/Stop/Details/${encodeURIComponent(stopCode)}`);
34
+ }
35
+ async getStopAll() {
36
+ return this.cache.getOrFetch("stop-all", STOP_ALL_TTL_MS, () => this.get("/Stop/All"));
37
+ }
38
+ async getNextService(stopCode) {
39
+ return this.get(`/Stop/NextService/${encodeURIComponent(stopCode)}`);
40
+ }
41
+ async getStopDestinations(stopCode, fromTimeWire, toTimeWire) {
42
+ return this.get(`/Stop/Destinations/${encodeURIComponent(stopCode)}/${fromTimeWire}/${toTimeWire}`);
43
+ }
44
+ async getFares(fromStopCode, toStopCode, dateWire) {
45
+ const from = encodeURIComponent(fromStopCode);
46
+ const to = encodeURIComponent(toStopCode);
47
+ const path = dateWire
48
+ ? `/Fares/${from}/${to}/${dateWire}`
49
+ : `/Fares/${from}/${to}`;
50
+ return this.cache.getOrFetch(`fares:${fromStopCode}:${toStopCode}:${dateWire ?? ""}`, FARES_TTL_MS, () => this.get(path));
51
+ }
52
+ async getFleetConsistAll() {
53
+ return this.get("/Fleet/Consist/All");
54
+ }
55
+ async getFleetConsistByEngine(engineNumber) {
56
+ return this.get(`/Fleet/Consist/Engine/${encodeURIComponent(engineNumber)}`);
57
+ }
58
+ // Alerts change through the day and have no caching tier assigned by the
59
+ // caching spec (ticket 005 only names stops/schedules/fares) — left
60
+ // uncached rather than guessing a TTL.
61
+ async getServiceAlerts() {
62
+ return this.get("/ServiceUpdate/ServiceAlert/All");
63
+ }
64
+ async getInformationAlerts() {
65
+ return this.get("/ServiceUpdate/InformationAlert/All");
66
+ }
67
+ async getMarketingAlerts() {
68
+ return this.get("/ServiceUpdate/MarketingAlert/All");
69
+ }
70
+ async getUnionDepartures() {
71
+ return this.get("/ServiceUpdate/UnionDepartures/All");
72
+ }
73
+ async getServiceExceptions(mode) {
74
+ const segment = mode === "train" ? "Train" : mode === "bus" ? "Bus" : "All";
75
+ return this.get(`/ServiceUpdate/Exceptions/${segment}`);
76
+ }
77
+ async getServiceGuarantee(tripNumber, dateWire) {
78
+ return this.get(`/ServiceUpdate/ServiceGuarantee/${encodeURIComponent(tripNumber)}/${dateWire}`);
79
+ }
80
+ async getLineAll(dateWire) {
81
+ return this.cache.getOrFetch(`line-all:${dateWire}`, SCHEDULE_TTL_MS, () => this.get(`/Schedule/Line/All/${dateWire}`));
82
+ }
83
+ async getLineSchedule(dateWire, lineCode, direction) {
84
+ return this.cache.getOrFetch(`line-schedule:${dateWire}:${lineCode}:${direction}`, SCHEDULE_TTL_MS, () => this.get(`/Schedule/Line/${dateWire}/${encodeURIComponent(lineCode)}/${encodeURIComponent(direction)}`));
85
+ }
86
+ async getTripStatus(dateWire, tripNumber) {
87
+ return this.get(`/Schedule/Trip/${dateWire}/${encodeURIComponent(tripNumber)}`);
88
+ }
89
+ // Real-time, never cached (caching spec, ticket 005).
90
+ async getServiceGlance(mode) {
91
+ return this.get(`/ServiceataGlance/${SERVICE_GLANCE_SEGMENT[mode]}/All`);
92
+ }
93
+ // Plain feed, same section as getTripUpdates below. NOT
94
+ // Fleet/Occupancy/GtfsRT/Feed/VehiclePosition: that Fleet-branded twin is
95
+ // documented (research handoff §2.7) as where occupancy_percentage is
96
+ // actually populated, but it empirically returns a genuine HTTP 401 for a
97
+ // standard registered key (confirmed live, issue #11/PR #26) — see
98
+ // docs/spec/tool-schemas.md §5. occupancy_percent is consequently
99
+ // expected to be absent in practice; kept optional in the DTO rather than
100
+ // dropped, so it starts populating automatically if that ever changes.
101
+ async getVehiclePositions() {
102
+ return this.get("/Gtfs/Feed/VehiclePosition");
103
+ }
104
+ async getTripUpdates() {
105
+ return this.get("/Gtfs/Feed/TripUpdates");
106
+ }
107
+ // Journey plans are real-time (caching spec, ticket 005) — never cached.
108
+ async getJourney(dateWire, fromStopCode, toStopCode, startTimeWire, maxJourneys) {
109
+ return this.get(`/Schedule/Journey/${dateWire}/${encodeURIComponent(fromStopCode)}/${encodeURIComponent(toStopCode)}/${startTimeWire}/${String(maxJourneys)}`);
110
+ }
111
+ // ADR 0001: at most 2 retries, only on retryable failures — network errors
112
+ // and 5xx, including 5xx tunneled through HTTP 200 bodies. 429 is never
113
+ // retried; it surfaces to the LLM immediately.
114
+ async get(path) {
115
+ for (let attempt = 0;; attempt += 1) {
116
+ try {
117
+ return await this.requestOnce(path);
118
+ }
119
+ catch (error) {
120
+ if (error instanceof MetrolinxError &&
121
+ error.retryable &&
122
+ attempt < MAX_RETRIES) {
123
+ await this.sleep(fullJitterDelay(attempt));
124
+ continue;
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ }
130
+ async requestOnce(path) {
131
+ const url = new URL(`${this.baseUrl}${path}`);
132
+ url.searchParams.set("key", this.apiKey);
133
+ let response;
134
+ try {
135
+ response = await fetch(url, { headers: { Accept: "application/json" } });
136
+ }
137
+ catch {
138
+ throw new MetrolinxError("upstream_unavailable", "Metrolinx API unreachable; try again later.", true);
139
+ }
140
+ if (response.status === 429)
141
+ throw rateLimitedError();
142
+ if (response.status >= 500) {
143
+ throw new MetrolinxError("upstream_unavailable", "Metrolinx API unreachable; try again later.", true);
144
+ }
145
+ if (!response.ok) {
146
+ throw new MetrolinxError("upstream_error", `Metrolinx API returned HTTP ${String(response.status)}.`, false);
147
+ }
148
+ const body = (await response.json());
149
+ // Metrolinx tunnels failures through HTTP 200 bodies — the status line
150
+ // lies; only Metadata.ErrorCode tells the truth (ticket 001). "204" is
151
+ // not a failure despite the tunneling mechanism: confirmed live
152
+ // (issue #7) for an unknown stop code across Stop/Details,
153
+ // Stop/NextService, and Stop/Destinations — ErrorMessage "No Content",
154
+ // data field null. Treated as a passthrough so each tool's own
155
+ // null-check produces `not_found` instead of a generic upstream error.
156
+ const tunneled = body.Metadata?.ErrorCode;
157
+ if (tunneled && tunneled !== "200" && tunneled !== "204") {
158
+ throw tunneledError(tunneled, body.Metadata?.ErrorMessage ?? undefined);
159
+ }
160
+ return body;
161
+ }
162
+ }
163
+ function rateLimitedError() {
164
+ return new MetrolinxError("rate_limited", "Metrolinx rate limit hit. Do not retry now; tell the user real-time data is temporarily unavailable.", false);
165
+ }
166
+ function tunneledError(code, message) {
167
+ if (code === "429")
168
+ return rateLimitedError();
169
+ if (code === "401") {
170
+ return new MetrolinxError("upstream_auth_failed", "Server misconfigured; operator must check METROLINX_API_KEY. Do not retry.", false);
171
+ }
172
+ if (code.startsWith("5")) {
173
+ return new MetrolinxError("upstream_unavailable", "Metrolinx API unreachable; try again later.", true);
174
+ }
175
+ return new MetrolinxError("upstream_error", message ?? `Metrolinx reported error ${code}.`, false);
176
+ }
177
+ function fullJitterDelay(attempt) {
178
+ return (Math.random() * Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt));
179
+ }
180
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/metrolinx/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAqBtC,MAAM,gBAAgB,GAAG,kDAAkD,CAAC;AAC5E,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,eAAe,GAAG,GAAG,CAAC;AAC5B,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,sEAAsE;AACtE,mEAAmE;AACnE,sEAAsE;AACtE,0EAA0E;AAC1E,MAAM,eAAe,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC5C,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AACxC,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAI3C,MAAM,sBAAsB,GAAsC;IAChE,KAAK,EAAE,QAAQ;IACf,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,KAAK;CACX,CAAC;AAyEF,MAAM,OAAO,mBAAmB;IACb,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,KAAK,CAAgC;IACrC,KAAK,CAAW;IAEjC,YAAY,OAAmC;QAC7C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;QACnD,IAAI,CAAC,KAAK;YACR,OAAO,CAAC,KAAK;gBACb,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,KAAK,GAAG,IAAI,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,IAAI,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,QAAgB;QACnC,OAAO,IAAI,CAAC,GAAG,CACb,iBAAiB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAChD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,CAC7D,IAAI,CAAC,GAAG,CAAqB,WAAW,CAAC,CAC1C,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,QAAgB;QACnC,OAAO,IAAI,CAAC,GAAG,CACb,qBAAqB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CACpD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,mBAAmB,CACvB,QAAgB,EAChB,YAAoB,EACpB,UAAkB;QAElB,OAAO,IAAI,CAAC,GAAG,CACb,sBAAsB,kBAAkB,CAAC,QAAQ,CAAC,IAAI,YAAY,IAAI,UAAU,EAAE,CACnF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,QAAQ,CACZ,YAAoB,EACpB,UAAkB,EAClB,QAAiB;QAEjB,MAAM,IAAI,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;QAC9C,MAAM,EAAE,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,QAAQ;YACnB,CAAC,CAAC,UAAU,IAAI,IAAI,EAAE,IAAI,QAAQ,EAAE;YACpC,CAAC,CAAC,UAAU,IAAI,IAAI,EAAE,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAC1B,SAAS,YAAY,IAAI,UAAU,IAAI,QAAQ,IAAI,EAAE,EAAE,EACvD,YAAY,EACZ,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAmB,IAAI,CAAC,CACvC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,GAAG,CAA0B,oBAAoB,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,uBAAuB,CAC3B,YAAoB;QAEpB,OAAO,IAAI,CAAC,GAAG,CACb,yBAAyB,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAC5D,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,oEAAoE;IACpE,uCAAuC;IACvC,KAAK,CAAC,gBAAgB;QACpB,OAAO,IAAI,CAAC,GAAG,CAAoB,iCAAiC,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,oBAAoB;QACxB,OAAO,IAAI,CAAC,GAAG,CAAoB,qCAAqC,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,GAAG,CAAoB,mCAAmC,CAAC,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,GAAG,CACb,oCAAoC,CACrC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,oBAAoB,CACxB,IAA6B;QAE7B,MAAM,OAAO,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5E,OAAO,IAAI,CAAC,GAAG,CACb,6BAA6B,OAAO,EAAE,CACvC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,mBAAmB,CACvB,UAAkB,EAClB,QAAgB;QAEhB,OAAO,IAAI,CAAC,GAAG,CACb,mCAAmC,kBAAkB,CAAC,UAAU,CAAC,IAAI,QAAQ,EAAE,CAChF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAgB;QAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,YAAY,QAAQ,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,CACzE,IAAI,CAAC,GAAG,CAAqB,sBAAsB,QAAQ,EAAE,CAAC,CAC/D,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,QAAgB,EAChB,QAAgB,EAChB,SAAiB;QAEjB,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAC1B,iBAAiB,QAAQ,IAAI,QAAQ,IAAI,SAAS,EAAE,EACpD,eAAe,EACf,GAAG,EAAE,CACH,IAAI,CAAC,GAAG,CACN,kBAAkB,QAAQ,IAAI,kBAAkB,CAAC,QAAQ,CAAC,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAC9F,CACJ,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,QAAgB,EAChB,UAAkB;QAElB,OAAO,IAAI,CAAC,GAAG,CACb,kBAAkB,QAAQ,IAAI,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAC/D,CAAC;IACJ,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,gBAAgB,CACpB,IAAuB;QAEvB,OAAO,IAAI,CAAC,GAAG,CACb,qBAAqB,sBAAsB,CAAC,IAAI,CAAC,MAAM,CACxD,CAAC;IACJ,CAAC;IAED,wDAAwD;IACxD,0EAA0E;IAC1E,sEAAsE;IACtE,0EAA0E;IAC1E,mEAAmE;IACnE,kEAAkE;IAClE,0EAA0E;IAC1E,uEAAuE;IACvE,KAAK,CAAC,mBAAmB;QACvB,OAAO,IAAI,CAAC,GAAG,CACb,4BAA4B,CAC7B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,OAAO,IAAI,CAAC,GAAG,CAA6B,wBAAwB,CAAC,CAAC;IACxE,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,UAAU,CACd,QAAgB,EAChB,YAAoB,EACpB,UAAkB,EAClB,aAAqB,EACrB,WAAmB;QAEnB,OAAO,IAAI,CAAC,GAAG,CACb,qBAAqB,QAAQ,IAAI,kBAAkB,CAAC,YAAY,CAAC,IAAI,kBAAkB,CAAC,UAAU,CAAC,IAAI,aAAa,IAAI,MAAM,CAAC,WAAW,CAAC,EAAE,CAC9I,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,wEAAwE;IACxE,+CAA+C;IACvC,KAAK,CAAC,GAAG,CAAwB,IAAY;QACnD,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,WAAW,CAAI,IAAI,CAAC,CAAC;YACzC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IACE,KAAK,YAAY,cAAc;oBAC/B,KAAK,CAAC,SAAS;oBACf,OAAO,GAAG,WAAW,EACrB,CAAC;oBACD,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;oBAC3C,SAAS;gBACX,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAwB,IAAY;QAC3D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAEzC,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,6CAA6C,EAC7C,IAAI,CACL,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;YAAE,MAAM,gBAAgB,EAAE,CAAC;QACtD,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YAC3B,MAAM,IAAI,cAAc,CACtB,sBAAsB,EACtB,6CAA6C,EAC7C,IAAI,CACL,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,cAAc,CACtB,gBAAgB,EAChB,+BAA+B,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,EACzD,KAAK,CACN,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;QAC1C,uEAAuE;QACvE,uEAAuE;QACvE,gEAAgE;QAChE,2DAA2D;QAC3D,uEAAuE;QACvE,+DAA+D;QAC/D,uEAAuE;QACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC;QAC1C,IAAI,QAAQ,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;YACzD,MAAM,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,IAAI,SAAS,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAED,SAAS,gBAAgB;IACvB,OAAO,IAAI,cAAc,CACvB,cAAc,EACd,sGAAsG,EACtG,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,OAAgB;IACnD,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,gBAAgB,EAAE,CAAC;IAC9C,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,IAAI,cAAc,CACvB,sBAAsB,EACtB,4EAA4E,EAC5E,KAAK,CACN,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,cAAc,CACvB,sBAAsB,EACtB,6CAA6C,EAC7C,IAAI,CACL,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,cAAc,CACvB,gBAAgB,EAChB,OAAO,IAAI,4BAA4B,IAAI,GAAG,EAC9C,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,CACL,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,eAAe,GAAG,CAAC,IAAI,OAAO,CAAC,CACzE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ // Raw Metrolinx response shapes, PascalCase exactly as delivered upstream.
2
+ // Confirmed against live captures by scripts/capture-fixtures.ts (issue #3) —
3
+ // see test/fixtures/ for the underlying samples.
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/metrolinx/types.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,8EAA8E;AAC9E,iDAAiD"}
@@ -0,0 +1,44 @@
1
+ import { MetrolinxError } from "../errors.js";
2
+ const RIDER_BY_CATEGORY = {
3
+ Adult: "adult",
4
+ Student: "student",
5
+ Senior: "senior",
6
+ Child: "child",
7
+ // "Group Pass" (live-confirmed, issue #3) is a bulk purchase, not a rider
8
+ // category — it has no slot in the closed rider enum and is dropped below.
9
+ };
10
+ function normalizeMethod(type) {
11
+ // Only "Paper"/"Presto" observed live (issue #3); best-effort default to
12
+ // "paper" for anything else, same spirit as normalizeMode's ServiceType
13
+ // fallback in normalize/next-service.ts.
14
+ return type.toLowerCase() === "presto" ? "presto" : "paper";
15
+ }
16
+ export function normalizeFares(raw) {
17
+ const categories = raw.AllFares?.FareCategory;
18
+ if (!categories) {
19
+ throw new MetrolinxError("not_found", "No fare found between those stops. Verify both codes via search_stops.", false);
20
+ }
21
+ const fares = [];
22
+ for (const category of categories) {
23
+ const rider = RIDER_BY_CATEGORY[category.Type];
24
+ if (!rider)
25
+ continue;
26
+ for (const ticket of category.Tickets ?? []) {
27
+ const method = normalizeMethod(ticket.Type);
28
+ for (const fare of ticket.Fares ?? []) {
29
+ fares.push({
30
+ rider,
31
+ method,
32
+ amount: fare.Amount,
33
+ category: fare.Category,
34
+ });
35
+ }
36
+ }
37
+ }
38
+ return {
39
+ fares,
40
+ truncated: false,
41
+ total_matched: fares.length,
42
+ };
43
+ }
44
+ //# sourceMappingURL=fares.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fares.js","sourceRoot":"","sources":["../../src/normalize/fares.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAI9C,MAAM,iBAAiB,GAAkC;IACvD,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,0EAA0E;IAC1E,2EAA2E;CAC5E,CAAC;AAEF,SAAS,eAAe,CAAC,IAAY;IACnC,yEAAyE;IACzE,wEAAwE;IACxE,yCAAyC;IACzC,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAqB;IAClD,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC;IAC9C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,cAAc,CACtB,WAAW,EACX,wEAAwE,EACxE,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YAC5C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC5C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBACtC,KAAK,CAAC,IAAI,CAAC;oBACT,KAAK;oBACL,MAAM;oBACN,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;iBACxB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,KAAK,CAAC,MAAM;KAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /** The consist currently assigned to a trip, found by scanning each consist's remaining-trip list. */
2
+ export function findConsistByTrip(raw, tripNumber) {
3
+ const consists = raw.AllConsists?.Consists ?? [];
4
+ return consists.find((consist) => (consist.RemainingTrip ?? []).some((trip) => trip.Number === tripNumber));
5
+ }
6
+ /** Fleet/Consist/Engine/{EngineNumber} returns the AllConsists shape filtered to one engine. */
7
+ export function firstConsist(raw) {
8
+ return raw.AllConsists?.Consists?.[0];
9
+ }
10
+ export function normalizeFleetConsist(consist) {
11
+ return {
12
+ engine_number: consist.EngineNumber,
13
+ coach_count: consist.CoachCount,
14
+ cars: (consist.Lineup ?? []).map((car) => ({
15
+ type: car.Type,
16
+ order: car.Order,
17
+ number: car.Number,
18
+ })),
19
+ // start_time/end_time formats aren't live-confirmed (no Fleet access this
20
+ // session, see metrolinx/types.ts) — passed through as opaque strings
21
+ // rather than guessed at, same treatment as get_trip_status's status
22
+ // fields (issue #8/#22).
23
+ remaining_trips: (consist.RemainingTrip ?? []).map((trip) => ({
24
+ trip_number: trip.Number,
25
+ line: trip.Corridor,
26
+ start_time: trip.StartTime,
27
+ end_time: trip.EndTime,
28
+ })),
29
+ };
30
+ }
31
+ //# sourceMappingURL=fleet-consist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-consist.js","sourceRoot":"","sources":["../../src/normalize/fleet-consist.ts"],"names":[],"mappings":"AAMA,sGAAsG;AACtG,MAAM,UAAU,iBAAiB,CAC/B,GAA4B,EAC5B,UAAkB;IAElB,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,QAAQ,IAAI,EAAE,CAAC;IACjD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC/B,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,CACzE,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,YAAY,CAC1B,GAA4B;IAE5B,OAAO,GAAG,CAAC,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAmB;IACvD,OAAO;QACL,aAAa,EAAE,OAAO,CAAC,YAAY;QACnC,WAAW,EAAE,OAAO,CAAC,UAAU;QAC/B,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACzC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC,CAAC;QACH,0EAA0E;QAC1E,sEAAsE;QACtE,qEAAqE;QACrE,yBAAyB;QACzB,eAAe,EAAE,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC5D,WAAW,EAAE,IAAI,CAAC,MAAM;YACxB,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,UAAU,EAAE,IAAI,CAAC,SAAS;YAC1B,QAAQ,EAAE,IAAI,CAAC,OAAO;SACvB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC"}