@openephemeris/mcp-server 3.24.0 → 4.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.
- package/CHANGELOG.md +97 -0
- package/LICENSE +21 -21
- package/README.md +40 -2
- package/dist/analytics.js +37 -5
- package/dist/backend/client.d.ts +7 -0
- package/dist/backend/client.js +39 -38
- package/dist/index.js +64 -2
- package/dist/oauth/session-utils.d.ts +42 -18
- package/dist/oauth/session-utils.js +79 -0
- package/dist/server-sse.js +114 -14
- package/dist/tools/apps/bazi-app.js +12 -26
- package/dist/tools/apps/bi-wheel-app.js +2 -2
- package/dist/tools/apps/bodygraph-app.d.ts +5 -5
- package/dist/tools/apps/bodygraph-app.js +157 -210
- package/dist/tools/apps/chart-wheel-app.js +5 -3
- package/dist/tools/apps/location-tools.js +167 -18
- package/dist/tools/apps/moon-phase-app.js +10 -3
- package/dist/tools/apps/transit-timeline-app.js +6 -4
- package/dist/tools/apps/vedic-chart-app.js +15 -49
- package/dist/tools/datetime.d.ts +65 -0
- package/dist/tools/datetime.js +153 -0
- package/dist/tools/index.d.ts +45 -2
- package/dist/tools/index.js +78 -2
- package/dist/tools/specialized/account.d.ts +1 -0
- package/dist/tools/specialized/account.js +100 -0
- package/dist/tools/specialized/acg.js +16 -14
- package/dist/tools/specialized/bazi.d.ts +7 -1
- package/dist/tools/specialized/bazi.js +86 -19
- package/dist/tools/specialized/bi_wheel.js +5 -4
- package/dist/tools/specialized/chart_wheel.js +5 -8
- package/dist/tools/specialized/comparative.js +13 -5
- package/dist/tools/specialized/electional.js +7 -7
- package/dist/tools/specialized/ephemeris_core.js +13 -8
- package/dist/tools/specialized/ephemeris_extended.js +27 -17
- package/dist/tools/specialized/hd_bodygraph.js +8 -10
- package/dist/tools/specialized/hd_cycles.js +7 -14
- package/dist/tools/specialized/hd_group.js +20 -13
- package/dist/tools/specialized/human_design.js +11 -17
- package/dist/tools/specialized/moon.js +8 -2
- package/dist/tools/specialized/natal.js +7 -9
- package/dist/tools/specialized/progressed.js +12 -8
- package/dist/tools/specialized/relocation.js +9 -3
- package/dist/tools/specialized/returns.js +23 -11
- package/dist/tools/specialized/synastry.js +17 -6
- package/dist/tools/specialized/transits.js +9 -5
- package/dist/tools/specialized/vedic.js +5 -3
- package/dist/tools/specialized/venus_star_points.js +14 -9
- package/dist/ui/bazi.html +1063 -1049
- package/dist/ui/bi-wheel.html +4188 -4128
- package/dist/ui/bodygraph.html +3673 -3616
- package/dist/ui/chart-wheel.html +3769 -3713
- package/dist/ui/moon-phase.html +3219 -3153
- package/dist/ui/transit-timeline.html +199 -170
- package/dist/ui/vedic-chart.html +1116 -1098
- package/package.json +3 -2
- package/smithery.yaml +1 -1
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { registerTool } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE_AND_JSON } from "../output-schemas.js";
|
|
4
|
-
function parseBaziArgs(args) {
|
|
4
|
+
export function parseBaziArgs(args, datetimeField = "datetime") {
|
|
5
5
|
let { year, month, day, hour } = args;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
6
|
+
const datetime = args[datetimeField];
|
|
7
|
+
if (datetime && (!year || !month || !day)) {
|
|
8
|
+
const parts = baziLocalComponents(datetime, args.timezone, datetimeField);
|
|
9
|
+
year = parts.year;
|
|
10
|
+
month = parts.month;
|
|
11
|
+
day = parts.day;
|
|
12
|
+
if (hour == null)
|
|
13
|
+
hour = parts.hour;
|
|
15
14
|
}
|
|
16
15
|
if (!year || !month || !day) {
|
|
17
16
|
throw new Error("Provide year/month/day fields, or a datetime ISO string. " +
|
|
@@ -22,6 +21,49 @@ function parseBaziArgs(args) {
|
|
|
22
21
|
out.hour = hour;
|
|
23
22
|
return out;
|
|
24
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract the LOCAL calendar/clock components a BaZi chart is built from.
|
|
26
|
+
*
|
|
27
|
+
* BaZi pillars are a function of local wall-clock time at the birth place — the
|
|
28
|
+
* hour pillar is a two-hour shí block on the local clock — so unlike a western
|
|
29
|
+
* chart there is no UTC instant to resolve to, and a zone-less datetime is
|
|
30
|
+
* exactly right here. What was wrong was HOW those components were read: the
|
|
31
|
+
* previous implementation did `new Date("1987-07-15T14:00:00")`, which JS parses
|
|
32
|
+
* in the *host process's* timezone, then read `.getUTCHours()`. On any server
|
|
33
|
+
* not running in UTC that silently shifted the hour pillar, and near midnight
|
|
34
|
+
* the day pillar too. Reading the components lexically removes the dependency on
|
|
35
|
+
* where the server happens to be.
|
|
36
|
+
*
|
|
37
|
+
* A datetime that carries its own zone (`Z` / `±HH:MM`) is not local wall-clock
|
|
38
|
+
* time. It is converted into `timezone` when one is supplied; without one, its
|
|
39
|
+
* own stated zone is taken as the local clock, which is the long-standing
|
|
40
|
+
* behaviour and the only reading available.
|
|
41
|
+
*/
|
|
42
|
+
function baziLocalComponents(datetime, timezone, field) {
|
|
43
|
+
const value = String(datetime).trim();
|
|
44
|
+
const zoned = /([Zz]|[+-]\d{2}:?\d{2})$/.test(value);
|
|
45
|
+
if (zoned && timezone) {
|
|
46
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
47
|
+
timeZone: timezone,
|
|
48
|
+
hour12: false,
|
|
49
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
50
|
+
hour: "2-digit", minute: "2-digit",
|
|
51
|
+
}).formatToParts(new Date(value));
|
|
52
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
53
|
+
return { year: get("year"), month: get("month"), day: get("day"), hour: get("hour") % 24 };
|
|
54
|
+
}
|
|
55
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?/.exec(value);
|
|
56
|
+
if (!m) {
|
|
57
|
+
throw new Error(`\`${field}\` is "${value}", which is not an ISO 8601 date or datetime. ` +
|
|
58
|
+
"Example: '1987-07-15T14:00:00' (local time at the birth place).");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
year: Number(m[1]),
|
|
62
|
+
month: Number(m[2]),
|
|
63
|
+
day: Number(m[3]),
|
|
64
|
+
hour: m[4] != null ? Number(m[4]) : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
25
67
|
// Shared datetime input schema fragment — used across all BaZiRequest tools.
|
|
26
68
|
const DATETIME_PROPERTIES = {
|
|
27
69
|
year: {
|
|
@@ -43,8 +85,17 @@ const DATETIME_PROPERTIES = {
|
|
|
43
85
|
},
|
|
44
86
|
datetime: {
|
|
45
87
|
type: "string",
|
|
46
|
-
description: "Alternative to year/month/day: ISO 8601 datetime
|
|
47
|
-
"
|
|
88
|
+
description: "Alternative to year/month/day: ISO 8601 datetime read as LOCAL wall-clock time at the " +
|
|
89
|
+
"birth place — BaZi pillars are local by definition, so a zone-less value like " +
|
|
90
|
+
"'1987-07-15T14:00:00' is correct here and is NOT converted to UTC. " +
|
|
91
|
+
"If you pass an instant instead ('1987-07-15T19:00:00Z' or with an offset), also pass " +
|
|
92
|
+
"`timezone` so the local hour can be derived. Use this OR the individual fields.",
|
|
93
|
+
},
|
|
94
|
+
timezone: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "IANA timezone name for the birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
97
|
+
"`datetime` carries a 'Z' or ±HH:MM offset, to convert that instant back to the local " +
|
|
98
|
+
"clock the pillars are built from.",
|
|
48
99
|
},
|
|
49
100
|
};
|
|
50
101
|
// Shared visual input schema fragment — mirrors natal.ts pattern.
|
|
@@ -132,7 +183,7 @@ registerTool({
|
|
|
132
183
|
" RESOURCE: Indirect Resource (偏印), Direct Resource (正印)\n\n" +
|
|
133
184
|
"The Day Pillar itself has no Ten God (it IS the Day Master). " +
|
|
134
185
|
"Hidden stems in the Day Branch still receive Ten God labels.\n\n" +
|
|
135
|
-
"CREDIT COST:
|
|
186
|
+
"CREDIT COST: 3 credits (5 credits when include_visual=true).\n\n" +
|
|
136
187
|
"Set include_visual=true to receive an SVG Four Pillars chart alongside the Ten Gods data.\n\n" +
|
|
137
188
|
"EXAMPLE: Ten Gods for someone born July 15, 1987 at 2 PM:\n" +
|
|
138
189
|
" year=1987, month=7, day=15, hour=14",
|
|
@@ -167,7 +218,7 @@ registerTool({
|
|
|
167
218
|
"Also returns:\n" +
|
|
168
219
|
" • Day Master strength: 'strong' (旺, ≥50% own+resource elements) or 'weak' (弱)\n" +
|
|
169
220
|
" • Yong Shen (用神): the favorable element — what the chart needs most\n\n" +
|
|
170
|
-
"CREDIT COST:
|
|
221
|
+
"CREDIT COST: 3 credits (5 credits when include_visual=true).\n\n" +
|
|
171
222
|
"Set include_visual=true to receive an SVG Four Pillars chart alongside the element data.\n\n" +
|
|
172
223
|
"EXAMPLE: Element balance for 1987-07-15 at 14:00:\n" +
|
|
173
224
|
" year=1987, month=7, day=15, hour=14",
|
|
@@ -204,7 +255,7 @@ registerTool({
|
|
|
204
255
|
"gender is REQUIRED — the direction of luck pillars is gender-dependent.\n\n" +
|
|
205
256
|
"Returns: starting_age, direction, direction_reason, and 8 pillars each with:\n" +
|
|
206
257
|
" stem, branch, Chinese characters, element, start_age, end_age\n\n" +
|
|
207
|
-
"CREDIT COST:
|
|
258
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
208
259
|
"EXAMPLE: Luck pillars for a female born July 15, 1987 at 2 PM:\n" +
|
|
209
260
|
" year=1987, month=7, day=15, hour=14, gender='female'",
|
|
210
261
|
inputSchema: {
|
|
@@ -251,7 +302,7 @@ registerTool({
|
|
|
251
302
|
" • Identify the energetic quality of any given year\n" +
|
|
252
303
|
" • Determine a person's birth year pillar for compatibility context\n" +
|
|
253
304
|
" • Find the NaYin element for year or day interpretations\n\n" +
|
|
254
|
-
"CREDIT COST:
|
|
305
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
255
306
|
"EXAMPLE: Year pillar for 2025:\n" +
|
|
256
307
|
" year=2025",
|
|
257
308
|
inputSchema: {
|
|
@@ -292,7 +343,7 @@ registerTool({
|
|
|
292
343
|
" -6 Three Penalties (三刑 Sān Xíng) — branch penalty formations\n\n" +
|
|
293
344
|
"Assessment grades:\n" +
|
|
294
345
|
" 90–100: excellent | 70–89: good | 50–69: moderate | below 50: challenging\n\n" +
|
|
295
|
-
"CREDIT COST:
|
|
346
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
296
347
|
"EXAMPLE: Compatibility between two people:\n" +
|
|
297
348
|
" chart_a_year=1987, chart_a_month=7, chart_a_day=15, chart_a_hour=14\n" +
|
|
298
349
|
" chart_b_year=1990, chart_b_month=3, chart_b_day=22, chart_b_hour=8",
|
|
@@ -306,7 +357,14 @@ registerTool({
|
|
|
306
357
|
chart_a_hour: { type: "integer", description: "Chart A birth hour (0–23). Optional." },
|
|
307
358
|
chart_a_datetime: {
|
|
308
359
|
type: "string",
|
|
309
|
-
description: "Chart A alternative: ISO 8601 datetime
|
|
360
|
+
description: "Chart A alternative: ISO 8601 datetime read as LOCAL wall-clock time at the birth " +
|
|
361
|
+
"place — a zone-less value is correct here and is not converted to UTC. If it carries " +
|
|
362
|
+
"a 'Z' or offset, also pass chart_a_timezone. Extracts year/month/day/hour automatically.",
|
|
363
|
+
},
|
|
364
|
+
chart_a_timezone: {
|
|
365
|
+
type: "string",
|
|
366
|
+
description: "IANA timezone for Chart A's birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
367
|
+
"chart_a_datetime carries a 'Z' or ±HH:MM offset.",
|
|
310
368
|
},
|
|
311
369
|
// Chart B
|
|
312
370
|
chart_b_year: { type: "integer", description: "Chart B birth year." },
|
|
@@ -315,7 +373,14 @@ registerTool({
|
|
|
315
373
|
chart_b_hour: { type: "integer", description: "Chart B birth hour (0–23). Optional." },
|
|
316
374
|
chart_b_datetime: {
|
|
317
375
|
type: "string",
|
|
318
|
-
description: "Chart B alternative: ISO 8601 datetime
|
|
376
|
+
description: "Chart B alternative: ISO 8601 datetime read as LOCAL wall-clock time at the birth " +
|
|
377
|
+
"place — a zone-less value is correct here and is not converted to UTC. If it carries " +
|
|
378
|
+
"a 'Z' or offset, also pass chart_b_timezone. Extracts year/month/day/hour automatically.",
|
|
379
|
+
},
|
|
380
|
+
chart_b_timezone: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description: "IANA timezone for Chart B's birth place, e.g. 'Asia/Shanghai'. Only needed when " +
|
|
383
|
+
"chart_b_datetime carries a 'Z' or ±HH:MM offset.",
|
|
319
384
|
},
|
|
320
385
|
},
|
|
321
386
|
additionalProperties: false,
|
|
@@ -329,6 +394,7 @@ registerTool({
|
|
|
329
394
|
day: args.chart_a_day,
|
|
330
395
|
hour: args.chart_a_hour,
|
|
331
396
|
datetime: args.chart_a_datetime,
|
|
397
|
+
timezone: args.chart_a_timezone,
|
|
332
398
|
});
|
|
333
399
|
const chartBComponents = parseBaziArgs({
|
|
334
400
|
year: args.chart_b_year,
|
|
@@ -336,6 +402,7 @@ registerTool({
|
|
|
336
402
|
day: args.chart_b_day,
|
|
337
403
|
hour: args.chart_b_hour,
|
|
338
404
|
datetime: args.chart_b_datetime,
|
|
405
|
+
timezone: args.chart_b_timezone,
|
|
339
406
|
});
|
|
340
407
|
return await getActiveClient().request("POST", "/chinese/bazi/compatibility", {
|
|
341
408
|
data: {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
|
-
|
|
5
|
-
"Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
|
|
4
|
+
import { DATETIME_DESC, assertZonedDatetime, timezoneProperty } from "../datetime.js";
|
|
6
5
|
const HOUSE_SYSTEM_MAP = {
|
|
7
6
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
7
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
@@ -19,11 +18,11 @@ registerTool({
|
|
|
19
18
|
type: "object",
|
|
20
19
|
properties: {
|
|
21
20
|
datetime_a: { type: "string", description: DATETIME_DESC },
|
|
22
|
-
timezone_a:
|
|
21
|
+
timezone_a: timezoneProperty("subject A's location", "America/Denver"),
|
|
23
22
|
latitude_a: { type: "number", description: "Latitude for subject A" },
|
|
24
23
|
longitude_a: { type: "number", description: "Longitude for subject A" },
|
|
25
24
|
datetime_b: { type: "string", description: DATETIME_DESC },
|
|
26
|
-
timezone_b:
|
|
25
|
+
timezone_b: timezoneProperty("subject B's location", "Europe/London"),
|
|
27
26
|
latitude_b: { type: "number", description: "Latitude for subject B" },
|
|
28
27
|
longitude_b: { type: "number", description: "Longitude for subject B" },
|
|
29
28
|
house_system: {
|
|
@@ -44,6 +43,8 @@ registerTool({
|
|
|
44
43
|
annotations: { title: "Bi-Wheel Chart", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
45
44
|
handler: async (args) => {
|
|
46
45
|
validateRequired(args, ["datetime_a", "latitude_a", "longitude_a", "datetime_b", "latitude_b", "longitude_b"]);
|
|
46
|
+
assertZonedDatetime("datetime_a", args.datetime_a, args.timezone_a, "timezone_a");
|
|
47
|
+
assertZonedDatetime("datetime_b", args.datetime_b, args.timezone_b, "timezone_b");
|
|
47
48
|
const body = {
|
|
48
49
|
subject_a: {
|
|
49
50
|
name: "Subject A",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { registerTool, validateRequired, pickEnum } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_IMAGE } from "../output-schemas.js";
|
|
4
|
-
|
|
5
|
-
"Include timezone offset if known, e.g. '1990-04-15T14:30:00-05:00'.";
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime } from "../datetime.js";
|
|
6
5
|
const HOUSE_SYSTEM_MAP = {
|
|
7
6
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
8
7
|
campanus: "C", regiomontanus: "R", porphyry: "O",
|
|
@@ -13,8 +12,8 @@ registerTool({
|
|
|
13
12
|
description: "Generate a classic astrological Chart Wheel image (SVG) for a person/event. This draws a standard circular chart wheel with planets, aspects, and house cusps. The tool returns a native SVG image that Claude displays inline in the conversation — no external tools needed.\n\n" +
|
|
14
13
|
"For a user-facing interactive chart wheel, use explore_natal_chart instead.\n\n" +
|
|
15
14
|
"CREDIT COST: 2 credits per call.\n\n" +
|
|
16
|
-
"EXAMPLE:
|
|
17
|
-
" datetime='1990-04-15T14:30:00', latitude=41.8781, longitude=-87.6298",
|
|
15
|
+
"EXAMPLE: born 15 April 1990 at 2:30 PM local time in Chicago:\n" +
|
|
16
|
+
" datetime='1990-04-15T14:30:00', timezone='America/Chicago', latitude=41.8781, longitude=-87.6298",
|
|
18
17
|
inputSchema: {
|
|
19
18
|
type: "object",
|
|
20
19
|
properties: {
|
|
@@ -22,10 +21,7 @@ registerTool({
|
|
|
22
21
|
type: "string",
|
|
23
22
|
description: DATETIME_DESC,
|
|
24
23
|
},
|
|
25
|
-
timezone:
|
|
26
|
-
type: "string",
|
|
27
|
-
description: "IANA timezone name, e.g. 'America/Denver'. Use this if passing local time without a UTC offset in the datetime string.",
|
|
28
|
-
},
|
|
24
|
+
timezone: TIMEZONE_PROPERTY,
|
|
29
25
|
latitude: {
|
|
30
26
|
type: "number",
|
|
31
27
|
description: "Geographic latitude of birth location in decimal degrees (positive = North).",
|
|
@@ -52,6 +48,7 @@ registerTool({
|
|
|
52
48
|
annotations: { title: "Chart Wheel SVG", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
53
49
|
handler: async (args) => {
|
|
54
50
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
51
|
+
assertZonedDatetime("datetime", args.datetime, args.timezone);
|
|
55
52
|
const body = {
|
|
56
53
|
subject: {
|
|
57
54
|
name: "Chart Wheel Request",
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { localToUtcIso } from "../datetime.js";
|
|
4
5
|
function buildSubject(name, datetime, lat, lon, timezone) {
|
|
5
6
|
const subj = {
|
|
6
7
|
name,
|
|
@@ -22,8 +23,10 @@ registerTool({
|
|
|
22
23
|
"uses midpoints of each planet pair to derive a single relationship chart.\n\n" +
|
|
23
24
|
"CREDIT COST: 3 credits per call.\n\n" +
|
|
24
25
|
"EXAMPLE: Composite for two people:\n" +
|
|
25
|
-
" person_a_datetime='1990-04-15T14:30:00',
|
|
26
|
-
"
|
|
26
|
+
" person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
|
|
27
|
+
" person_a_latitude=41.88, person_a_longitude=-87.63,\n" +
|
|
28
|
+
" person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles',\n" +
|
|
29
|
+
" person_b_latitude=34.05, person_b_longitude=-118.24",
|
|
27
30
|
inputSchema: {
|
|
28
31
|
type: "object",
|
|
29
32
|
properties: {
|
|
@@ -163,7 +166,8 @@ registerTool({
|
|
|
163
166
|
"are aspecting natal positions. Essential for predictive astrology.\n\n" +
|
|
164
167
|
"CREDIT COST: 3 credits per call.\n\n" +
|
|
165
168
|
"EXAMPLE: Current transits to someone born 1990-04-15:\n" +
|
|
166
|
-
" natal_datetime='1990-04-15T14:30:00',
|
|
169
|
+
" natal_datetime='1990-04-15T14:30:00', natal_timezone='America/Chicago',\n" +
|
|
170
|
+
" natal_latitude=41.88, natal_longitude=-87.63",
|
|
167
171
|
inputSchema: {
|
|
168
172
|
type: "object",
|
|
169
173
|
properties: {
|
|
@@ -188,8 +192,12 @@ registerTool({
|
|
|
188
192
|
subject: buildSubject("Natal", args.natal_datetime, args.natal_latitude, args.natal_longitude, args.natal_timezone),
|
|
189
193
|
};
|
|
190
194
|
if (args.transit_datetime) {
|
|
191
|
-
// transit_datetime is a DateTimeInput on the backend
|
|
192
|
-
|
|
195
|
+
// transit_datetime is a DateTimeInput on the backend, and unlike the
|
|
196
|
+
// subject it has no timezone companion — so , which
|
|
197
|
+
// was declared but never read, is resolved here.
|
|
198
|
+
body.transit_datetime = {
|
|
199
|
+
iso: localToUtcIso("transit_datetime", args.transit_datetime, args.transit_timezone, "transit_timezone"),
|
|
200
|
+
};
|
|
193
201
|
}
|
|
194
202
|
const query = {};
|
|
195
203
|
if (args.format)
|
|
@@ -25,7 +25,7 @@ registerTool({
|
|
|
25
25
|
},
|
|
26
26
|
latitude: {
|
|
27
27
|
type: "number",
|
|
28
|
-
description: "Latitude of location in decimal degrees (positive = North).",
|
|
28
|
+
description: "Latitude of location in decimal degrees (positive = North). Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
29
29
|
},
|
|
30
30
|
longitude: {
|
|
31
31
|
type: "number",
|
|
@@ -89,15 +89,15 @@ registerTool({
|
|
|
89
89
|
description: "Analyze the astrological quality of a specific moment: planet positions, aspects, " +
|
|
90
90
|
"void of course status, lunar phase, day ruler, and an overall electional score (0-100). " +
|
|
91
91
|
"Perfect for evaluating whether 'right now' or a specific date/time is good for action.\n\n" +
|
|
92
|
-
"CREDIT COST:
|
|
92
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
93
93
|
"EXAMPLE: Analyze March 21, 2026 at noon:\n" +
|
|
94
|
-
" date='2026-03-21T12:00:
|
|
94
|
+
" date='2026-03-21T12:00:00Z'",
|
|
95
95
|
inputSchema: {
|
|
96
96
|
type: "object",
|
|
97
97
|
properties: {
|
|
98
98
|
date: {
|
|
99
99
|
type: "string",
|
|
100
|
-
description: "ISO 8601 datetime to analyze (e.g., '2026-03-21T12:00:00'). Defaults to now.",
|
|
100
|
+
description: "ISO 8601 datetime to analyze, with a zone (e.g., '2026-03-21T12:00:00Z' or '2026-03-21T08:00:00-04:00'). Defaults to now.",
|
|
101
101
|
},
|
|
102
102
|
format: {
|
|
103
103
|
type: "string",
|
|
@@ -129,7 +129,7 @@ registerTool({
|
|
|
129
129
|
"USE THIS TOOL FOR: 'When does Mercury go retrograde?', 'Is Venus retrograde this year?', " +
|
|
130
130
|
"'What planets station this month?', 'When does Mars go direct?'\n\n" +
|
|
131
131
|
"All required fields have smart defaults (searches the next 90 days from today).\n\n" +
|
|
132
|
-
"CREDIT COST:
|
|
132
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
133
133
|
"EXAMPLE: Mercury and Venus stations in the next 3 months (all defaults):\n" +
|
|
134
134
|
" (no args required, will auto-scan next 90 days for all inner planets)\n\n" +
|
|
135
135
|
"EXAMPLE: Outer planet stations in 2026:\n" +
|
|
@@ -198,9 +198,9 @@ registerTool({
|
|
|
198
198
|
description: "Find all active aspects between planets at a specific moment. Returns aspect type, " +
|
|
199
199
|
"orb, quality score, and whether it's applying or separating. Great for checking " +
|
|
200
200
|
"the 'weather' of a given day.\n\n" +
|
|
201
|
-
"CREDIT COST:
|
|
201
|
+
"CREDIT COST: 5 credits per call.\n\n" +
|
|
202
202
|
"EXAMPLE: What aspects are active on March 21, 2026?\n" +
|
|
203
|
-
" date='2026-03-21T12:00:
|
|
203
|
+
" date='2026-03-21T12:00:00Z'",
|
|
204
204
|
inputSchema: {
|
|
205
205
|
type: "object",
|
|
206
206
|
properties: {
|
|
@@ -1,14 +1,17 @@
|
|
|
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, localToUtcIso } from "../datetime.js";
|
|
4
5
|
// POST /ephemeris/planet-position — OE-016
|
|
5
6
|
registerTool({
|
|
6
7
|
name: "ephemeris_planet_position",
|
|
7
8
|
description: "Get the precise ecliptic longitude, latitude, distance, speed, and retrograde status " +
|
|
8
9
|
"for a single planet/body at a given date and time. " +
|
|
9
10
|
"Planet IDs: 0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, " +
|
|
10
|
-
"7=Uranus, 8=Neptune, 9=Pluto, 10=North Node, 11=
|
|
11
|
-
"15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta.\n\n" +
|
|
11
|
+
"7=Uranus, 8=Neptune, 9=Pluto, 10=North Node (Mean), 11=North Node (True), " +
|
|
12
|
+
"12=Lilith (Mean), 15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta.\n\n" +
|
|
13
|
+
"SOUTH NODE: there is no South Node id. The South Node is always exactly " +
|
|
14
|
+
"opposite the North Node — request id 10 or 11 and add 180° (mod 360).\n\n" +
|
|
12
15
|
"CREDIT COST: 1 credit per call.\n\n" +
|
|
13
16
|
"EXAMPLE: Where is Mars on 2026-03-20 at noon UTC?\n" +
|
|
14
17
|
" planet_id=4, datetime='2026-03-20T12:00:00Z'",
|
|
@@ -17,12 +20,13 @@ registerTool({
|
|
|
17
20
|
properties: {
|
|
18
21
|
planet_id: {
|
|
19
22
|
type: "integer",
|
|
20
|
-
description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=
|
|
23
|
+
description: "Planet/body ID (0=Sun, 1=Moon, 2=Mercury, 3=Venus, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune, 9=Pluto, 10=North Node (Mean), 11=North Node (True), 12=Lilith (Mean), 15=Chiron, 17=Ceres, 18=Pallas, 19=Juno, 20=Vesta). There is no South Node id — take the North Node and add 180°.",
|
|
21
24
|
},
|
|
22
25
|
datetime: {
|
|
23
26
|
type: "string",
|
|
24
|
-
description:
|
|
27
|
+
description: DATETIME_DESC,
|
|
25
28
|
},
|
|
29
|
+
timezone: TIMEZONE_PROPERTY,
|
|
26
30
|
latitude: {
|
|
27
31
|
type: "number",
|
|
28
32
|
description: "Observer latitude for topocentric position (optional). Omit for geocentric.",
|
|
@@ -41,7 +45,7 @@ registerTool({
|
|
|
41
45
|
validateRequired(args, ["planet_id", "datetime"]);
|
|
42
46
|
const body = {
|
|
43
47
|
planet_id: args.planet_id,
|
|
44
|
-
date_time: { iso: args.datetime },
|
|
48
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
45
49
|
};
|
|
46
50
|
if (args.latitude != null)
|
|
47
51
|
body.latitude = args.latitude;
|
|
@@ -64,11 +68,12 @@ registerTool({
|
|
|
64
68
|
properties: {
|
|
65
69
|
datetime: {
|
|
66
70
|
type: "string",
|
|
67
|
-
description:
|
|
71
|
+
description: DATETIME_DESC,
|
|
68
72
|
},
|
|
73
|
+
timezone: TIMEZONE_PROPERTY,
|
|
69
74
|
latitude: {
|
|
70
75
|
type: "number",
|
|
71
|
-
description: "Observer latitude in decimal degrees.",
|
|
76
|
+
description: "Observer latitude in decimal degrees. Resolve from a place name with location_search; never recall coordinates from memory.",
|
|
72
77
|
},
|
|
73
78
|
longitude: {
|
|
74
79
|
type: "number",
|
|
@@ -88,7 +93,7 @@ registerTool({
|
|
|
88
93
|
handler: async (args) => {
|
|
89
94
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
90
95
|
const body = {
|
|
91
|
-
date_time: { iso: args.datetime },
|
|
96
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
92
97
|
latitude: args.latitude,
|
|
93
98
|
longitude: args.longitude,
|
|
94
99
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
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, assertZonedDatetime, localToUtcIso } from "../datetime.js";
|
|
4
5
|
function buildSubject(name, datetime, lat, lon) {
|
|
5
6
|
return {
|
|
6
7
|
name,
|
|
@@ -21,8 +22,8 @@ registerTool({
|
|
|
21
22
|
"(100 subjects = 100 credits). Confirm with the user before large batches.\n\n" +
|
|
22
23
|
"EXAMPLE: Two people batch:\n" +
|
|
23
24
|
" subjects: [\n" +
|
|
24
|
-
" { name: 'Alice', datetime: '1990-04-15T14:30:00', latitude: 41.88, longitude: -87.63 },\n" +
|
|
25
|
-
" { name: 'Bob', datetime: '1988-09-
|
|
25
|
+
" { name: 'Alice', datetime: '1990-04-15T14:30:00', timezone: 'America/Chicago', latitude: 41.88, longitude: -87.63 },\n" +
|
|
26
|
+
" { name: 'Bob', datetime: '1988-09-22T15:15:00Z', latitude: 34.05, longitude: -118.24 }\n" +
|
|
26
27
|
" ]",
|
|
27
28
|
inputSchema: {
|
|
28
29
|
type: "object",
|
|
@@ -33,7 +34,8 @@ registerTool({
|
|
|
33
34
|
type: "object",
|
|
34
35
|
properties: {
|
|
35
36
|
name: { type: "string", description: "Subject name." },
|
|
36
|
-
datetime: { type: "string", description:
|
|
37
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
38
|
+
timezone: TIMEZONE_PROPERTY,
|
|
37
39
|
latitude: { type: "number", description: "Birth latitude." },
|
|
38
40
|
longitude: { type: "number", description: "Birth longitude." },
|
|
39
41
|
},
|
|
@@ -49,6 +51,7 @@ registerTool({
|
|
|
49
51
|
annotations: { title: "Natal Batch Charts", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
50
52
|
handler: async (args) => {
|
|
51
53
|
validateRequired(args, ["subjects"]);
|
|
54
|
+
args.subjects.forEach((s, i) => assertZonedDatetime(`subjects[${i}].datetime`, s?.datetime, s?.timezone, `subjects[${i}].timezone`));
|
|
52
55
|
const items = args.subjects.map((s) => ({
|
|
53
56
|
subject: buildSubject(s.name, s.datetime, s.latitude, s.longitude),
|
|
54
57
|
}));
|
|
@@ -64,7 +67,8 @@ registerTool({
|
|
|
64
67
|
inputSchema: {
|
|
65
68
|
type: "object",
|
|
66
69
|
properties: {
|
|
67
|
-
datetime: { type: "string", description:
|
|
70
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
71
|
+
timezone: TIMEZONE_PROPERTY,
|
|
68
72
|
},
|
|
69
73
|
required: ["datetime"],
|
|
70
74
|
additionalProperties: false,
|
|
@@ -74,7 +78,7 @@ registerTool({
|
|
|
74
78
|
handler: async (args) => {
|
|
75
79
|
validateRequired(args, ["datetime"]);
|
|
76
80
|
return await getActiveClient().post("/ephemeris/dignities", {
|
|
77
|
-
date_time: { iso: args.datetime },
|
|
81
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
78
82
|
});
|
|
79
83
|
},
|
|
80
84
|
});
|
|
@@ -86,12 +90,14 @@ registerTool({
|
|
|
86
90
|
name: "ephemeris_retrograde_status",
|
|
87
91
|
description: "Get retrograde/direct status and speed for all planets at a given date/time. " +
|
|
88
92
|
"Returns is_retrograde flag, longitude speed, and station proximity for every planet.\n\n" +
|
|
89
|
-
"CREDIT COST:
|
|
93
|
+
"CREDIT COST: 10 credits for the all-planets sweep (the backend bills one " +
|
|
94
|
+
"credit per body and this fans out to 10), or 1 credit when planet_id is given.\n\n" +
|
|
90
95
|
"Optionally pass planet_id (0-9) to query a single planet.",
|
|
91
96
|
inputSchema: {
|
|
92
97
|
type: "object",
|
|
93
98
|
properties: {
|
|
94
|
-
datetime: { type: "string", description:
|
|
99
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
100
|
+
timezone: TIMEZONE_PROPERTY,
|
|
95
101
|
planet_id: { type: "integer", description: "Optional single planet ID (0-9). Omit for all planets." },
|
|
96
102
|
},
|
|
97
103
|
required: ["datetime"],
|
|
@@ -105,14 +111,14 @@ registerTool({
|
|
|
105
111
|
// If a specific planet is requested, delegate directly.
|
|
106
112
|
if (args.planet_id != null) {
|
|
107
113
|
return await client.post("/ephemeris/retrograde-status", {
|
|
108
|
-
date_time: { iso: args.datetime },
|
|
114
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
109
115
|
planet_id: args.planet_id,
|
|
110
116
|
});
|
|
111
117
|
}
|
|
112
118
|
// Fan-out: the backend only handles one planet per call; query all 10 in parallel.
|
|
113
119
|
const PLANET_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
114
120
|
const results = await Promise.all(PLANET_IDS.map((pid) => client.post("/ephemeris/retrograde-status", {
|
|
115
|
-
date_time: { iso: args.datetime },
|
|
121
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
116
122
|
planet_id: pid,
|
|
117
123
|
}).catch(() => null)));
|
|
118
124
|
// Merge into a keyed object: { planet_name: {...status} }
|
|
@@ -140,7 +146,8 @@ registerTool({
|
|
|
140
146
|
inputSchema: {
|
|
141
147
|
type: "object",
|
|
142
148
|
properties: {
|
|
143
|
-
datetime: { type: "string", description:
|
|
149
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
150
|
+
timezone: TIMEZONE_PROPERTY,
|
|
144
151
|
latitude: { type: "number", description: "Observer latitude." },
|
|
145
152
|
longitude: { type: "number", description: "Observer longitude." },
|
|
146
153
|
},
|
|
@@ -152,7 +159,7 @@ registerTool({
|
|
|
152
159
|
handler: async (args) => {
|
|
153
160
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
154
161
|
return await getActiveClient().post("/ephemeris/midpoints", {
|
|
155
|
-
date_time: { iso: args.datetime },
|
|
162
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
156
163
|
latitude: args.latitude,
|
|
157
164
|
longitude: args.longitude,
|
|
158
165
|
});
|
|
@@ -167,7 +174,8 @@ registerTool({
|
|
|
167
174
|
inputSchema: {
|
|
168
175
|
type: "object",
|
|
169
176
|
properties: {
|
|
170
|
-
datetime: { type: "string", description:
|
|
177
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
178
|
+
timezone: TIMEZONE_PROPERTY,
|
|
171
179
|
star_names: {
|
|
172
180
|
type: "array",
|
|
173
181
|
items: { type: "string" },
|
|
@@ -183,7 +191,7 @@ registerTool({
|
|
|
183
191
|
handler: async (args) => {
|
|
184
192
|
validateRequired(args, ["datetime"]);
|
|
185
193
|
const body = {
|
|
186
|
-
date_time: { iso: args.datetime },
|
|
194
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
187
195
|
};
|
|
188
196
|
if (args.star_names)
|
|
189
197
|
body.star_names = args.star_names;
|
|
@@ -232,7 +240,8 @@ registerTool({
|
|
|
232
240
|
inputSchema: {
|
|
233
241
|
type: "object",
|
|
234
242
|
properties: {
|
|
235
|
-
datetime: { type: "string", description:
|
|
243
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
244
|
+
timezone: TIMEZONE_PROPERTY,
|
|
236
245
|
latitude: { type: "number", description: "Observer latitude." },
|
|
237
246
|
longitude: { type: "number", description: "Observer longitude." },
|
|
238
247
|
},
|
|
@@ -244,7 +253,7 @@ registerTool({
|
|
|
244
253
|
handler: async (args) => {
|
|
245
254
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
246
255
|
return await getActiveClient().post("/ephemeris/hermetic-lots", {
|
|
247
|
-
date_time: { iso: args.datetime },
|
|
256
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
248
257
|
latitude: args.latitude,
|
|
249
258
|
longitude: args.longitude,
|
|
250
259
|
});
|
|
@@ -259,7 +268,8 @@ registerTool({
|
|
|
259
268
|
inputSchema: {
|
|
260
269
|
type: "object",
|
|
261
270
|
properties: {
|
|
262
|
-
datetime: { type: "string", description:
|
|
271
|
+
datetime: { type: "string", description: DATETIME_DESC },
|
|
272
|
+
timezone: TIMEZONE_PROPERTY,
|
|
263
273
|
latitude: { type: "number", description: "Observer latitude." },
|
|
264
274
|
longitude: { type: "number", description: "Observer longitude." },
|
|
265
275
|
},
|
|
@@ -271,7 +281,7 @@ registerTool({
|
|
|
271
281
|
handler: async (args) => {
|
|
272
282
|
validateRequired(args, ["datetime", "latitude", "longitude"]);
|
|
273
283
|
return await getActiveClient().post("/ephemeris/angles-points", {
|
|
274
|
-
date_time: { iso: args.datetime },
|
|
284
|
+
date_time: { iso: localToUtcIso("datetime", args.datetime, args.timezone) },
|
|
275
285
|
latitude: args.latitude,
|
|
276
286
|
longitude: args.longitude,
|
|
277
287
|
});
|
|
@@ -1,12 +1,7 @@
|
|
|
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
|
-
|
|
5
|
-
function ensureTimezone(dt) {
|
|
6
|
-
if (/[Zz]$/.test(dt) || /[+-]\d{2}:\d{2}$/.test(dt))
|
|
7
|
-
return dt;
|
|
8
|
-
return dt + "Z";
|
|
9
|
-
}
|
|
4
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY, localToUtcIso } from "../datetime.js";
|
|
10
5
|
registerTool({
|
|
11
6
|
name: "human_design_bodygraph",
|
|
12
7
|
description: "Generate a Human Design Bodygraph image (SVG) from a birth datetime (UTC). " +
|
|
@@ -16,16 +11,19 @@ registerTool({
|
|
|
16
11
|
"Returns a native SVG that Claude displays inline. Use format='png' to opt into raster output (requires server-side rasterizer).\n\n" +
|
|
17
12
|
"For a user-facing interactive bodygraph explorer, use explore_human_design instead.\n\n" +
|
|
18
13
|
"CREDIT COST: 2 credits per call.\n\n" +
|
|
19
|
-
"EXAMPLE
|
|
14
|
+
"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
|
+
"EXAMPLE (already in UTC):\n" +
|
|
20
17
|
" datetime='1990-04-15T19:30:00Z'",
|
|
21
18
|
inputSchema: {
|
|
22
19
|
type: "object",
|
|
23
20
|
properties: {
|
|
24
21
|
datetime: {
|
|
25
22
|
type: "string",
|
|
26
|
-
description:
|
|
27
|
-
"Human Design
|
|
23
|
+
description: DATETIME_DESC +
|
|
24
|
+
" Human Design is time-sensitive — accuracy to the minute matters.",
|
|
28
25
|
},
|
|
26
|
+
timezone: TIMEZONE_PROPERTY,
|
|
29
27
|
style: {
|
|
30
28
|
type: "string",
|
|
31
29
|
enum: ["light", "dark", "mono"],
|
|
@@ -46,7 +44,7 @@ registerTool({
|
|
|
46
44
|
handler: async (args) => {
|
|
47
45
|
validateRequired(args, ["datetime"]);
|
|
48
46
|
const body = {
|
|
49
|
-
birth_datetime_utc:
|
|
47
|
+
birth_datetime_utc: localToUtcIso("datetime", args.datetime, args.timezone),
|
|
50
48
|
};
|
|
51
49
|
const format = pickEnum(args.format, ["svg", "png"]) || "svg";
|
|
52
50
|
const style = pickEnum(args.style, ["light", "dark", "mono"]);
|