@openephemeris/mcp-server 4.6.0 → 4.7.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.
- package/CHANGELOG.md +38 -0
- package/dist/prompts.js +3 -2
- package/dist/tools/apps/bodygraph-app.js +8 -7
- package/dist/tools/apps/location-tools.js +67 -5
- package/dist/tools/apps/moon-phase-app.js +3 -2
- package/dist/tools/apps/vedic-chart-app.js +3 -3
- package/dist/tools/datetime-historical.d.ts +19 -0
- package/dist/tools/datetime-historical.js +77 -0
- package/dist/tools/specialized/acg.js +4 -3
- package/dist/tools/specialized/hd_bodygraph.js +12 -3
- package/dist/tools/specialized/hd_cycles.js +19 -3
- package/dist/tools/specialized/hd_group.js +31 -6
- package/dist/tools/specialized/human_design.js +3 -2
- package/dist/tools/specialized/moon.js +3 -2
- package/dist/tools/specialized/vedic.js +3 -2
- package/dist/ui/bazi.html +30 -30
- package/dist/ui/bi-wheel.html +35 -35
- package/dist/ui/bodygraph.html +39 -39
- package/dist/ui/chart-wheel.html +37 -37
- package/dist/ui/moon-phase.html +32 -32
- package/dist/ui/transit-timeline.html +30 -30
- package/dist/ui/vedic-chart.html +35 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.7.0] — 2026-08-02
|
|
11
|
+
|
|
12
|
+
Historically correct birth-time conversion. IANA timezone databases — including
|
|
13
|
+
the one inside every JavaScript runtime — are only authoritative from 1970.
|
|
14
|
+
Before the US Uniform Time Act took effect in 1967, daylight saving was state
|
|
15
|
+
and municipal law, and a zone like `America/Chicago` models Chicago alone. A
|
|
16
|
+
1961 Minnesota birth converted with standard timezone math lands an hour off,
|
|
17
|
+
which flips the Ascendant sign and the Human Design design-Moon gate.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Pre-1970 births now resolve through the API's historical correction
|
|
22
|
+
overlay.** Any tool given a naive local birth time with a timezone and
|
|
23
|
+
coordinates — natal, Human Design (chart, composite, penta, cycles, groups),
|
|
24
|
+
Vedic, BaZi apps, ACG, moon tools, and the embedded chart apps — routes the
|
|
25
|
+
local→UTC conversion through the API's `/timezone/offset` `datetime_local`
|
|
26
|
+
mode, which applies primary-source-cited state and local law (with a
|
|
27
|
+
`tz_confidence` grade and rule citation) instead of assuming the reference
|
|
28
|
+
city's rules. Post-1970 conversions stay on the local, zero-latency path,
|
|
29
|
+
which is exactly as authoritative as the server. If the server is
|
|
30
|
+
unreachable, tools fall back to the previous local conversion rather than
|
|
31
|
+
failing the chart call.
|
|
32
|
+
- **`timezone_resolve`** consults the same historical path for pre-1970 dates
|
|
33
|
+
and reports `tzConfidence`, `tzRuleSource`, and `datetimeStatus` (ambiguous
|
|
34
|
+
DST-fold and nonexistent-gap times are called out explicitly).
|
|
35
|
+
- **`location_search`** tags each suggestion's historical offset with a
|
|
36
|
+
confidence grade and a note when a pre-1970 date may be affected by
|
|
37
|
+
state/local law divergence.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Three skills and one prompt still instructed the model to convert local
|
|
42
|
+
birth times to UTC itself with generic timezone math — the exact manual
|
|
43
|
+
conversion the historical overlay exists to prevent. They now direct all
|
|
44
|
+
conversion through the API.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
10
48
|
## [4.6.0] — 2026-07-30
|
|
11
49
|
|
|
12
50
|
Fixes the tool that could not answer the question it is named for, closes the
|
package/dist/prompts.js
CHANGED
|
@@ -377,8 +377,9 @@ export const PROMPTS = [
|
|
|
377
377
|
"even 10–15 minutes can shift the Type, Authority, or Profile. If truly unknown, " +
|
|
378
378
|
"proceed with solar noon but flag that results may be imprecise.\n" +
|
|
379
379
|
"4. **Birth city and country** — needed for timezone conversion\n\n" +
|
|
380
|
-
"## Step 2 —
|
|
381
|
-
"Human Design
|
|
380
|
+
"## Step 2 — Resolve Coordinates and Timezone\n" +
|
|
381
|
+
"Human Design needs the exact birth instant; the tool resolves it from local time + " +
|
|
382
|
+
"timezone + coordinates automatically (historically correct even pre-1970).\n\n" +
|
|
382
383
|
"**Resolve the birth place with `location_search`** — pass `query: 'Birth City, Country'` " +
|
|
383
384
|
"and `date` set to the birth date (`YYYY-MM-DD`). One call returns decimal lat/lon, the " +
|
|
384
385
|
"IANA timezone, and `utcOffsetAtDate` — the offset that actually applied at that place on " +
|
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
26
26
|
import { getActiveClient } from "../../backend/client.js";
|
|
27
27
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
28
|
-
import {
|
|
28
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
29
29
|
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
30
30
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
31
31
|
export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
|
|
@@ -372,9 +372,9 @@ registerTool({
|
|
|
372
372
|
location: args.location,
|
|
373
373
|
});
|
|
374
374
|
const timezone = args.timezone ?? resolved.timezone;
|
|
375
|
-
const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
376
375
|
const lat = resolved.latitude;
|
|
377
376
|
const lon = resolved.longitude;
|
|
377
|
+
const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
|
|
378
378
|
if (lat == null || lon == null) {
|
|
379
379
|
throw new Error("explore_human_design requires either `latitude` + `longitude` or a resolvable `location` name. " +
|
|
380
380
|
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
@@ -473,10 +473,11 @@ registerTool({
|
|
|
473
473
|
handler: async (args) => {
|
|
474
474
|
const client = getActiveClient();
|
|
475
475
|
const timezone = args.timezone;
|
|
476
|
-
// Convert local-time input → UTC using IANA timezone when provided
|
|
477
|
-
const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
478
476
|
const lat = args.latitude;
|
|
479
477
|
const lon = args.longitude;
|
|
478
|
+
// Convert local-time input → UTC using IANA timezone when provided;
|
|
479
|
+
// pre-1970 births route through the API's historical correction overlay.
|
|
480
|
+
const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
|
|
480
481
|
const theme = args.theme === "light" ? "light" : "dark";
|
|
481
482
|
const layout = args.layout === "mandala" ? "mandala" : undefined;
|
|
482
483
|
const rings = typeof args.rings === "string" ? args.rings : undefined;
|
|
@@ -1365,9 +1366,9 @@ registerTool({
|
|
|
1365
1366
|
location: args.location,
|
|
1366
1367
|
});
|
|
1367
1368
|
const timezone = args.timezone ?? resolved.timezone;
|
|
1368
|
-
const natalIso = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
1369
1369
|
const lat = resolved.latitude;
|
|
1370
1370
|
const lon = resolved.longitude;
|
|
1371
|
+
const natalIso = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
|
|
1371
1372
|
if (lat == null || lon == null) {
|
|
1372
1373
|
throw new Error("explore_human_design_transit requires either `latitude` + `longitude` or a resolvable `location` name. " +
|
|
1373
1374
|
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
@@ -1389,7 +1390,7 @@ registerTool({
|
|
|
1389
1390
|
};
|
|
1390
1391
|
if (args.transit_datetime) {
|
|
1391
1392
|
body.transit_datetime = {
|
|
1392
|
-
iso:
|
|
1393
|
+
iso: await localToUtcIsoHistorical("transit_datetime", String(args.transit_datetime), args.timezone, { latitude: lat, longitude: lon }),
|
|
1393
1394
|
};
|
|
1394
1395
|
}
|
|
1395
1396
|
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
@@ -1520,7 +1521,7 @@ registerTool({
|
|
|
1520
1521
|
"Human Design is sensitive to birth location; a chart at 0°N 0°E is silently wrong.");
|
|
1521
1522
|
}
|
|
1522
1523
|
return {
|
|
1523
|
-
birth_datetime_utc:
|
|
1524
|
+
birth_datetime_utc: await localToUtcIsoHistorical(`${label}.datetime`, String(p?.datetime), tz, { latitude: lat, longitude: lon }, `${label}.timezone`),
|
|
1524
1525
|
latitude: lat,
|
|
1525
1526
|
longitude: lon,
|
|
1526
1527
|
};
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
+
import { TZDATA_AUTHORITATIVE_FROM_YEAR } from "../datetime-historical.js";
|
|
5
|
+
/** Render minutes east of UTC as `±HH:MM`. */
|
|
6
|
+
function formatOffsetMinutes(offsetMinutes) {
|
|
7
|
+
const sign = offsetMinutes < 0 ? "-" : "+";
|
|
8
|
+
const abs = Math.abs(offsetMinutes);
|
|
9
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
10
|
+
const mm = String(Math.round(abs % 60)).padStart(2, "0");
|
|
11
|
+
return `${sign}${hh}:${mm}`;
|
|
12
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* The UTC offset of an IANA zone is a function of place AND date: 1987 US DST
|
|
6
15
|
* rules are not today's rules, and America/Chicago ran on CDT through the whole
|
|
@@ -143,6 +152,8 @@ registerTool({
|
|
|
143
152
|
// Map the API's snake_case response to the camelCase shape the app UI consumes.
|
|
144
153
|
const list = Array.isArray(raw?.suggestions) ? raw.suggestions : [];
|
|
145
154
|
const date = args.date != null && String(args.date).trim() !== "" ? String(args.date).trim() : null;
|
|
155
|
+
const dateYear = date ? Number(date.slice(0, 4)) : null;
|
|
156
|
+
const preTzdataEra = dateYear != null && Number.isFinite(dateYear) && dateYear < TZDATA_AUTHORITATIVE_FROM_YEAR;
|
|
146
157
|
const suggestions = list.map((s) => {
|
|
147
158
|
const mapped = {
|
|
148
159
|
displayName: s.display_name,
|
|
@@ -154,8 +165,17 @@ registerTool({
|
|
|
154
165
|
longitude: s.longitude,
|
|
155
166
|
timezone: s.timezone,
|
|
156
167
|
};
|
|
157
|
-
if (date && s.timezone)
|
|
158
|
-
|
|
168
|
+
if (date && s.timezone) {
|
|
169
|
+
const offset = offsetAtLocalNoon(s.timezone, date);
|
|
170
|
+
if (offset) {
|
|
171
|
+
// These offsets come from local tzdata, which is only
|
|
172
|
+
// authoritative from 1970 — before that it models the
|
|
173
|
+
// zone's reference city, not state/local law.
|
|
174
|
+
Object.assign(mapped, offset, {
|
|
175
|
+
tzConfidence: preTzdataEra ? "historical_estimate" : "authoritative",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
159
179
|
return mapped;
|
|
160
180
|
});
|
|
161
181
|
// Ambiguity signal. "portland" really does match Oregon, Maine, Texas,
|
|
@@ -180,6 +200,11 @@ registerTool({
|
|
|
180
200
|
return {
|
|
181
201
|
suggestions,
|
|
182
202
|
matchCount: suggestions.length,
|
|
203
|
+
...(preTzdataEra
|
|
204
|
+
? {
|
|
205
|
+
historicalNote: "Pre-1970 date: tzdata offsets are reference-city estimates. For the chosen place, call timezone_resolve with the same date (or submit the naive local time + timezone + coordinates to the chart endpoint) to apply the API's historical correction overlay.",
|
|
206
|
+
}
|
|
207
|
+
: {}),
|
|
183
208
|
ambiguous,
|
|
184
209
|
...(ambiguous
|
|
185
210
|
? {
|
|
@@ -203,7 +228,7 @@ registerTool({
|
|
|
203
228
|
longitude: { type: "number" },
|
|
204
229
|
date: {
|
|
205
230
|
type: "string",
|
|
206
|
-
description: "Optional birth/event date
|
|
231
|
+
description: "Optional birth/event date 'YYYY-MM-DD'. Adds utcOffsetAtDate / utcOffsetMinutes / isDst / tzConfidence. Post-1970 resolves locally (free); pre-1970 consults the API's historical correction overlay (1 extra credit).",
|
|
207
232
|
},
|
|
208
233
|
},
|
|
209
234
|
required: ["latitude", "longitude"],
|
|
@@ -228,9 +253,46 @@ registerTool({
|
|
|
228
253
|
const date = args.date != null && String(args.date).trim() !== "" ? String(args.date).trim() : null;
|
|
229
254
|
const tz = typeof result?.timezone === "string" ? result.timezone : null;
|
|
230
255
|
if (date && tz) {
|
|
256
|
+
// Pre-1970 dates: tzdata (Node's and Go's alike) models the zone's
|
|
257
|
+
// reference city only, so ask the API — its historical correction
|
|
258
|
+
// overlay knows where state law diverged (e.g. Minnesota 1959-66).
|
|
259
|
+
// Costs one extra credit, only for pre-1970 dates.
|
|
260
|
+
const year = Number(date.slice(0, 4));
|
|
261
|
+
if (Number.isFinite(year) && year < TZDATA_AUTHORITATIVE_FROM_YEAR) {
|
|
262
|
+
try {
|
|
263
|
+
const off = (await getActiveClient().request("POST", "/timezone/offset", {
|
|
264
|
+
data: { lat: args.latitude, lon: args.longitude, datetime_local: `${date.slice(0, 10)}T12:00:00` },
|
|
265
|
+
}));
|
|
266
|
+
if (typeof off?.offset_seconds === "number") {
|
|
267
|
+
const minutes = off.offset_seconds / 60;
|
|
268
|
+
return {
|
|
269
|
+
...result,
|
|
270
|
+
utcOffsetAtDate: formatOffsetMinutes(minutes),
|
|
271
|
+
utcOffsetMinutes: minutes,
|
|
272
|
+
isDst: Boolean(off.is_dst),
|
|
273
|
+
date,
|
|
274
|
+
tzConfidence: typeof off.tz_confidence === "string" ? off.tz_confidence : "historical_estimate",
|
|
275
|
+
...(typeof off.tz_rule_source === "string" ? { tzRuleSource: off.tz_rule_source } : {}),
|
|
276
|
+
...(typeof off.datetime_status === "string" ? { datetimeStatus: off.datetime_status } : {}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Server path unavailable — fall through to the local
|
|
282
|
+
// tzdata estimate below, flagged as such.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
231
285
|
const offset = offsetAtLocalNoon(tz, date);
|
|
232
|
-
if (offset)
|
|
233
|
-
return {
|
|
286
|
+
if (offset) {
|
|
287
|
+
return {
|
|
288
|
+
...result,
|
|
289
|
+
...offset,
|
|
290
|
+
date,
|
|
291
|
+
tzConfidence: Number.isFinite(year) && year < TZDATA_AUTHORITATIVE_FROM_YEAR
|
|
292
|
+
? "historical_estimate"
|
|
293
|
+
: "authoritative",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
234
296
|
}
|
|
235
297
|
return result;
|
|
236
298
|
},
|
|
@@ -17,7 +17,8 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
18
18
|
import { getActiveClient } from "../../backend/client.js";
|
|
19
19
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
20
|
-
import { TIMEZONE_PROPERTY
|
|
20
|
+
import { TIMEZONE_PROPERTY } from "../datetime.js";
|
|
21
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
21
22
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
22
23
|
export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
23
24
|
export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -98,7 +99,7 @@ async function computeMoonData(args) {
|
|
|
98
99
|
const client = getActiveClient();
|
|
99
100
|
const params = {};
|
|
100
101
|
params.datetime = args.datetime
|
|
101
|
-
?
|
|
102
|
+
? await localToUtcIsoHistorical("datetime", String(args.datetime), args.timezone, { latitude: args.latitude, longitude: args.longitude })
|
|
102
103
|
: new Date().toISOString();
|
|
103
104
|
if (args.latitude != null)
|
|
104
105
|
params.latitude = args.latitude;
|
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from "node:url";
|
|
|
23
23
|
import { registerTool, validateRequired, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
24
24
|
import { getActiveClient } from "../../backend/client.js";
|
|
25
25
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
26
|
-
import {
|
|
26
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
27
27
|
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
28
28
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
29
|
export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
|
|
@@ -165,9 +165,9 @@ registerTool({
|
|
|
165
165
|
validateCoordinates(argsWithResolved, "latitude", "longitude");
|
|
166
166
|
const client = getActiveClient();
|
|
167
167
|
const timezone = args.timezone ?? resolved.timezone;
|
|
168
|
-
const datetime = localToUtcIso("datetime", String(args.datetime), timezone);
|
|
169
168
|
const lat = Number(resolved.latitude);
|
|
170
169
|
const lon = Number(resolved.longitude);
|
|
170
|
+
const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), timezone, { latitude: lat, longitude: lon });
|
|
171
171
|
const ayanamsa = args.ayanamsa;
|
|
172
172
|
const location = String(resolved.location ?? args.location ?? `${lat}, ${lon}`).slice(0, 120);
|
|
173
173
|
const bundleAvailable = Boolean(getVedicChartBundle());
|
|
@@ -244,9 +244,9 @@ registerTool({
|
|
|
244
244
|
_meta: { ui: { resourceUri: VEDIC_CHART_RESOURCE_URI, visibility: ["app"] } },
|
|
245
245
|
handler: async (args) => {
|
|
246
246
|
const client = getActiveClient();
|
|
247
|
-
const datetime = localToUtcIso("datetime", String(args.datetime), args.timezone);
|
|
248
247
|
const lat = args.latitude != null ? Number(args.latitude) : undefined;
|
|
249
248
|
const lon = args.longitude != null ? Number(args.longitude) : undefined;
|
|
249
|
+
const datetime = await localToUtcIsoHistorical("datetime", String(args.datetime), args.timezone, { latitude: lat, longitude: lon });
|
|
250
250
|
const ayanamsa = args.ayanamsa;
|
|
251
251
|
const theme = args.theme === "light" ? "light" : "dark";
|
|
252
252
|
const body = {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Where IANA tzdata stops being reference-city best effort. */
|
|
2
|
+
export declare const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
3
|
+
export interface BirthCoords {
|
|
4
|
+
latitude?: number | null;
|
|
5
|
+
longitude?: number | null;
|
|
6
|
+
}
|
|
7
|
+
/** True when a naive local datetime needs the server-side historical path. */
|
|
8
|
+
export declare function needsHistoricalResolution(dt: string, coords?: BirthCoords): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `localToUtcIso`, but historically correct when it matters.
|
|
11
|
+
*
|
|
12
|
+
* Same contract as `localToUtcIso` (naive value with no `tz` throws the
|
|
13
|
+
* ambiguous-datetime error; zone-suffixed and date-only values pass through).
|
|
14
|
+
* For a pre-1970 naive value with known coordinates, the conversion is done
|
|
15
|
+
* by the API's `/timezone/offset` `datetime_local` mode so the historical
|
|
16
|
+
* correction overlay applies. Everything else — including every post-1970
|
|
17
|
+
* conversion — stays on the local, zero-latency Intl path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function localToUtcIsoHistorical(field: string, dt: string, tz?: string, coords?: BirthCoords, timezoneField?: string): Promise<string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historically-correct local → UTC conversion.
|
|
3
|
+
*
|
|
4
|
+
* IANA tzdata — Node's Intl and the Go engine's tzdata alike — is only
|
|
5
|
+
* authoritative from 1970. Before the US Uniform Time Act (effective 1967),
|
|
6
|
+
* DST was a state and municipal matter, and tzdata models each zone's
|
|
7
|
+
* reference city only. `localToUtcIso`'s Intl math is therefore silently an
|
|
8
|
+
* hour off for births like Robbinsdale, MN 1961-10-09 17:56 (Minnesota was on
|
|
9
|
+
* CST while America/Chicago says CDT), which flips the Ascendant sign and the
|
|
10
|
+
* HD design-Moon gate.
|
|
11
|
+
*
|
|
12
|
+
* The Go API owns the correction: `POST /timezone/offset` with
|
|
13
|
+
* `datetime_local` + coordinates resolves through its historical-correction
|
|
14
|
+
* overlay (apps/api/go-sidecar/internal/historicaltz) and returns the true
|
|
15
|
+
* instant plus a `tz_confidence` grade. This module routes the conversions
|
|
16
|
+
* that need it — pre-1970, naive, with known coordinates — through that
|
|
17
|
+
* endpoint, and leaves every other case on the local Intl path, which is
|
|
18
|
+
* exactly as authoritative as the server for 1970+.
|
|
19
|
+
*
|
|
20
|
+
* Fallback discipline: if the server call fails or resolves a different zone
|
|
21
|
+
* than the caller explicitly named, we fall back to `localToUtcIso` — the
|
|
22
|
+
* pre-existing behavior — rather than failing the chart call. Best effort
|
|
23
|
+
* beats an outage; the server remains the single authority whenever it is
|
|
24
|
+
* reachable.
|
|
25
|
+
*/
|
|
26
|
+
import { getActiveClient } from "../backend/client.js";
|
|
27
|
+
import { hasZoneSuffix, isNaiveClockTime, localToUtcIso } from "./datetime.js";
|
|
28
|
+
/** Where IANA tzdata stops being reference-city best effort. */
|
|
29
|
+
export const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
30
|
+
/** True when a naive local datetime needs the server-side historical path. */
|
|
31
|
+
export function needsHistoricalResolution(dt, coords) {
|
|
32
|
+
const value = (dt ?? "").trim();
|
|
33
|
+
if (!isNaiveClockTime(value) || hasZoneSuffix(value))
|
|
34
|
+
return false;
|
|
35
|
+
const year = Number(value.slice(0, 4));
|
|
36
|
+
return (Number.isFinite(year) &&
|
|
37
|
+
year < TZDATA_AUTHORITATIVE_FROM_YEAR &&
|
|
38
|
+
typeof coords?.latitude === "number" &&
|
|
39
|
+
typeof coords?.longitude === "number");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `localToUtcIso`, but historically correct when it matters.
|
|
43
|
+
*
|
|
44
|
+
* Same contract as `localToUtcIso` (naive value with no `tz` throws the
|
|
45
|
+
* ambiguous-datetime error; zone-suffixed and date-only values pass through).
|
|
46
|
+
* For a pre-1970 naive value with known coordinates, the conversion is done
|
|
47
|
+
* by the API's `/timezone/offset` `datetime_local` mode so the historical
|
|
48
|
+
* correction overlay applies. Everything else — including every post-1970
|
|
49
|
+
* conversion — stays on the local, zero-latency Intl path.
|
|
50
|
+
*/
|
|
51
|
+
export async function localToUtcIsoHistorical(field, dt, tz, coords, timezoneField = "timezone") {
|
|
52
|
+
const value = (dt ?? "").trim();
|
|
53
|
+
if (!needsHistoricalResolution(value, coords) || !tz || tz.trim() === "") {
|
|
54
|
+
// Includes the naive-without-tz case, which must throw the same
|
|
55
|
+
// ambiguous-datetime error localToUtcIso throws.
|
|
56
|
+
return localToUtcIso(field, dt, tz, timezoneField);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const res = (await getActiveClient().request("POST", "/timezone/offset", {
|
|
60
|
+
data: {
|
|
61
|
+
lat: coords.latitude,
|
|
62
|
+
lon: coords.longitude,
|
|
63
|
+
datetime_local: value,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
// Respect an explicit caller zone: the server resolves the zone from
|
|
67
|
+
// the coordinates, so only trust its instant when the zones agree.
|
|
68
|
+
if (res?.resolved_utc && (!res.timezone || res.timezone === tz.trim())) {
|
|
69
|
+
return res.resolved_utc;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Server unreachable or rejected the request — fall through to the
|
|
74
|
+
// local conversion rather than failing the chart call.
|
|
75
|
+
}
|
|
76
|
+
return localToUtcIso(field, dt, tz, timezoneField);
|
|
77
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
const ACG_BODY_DESCRIPTION = "List of celestial bodies for line calculation. " +
|
|
6
7
|
"E.g. ['Sun', 'Moon', 'Venus', 'Mars', 'Jupiter', 'Saturn']. " +
|
|
7
8
|
"Aliases: 'NorthNode'/'Node'/'Rahu' → MeanNode, 'SouthNode'/'Ketu' → SouthNode. " +
|
|
@@ -67,7 +68,7 @@ registerTool({
|
|
|
67
68
|
birthplace_lat: args.birth_latitude,
|
|
68
69
|
birthplace_lon: args.birth_longitude,
|
|
69
70
|
},
|
|
70
|
-
epoch:
|
|
71
|
+
epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
|
|
71
72
|
};
|
|
72
73
|
if (args.bodies)
|
|
73
74
|
body.bodies = args.bodies;
|
|
@@ -164,7 +165,7 @@ registerTool({
|
|
|
164
165
|
birthplace_lat: args.birth_latitude,
|
|
165
166
|
birthplace_lon: args.birth_longitude,
|
|
166
167
|
},
|
|
167
|
-
epoch:
|
|
168
|
+
epoch: await localToUtcIsoHistorical("birth_datetime", args.birth_datetime, args.timezone, { latitude: args.birth_latitude, longitude: args.birth_longitude }),
|
|
168
169
|
query_lat: args.query_latitude,
|
|
169
170
|
query_lon: args.query_longitude,
|
|
170
171
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "human_design_bodygraph",
|
|
7
8
|
description: "Generate a Human Design Bodygraph image (SVG) from a birth datetime (UTC). " +
|
|
@@ -12,7 +13,7 @@ registerTool({
|
|
|
12
13
|
"For a user-facing interactive bodygraph explorer, use explore_human_design instead.\n\n" +
|
|
13
14
|
"CREDIT COST: 2 credits per call.\n\n" +
|
|
14
15
|
"EXAMPLE (local birth time + zone): born 15 April 1990 at 14:30 in Chicago:\n" +
|
|
15
|
-
" datetime='1990-04-15T14:30:00', timezone='America/Chicago'\n" +
|
|
16
|
+
" datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298\n" +
|
|
16
17
|
"EXAMPLE (already in UTC):\n" +
|
|
17
18
|
" datetime='1990-04-15T19:30:00Z'",
|
|
18
19
|
inputSchema: {
|
|
@@ -24,6 +25,14 @@ registerTool({
|
|
|
24
25
|
" Human Design is time-sensitive — accuracy to the minute matters.",
|
|
25
26
|
},
|
|
26
27
|
timezone: TIMEZONE_PROPERTY,
|
|
28
|
+
latitude: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times (e.g. state-level DST deviations); has no effect on the bodygraph itself.",
|
|
31
|
+
},
|
|
32
|
+
longitude: {
|
|
33
|
+
type: "number",
|
|
34
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
35
|
+
},
|
|
27
36
|
style: {
|
|
28
37
|
type: "string",
|
|
29
38
|
enum: ["light", "dark", "mono"],
|
|
@@ -44,7 +53,7 @@ registerTool({
|
|
|
44
53
|
handler: async (args) => {
|
|
45
54
|
validateRequired(args, ["datetime"]);
|
|
46
55
|
const body = {
|
|
47
|
-
birth_datetime_utc:
|
|
56
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
48
57
|
};
|
|
49
58
|
const format = pickEnum(args.format, ["svg", "png"]) || "svg";
|
|
50
59
|
const style = pickEnum(args.style, ["light", "dark", "mono"]);
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
6
|
+
/** Optional birth coordinates, present only to feed the historical timezone
|
|
7
|
+
* correction for pre-1970 naive local birth times — not sent to the Go
|
|
8
|
+
* endpoint, which is non-relocational. */
|
|
9
|
+
const OPTIONAL_BIRTH_COORDS = {
|
|
10
|
+
latitude: {
|
|
11
|
+
type: "number",
|
|
12
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
13
|
+
},
|
|
14
|
+
longitude: {
|
|
15
|
+
type: "number",
|
|
16
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
5
19
|
registerTool({
|
|
6
20
|
name: "hd_planetary_return",
|
|
7
21
|
description: "Calculate a Human Design planetary return chart — full HD chart " +
|
|
@@ -29,6 +43,7 @@ registerTool({
|
|
|
29
43
|
description: DATETIME_DESC,
|
|
30
44
|
},
|
|
31
45
|
timezone: TIMEZONE_PROPERTY,
|
|
46
|
+
...OPTIONAL_BIRTH_COORDS,
|
|
32
47
|
return_year: {
|
|
33
48
|
type: "integer",
|
|
34
49
|
description: "Year to find the return near (e.g. 2020 for a Saturn return at ~age 29).",
|
|
@@ -49,7 +64,7 @@ registerTool({
|
|
|
49
64
|
return await getActiveClient().request("POST", "/human-design/cycles/return", {
|
|
50
65
|
data: {
|
|
51
66
|
planet: args.planet,
|
|
52
|
-
birth_datetime_utc:
|
|
67
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
53
68
|
return_year: args.return_year,
|
|
54
69
|
format: args.format,
|
|
55
70
|
include_chiron: args.include_chiron,
|
|
@@ -82,6 +97,7 @@ registerTool({
|
|
|
82
97
|
description: DATETIME_DESC,
|
|
83
98
|
},
|
|
84
99
|
timezone: TIMEZONE_PROPERTY,
|
|
100
|
+
...OPTIONAL_BIRTH_COORDS,
|
|
85
101
|
target_year: {
|
|
86
102
|
type: "integer",
|
|
87
103
|
description: "Year to search for the opposition near.",
|
|
@@ -102,7 +118,7 @@ registerTool({
|
|
|
102
118
|
return await getActiveClient().request("POST", "/human-design/cycles/opposition", {
|
|
103
119
|
data: {
|
|
104
120
|
planet: args.planet,
|
|
105
|
-
birth_datetime_utc:
|
|
121
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
106
122
|
target_year: args.target_year,
|
|
107
123
|
format: args.format,
|
|
108
124
|
include_chiron: args.include_chiron,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC,
|
|
4
|
+
import { DATETIME_DESC, timezoneProperty } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
// POST /human-design/composite — OE-027
|
|
6
7
|
registerTool({
|
|
7
8
|
name: "human_design_composite",
|
|
@@ -21,11 +22,27 @@ registerTool({
|
|
|
21
22
|
description: DATETIME_DESC,
|
|
22
23
|
},
|
|
23
24
|
person_a_timezone: timezoneProperty("Person A's birth location"),
|
|
25
|
+
person_a_latitude: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Optional Person A birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
28
|
+
},
|
|
29
|
+
person_a_longitude: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Optional Person A birth longitude in decimal degrees. See person_a_latitude.",
|
|
32
|
+
},
|
|
24
33
|
person_b_datetime: {
|
|
25
34
|
type: "string",
|
|
26
35
|
description: DATETIME_DESC,
|
|
27
36
|
},
|
|
28
37
|
person_b_timezone: timezoneProperty("Person B's birth location", "America/Los_Angeles"),
|
|
38
|
+
person_b_latitude: {
|
|
39
|
+
type: "number",
|
|
40
|
+
description: "Optional Person B birth latitude in decimal degrees. See person_a_latitude.",
|
|
41
|
+
},
|
|
42
|
+
person_b_longitude: {
|
|
43
|
+
type: "number",
|
|
44
|
+
description: "Optional Person B birth longitude in decimal degrees.",
|
|
45
|
+
},
|
|
29
46
|
format: {
|
|
30
47
|
type: "string",
|
|
31
48
|
enum: ["json", "llm"],
|
|
@@ -41,10 +58,10 @@ registerTool({
|
|
|
41
58
|
validateRequired(args, ["person_a_datetime", "person_b_datetime"]);
|
|
42
59
|
const body = {
|
|
43
60
|
subject_1: {
|
|
44
|
-
birth_datetime_utc:
|
|
61
|
+
birth_datetime_utc: await localToUtcIsoHistorical("person_a_datetime", args.person_a_datetime, args.person_a_timezone, { latitude: args.person_a_latitude, longitude: args.person_a_longitude }, "person_a_timezone"),
|
|
45
62
|
},
|
|
46
63
|
subject_2: {
|
|
47
|
-
birth_datetime_utc:
|
|
64
|
+
birth_datetime_utc: await localToUtcIsoHistorical("person_b_datetime", args.person_b_datetime, args.person_b_timezone, { latitude: args.person_b_latitude, longitude: args.person_b_longitude }, "person_b_timezone"),
|
|
48
65
|
},
|
|
49
66
|
};
|
|
50
67
|
if (args.format)
|
|
@@ -81,6 +98,14 @@ registerTool({
|
|
|
81
98
|
name: { type: "string", description: "Member's name." },
|
|
82
99
|
datetime: { type: "string", description: DATETIME_DESC },
|
|
83
100
|
timezone: timezoneProperty("this member's birth location"),
|
|
101
|
+
latitude: {
|
|
102
|
+
type: "number",
|
|
103
|
+
description: "Optional birth latitude in decimal degrees. Only used to apply the historical timezone correction for pre-1970 local birth times; has no other effect.",
|
|
104
|
+
},
|
|
105
|
+
longitude: {
|
|
106
|
+
type: "number",
|
|
107
|
+
description: "Optional birth longitude in decimal degrees. See latitude.",
|
|
108
|
+
},
|
|
84
109
|
},
|
|
85
110
|
required: ["id", "name", "datetime"],
|
|
86
111
|
},
|
|
@@ -101,13 +126,13 @@ registerTool({
|
|
|
101
126
|
annotations: { title: "HD Penta Group Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
102
127
|
handler: async (args) => {
|
|
103
128
|
validateRequired(args, ["group_name", "members"]);
|
|
104
|
-
const members = args.members.map((m, i) => ({
|
|
129
|
+
const members = await Promise.all(args.members.map(async (m, i) => ({
|
|
105
130
|
id: m.id,
|
|
106
131
|
name: m.name,
|
|
107
132
|
birth_data: {
|
|
108
|
-
birth_datetime_utc:
|
|
133
|
+
birth_datetime_utc: await localToUtcIsoHistorical(`members[${i}].datetime`, m.datetime, m.timezone, { latitude: m.latitude, longitude: m.longitude }, `members[${i}].timezone`),
|
|
109
134
|
},
|
|
110
|
-
}));
|
|
135
|
+
})));
|
|
111
136
|
const body = {
|
|
112
137
|
group_name: args.group_name,
|
|
113
138
|
members,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
|
|
4
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "human_design_chart",
|
|
7
8
|
description: "Calculate a full Human Design I Ching hexagram chart from birth data. Returns the person's Type " +
|
|
@@ -65,7 +66,7 @@ registerTool({
|
|
|
65
66
|
handler: async (args) => {
|
|
66
67
|
validateRequired(args, ["datetime"]);
|
|
67
68
|
const body = {
|
|
68
|
-
birth_datetime_utc:
|
|
69
|
+
birth_datetime_utc: await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude }),
|
|
69
70
|
};
|
|
70
71
|
if (args.latitude != null)
|
|
71
72
|
body.latitude = args.latitude;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { registerTool, validateCoordinates } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
|
-
import { TIMEZONE_PROPERTY
|
|
4
|
+
import { TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
|
+
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "ephemeris_moon_phase",
|
|
7
8
|
description: "Get the Moon's current phase angle, illumination, sign and void-of-course status AT a " +
|
|
@@ -49,7 +50,7 @@ registerTool({
|
|
|
49
50
|
// Backend requires datetime even though the schema marks it optional.
|
|
50
51
|
// Default to current UTC time when caller omits it.
|
|
51
52
|
params.datetime = args.datetime
|
|
52
|
-
?
|
|
53
|
+
? await localToUtcIsoHistorical("datetime", args.datetime, args.timezone, { latitude: args.latitude, longitude: args.longitude })
|
|
53
54
|
: new Date().toISOString();
|
|
54
55
|
if (args.latitude != null)
|
|
55
56
|
params.latitude = args.latitude;
|