@openephemeris/mcp-server 4.16.0 → 4.18.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 +46 -0
- package/README.md +15 -15
- package/config/dev-allowlist.json +31 -19
- package/dist/backend/client.d.ts +57 -1
- package/dist/backend/client.js +125 -15
- package/dist/prompts.js +26 -24
- package/dist/server-sse.d.ts +18 -0
- package/dist/server-sse.js +41 -6
- package/dist/tools/apps/_render-token.d.ts +34 -0
- package/dist/tools/apps/_render-token.js +50 -0
- package/dist/tools/apps/bazi-app.js +36 -12
- package/dist/tools/apps/bi-wheel-app.d.ts +9 -2
- package/dist/tools/apps/bi-wheel-app.js +76 -117
- package/dist/tools/apps/bodygraph-app.js +88 -41
- package/dist/tools/apps/chart-wheel-app.js +56 -16
- package/dist/tools/apps/location-tools.js +10 -3
- package/dist/tools/apps/moon-phase-app.js +10 -2
- package/dist/tools/apps/transit-timeline-app.d.ts +3 -0
- package/dist/tools/apps/transit-timeline-app.js +77 -9
- package/dist/tools/apps/vedic-chart-app.js +10 -1
- package/dist/tools/datetime-historical.js +7 -2
- package/dist/tools/datetime.js +2 -1
- package/dist/tools/dev.js +7 -6
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/ephemeris_extended.js +19 -4
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/moon.d.ts +1 -1
- package/dist/tools/specialized/moon.js +51 -43
- package/dist/tools/specialized/progressed.js +2 -25
- package/dist/tools/specialized/transits.js +5 -5
- package/dist/ui/bazi.html +1523 -1522
- package/dist/ui/bi-wheel.html +406 -374
- package/dist/ui/bodygraph.html +83 -79
- package/dist/ui/chart-wheel.html +394 -361
- package/dist/ui/transit-timeline.html +3 -3
- package/dist/ui/vedic-chart.html +818 -818
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ import { fileURLToPath } from "node:url";
|
|
|
21
21
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
22
22
|
import { getActiveClient } from "../../backend/client.js";
|
|
23
23
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
24
|
-
import { DATETIME_DESC, TIMEZONE_PROPERTY } from "../datetime.js";
|
|
24
|
+
import { DATETIME_DESC, TIMEZONE_PROPERTY, toDateTimeInputBody } from "../datetime.js";
|
|
25
25
|
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
26
26
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
27
27
|
export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
|
|
@@ -159,9 +159,10 @@ registerTool({
|
|
|
159
159
|
items: { type: "string" },
|
|
160
160
|
description: "Optional list of body names to include. " +
|
|
161
161
|
"Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
|
|
162
|
-
"Use 'all' as a single item to include every available body (
|
|
162
|
+
"Use 'all' as a single item to include every available body (the three Liliths — mean, true and interpolated — Ceres, Juno, Vesta, Pallas, Pholus, " +
|
|
163
163
|
"the 8 trans-Neptunian objects — eris, sedna, makemake, haumea, quaoar, orcus, ixion, varuna — and the " +
|
|
164
164
|
"8 Uranian/Hamburg-School points — cupido, hades, zeus, kronos, apollon, admetos, vulkanus, poseidon). " +
|
|
165
|
+
"Lilith slugs: 'lilith' (mean), 'lilith_true', 'lilith_interpolated'. " +
|
|
165
166
|
"Example: ['sun','moon','lilith','sedna'].",
|
|
166
167
|
},
|
|
167
168
|
},
|
|
@@ -410,19 +411,40 @@ registerTool({
|
|
|
410
411
|
const baseBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
411
412
|
let endpoint = "/ephemeris/natal-chart";
|
|
412
413
|
let requestBody = baseBody;
|
|
414
|
+
// Where the chart sits in the reply: natal-chart returns it top-level,
|
|
415
|
+
// /ephemeris/progressed under `data`, the return endpoints under `chart`.
|
|
416
|
+
let chartKey = null;
|
|
417
|
+
const target = toDateTimeInputBody("target_datetime", String(args.target_datetime ?? datetime), args.timezone);
|
|
413
418
|
if (chartType === "solar_return") {
|
|
419
|
+
// SolarReturnRequest is flat — birth_datetime at the top level, no `subject`.
|
|
414
420
|
endpoint = "/predictive/returns/solar";
|
|
415
|
-
|
|
421
|
+
const subject = baseBody.subject;
|
|
422
|
+
requestBody = {
|
|
423
|
+
birth_datetime: toDateTimeInputBody("datetime", datetime, args.timezone),
|
|
424
|
+
birth_location: subject.birth_location,
|
|
425
|
+
target_datetime: target,
|
|
426
|
+
house_system: baseBody.configuration.house_system,
|
|
427
|
+
};
|
|
428
|
+
chartKey = "chart";
|
|
416
429
|
}
|
|
417
430
|
else if (chartType === "progressed") {
|
|
418
431
|
// openapi.json exposes /ephemeris/progressed — /predictive/progressed
|
|
419
432
|
// does not exist and 404'd every progressed recalculate in the iframe.
|
|
420
433
|
endpoint = "/ephemeris/progressed";
|
|
421
|
-
requestBody
|
|
422
|
-
|
|
434
|
+
requestBody = {
|
|
435
|
+
subject: baseBody.subject,
|
|
436
|
+
configuration: { house_system: baseBody.configuration.house_system },
|
|
437
|
+
target_datetime: target,
|
|
438
|
+
progression_options: { method: "secondary" },
|
|
439
|
+
};
|
|
440
|
+
chartKey = "data";
|
|
423
441
|
}
|
|
424
442
|
// Fetch updated chart JSON only — chart renders client-side in the iframe
|
|
425
|
-
const
|
|
443
|
+
const reply = await client.post(endpoint, requestBody);
|
|
444
|
+
const chartData = chartKey ? reply[chartKey] : reply;
|
|
445
|
+
if (!chartData || typeof chartData !== "object") {
|
|
446
|
+
throw new Error(`Unexpected ${chartType} response: no \`${chartKey}\` chart in the API reply.`);
|
|
447
|
+
}
|
|
426
448
|
// Normalise through buildModelPayload so house-switch results use the same
|
|
427
449
|
// format as the initial response.
|
|
428
450
|
const payload = buildModelPayload(chartData, {
|
|
@@ -457,7 +479,10 @@ const CLASSICAL_PLANETS = new Set([
|
|
|
457
479
|
"sun", "moon", "mercury", "venus", "mars",
|
|
458
480
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
459
481
|
// Nodes use space form to match the renderer's glyph table (chart-renderer.ts).
|
|
460
|
-
"chiron", "
|
|
482
|
+
"chiron", "asc", "mc",
|
|
483
|
+
// The four lunar nodes keep the API's own names (lowercased): Mean and True
|
|
484
|
+
// are different points and never share a name.
|
|
485
|
+
"north node (mean)", "north node (true)", "south node (mean)", "south node (true)",
|
|
461
486
|
]);
|
|
462
487
|
/**
|
|
463
488
|
* Canonicalise the Go backend's verbose node keys ("North Node (Mean)" /
|
|
@@ -469,25 +494,40 @@ const CLASSICAL_PLANETS = new Set([
|
|
|
469
494
|
function canonicalizeBodyName(raw) {
|
|
470
495
|
const lower = raw.toLowerCase().trim();
|
|
471
496
|
if (lower === "north node (true)" || lower === "true node")
|
|
472
|
-
return "
|
|
497
|
+
return "north node (true)";
|
|
473
498
|
if (lower === "north node (mean)" || lower === "mean node" || lower === "north node")
|
|
474
|
-
return "north node";
|
|
475
|
-
if (lower === "south node (
|
|
476
|
-
return "south node";
|
|
499
|
+
return "north node (mean)";
|
|
500
|
+
if (lower === "south node (true)" || lower === "true south node")
|
|
501
|
+
return "south node (true)";
|
|
502
|
+
if (lower === "south node (mean)" || lower === "south node")
|
|
503
|
+
return "south node (mean)";
|
|
504
|
+
// Lilith: three different points (mean apogee 12, osculating 13, interpolated
|
|
505
|
+
// 40), each kept under the API's own name — never merged into one "lilith".
|
|
506
|
+
if (lower === "lilith (true)" || lower === "lilith_true" || lower === "true_lilith" || lower === "osculating apogee")
|
|
507
|
+
return "lilith (true)";
|
|
508
|
+
if (lower === "lilith (interpolated)" || lower === "lilith_interpolated" || lower === "lilith_natural" || lower === "interpolated apogee")
|
|
509
|
+
return "lilith (interpolated)";
|
|
510
|
+
if (lower === "lilith (mean)" || lower === "lilith" || lower === "mean_lilith" || lower === "mean apogee" || lower === "black moon lilith")
|
|
511
|
+
return "lilith (mean)";
|
|
477
512
|
return lower;
|
|
478
513
|
}
|
|
479
514
|
const EXTENDED_BODIES = new Set([
|
|
480
515
|
// Slugs match the Go API's ephemerisIDForSlug (handler_wheel_resolver.go) so
|
|
481
|
-
// additional_objects entries actually resolve server-side.
|
|
482
|
-
|
|
516
|
+
// additional_objects entries actually resolve server-side. Only ephemeris
|
|
517
|
+
// bodies belong here: vertex / part_of_fortune are not additional_objects and
|
|
518
|
+
// made the API reject every bodies:["all"] request.
|
|
519
|
+
"lilith", "mean_lilith", "lilith_true", "lilith_interpolated",
|
|
483
520
|
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
484
|
-
"vertex", "part_of_fortune",
|
|
485
521
|
// Trans-Neptunian objects (all 8 — #603 catalog expansion)
|
|
486
522
|
"eris", "sedna", "makemake", "haumea", "quaoar", "orcus", "ixion", "varuna",
|
|
487
523
|
// Uranian / Hamburg School hypothetical points (all 8 — #603 catalog expansion)
|
|
488
524
|
"cupido", "hades", "zeus", "kronos", "apollon", "admetos", "vulkanus", "poseidon",
|
|
489
525
|
]);
|
|
490
|
-
|
|
526
|
+
// Names as they come back from the API (canonicalised), for display filtering.
|
|
527
|
+
const ALL_KNOWN_BODIES = new Set([
|
|
528
|
+
...CLASSICAL_PLANETS, ...EXTENDED_BODIES,
|
|
529
|
+
"lilith (mean)", "lilith (true)", "lilith (interpolated)",
|
|
530
|
+
]);
|
|
491
531
|
/**
|
|
492
532
|
* Build a lean, SVG-free payload for Claude's text context.
|
|
493
533
|
* Strips SVG, filters to classical planets only, and packages birth params.
|
|
@@ -617,7 +657,7 @@ function buildChartSummary(data, location, houseSystem) {
|
|
|
617
657
|
const CLASSICAL_ORDER = [
|
|
618
658
|
"sun", "moon", "mercury", "venus", "mars",
|
|
619
659
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
620
|
-
"chiron", "
|
|
660
|
+
"chiron", "north node (mean)", "north node (true)", "south node (mean)", "south node (true)", "asc", "mc",
|
|
621
661
|
];
|
|
622
662
|
planetsArray.sort((a, b) => {
|
|
623
663
|
const ai = CLASSICAL_ORDER.indexOf(a.name.toLowerCase());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
2
|
+
import { getActiveClient, isBillingOrAuthError } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { TZDATA_AUTHORITATIVE_FROM_YEAR } from "../datetime-historical.js";
|
|
5
5
|
import { isQualifiedForTopHit } from "./_location-resolver.js";
|
|
@@ -245,7 +245,11 @@ registerTool({
|
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
-
catch {
|
|
248
|
+
catch (err) {
|
|
249
|
+
// The credit wall / auth / tier gate is not "server unavailable":
|
|
250
|
+
// surface it with its top-up link instead of a quiet estimate.
|
|
251
|
+
if (isBillingOrAuthError(err))
|
|
252
|
+
throw err;
|
|
249
253
|
// Server path unavailable — the tzdata estimate stands, still
|
|
250
254
|
// labelled historical_estimate. Degrading to a labelled
|
|
251
255
|
// estimate is acceptable; failing the whole search is not.
|
|
@@ -349,7 +353,10 @@ registerTool({
|
|
|
349
353
|
};
|
|
350
354
|
}
|
|
351
355
|
}
|
|
352
|
-
catch {
|
|
356
|
+
catch (err) {
|
|
357
|
+
// Credit wall / auth / tier gate: surface it, don't degrade.
|
|
358
|
+
if (isBillingOrAuthError(err))
|
|
359
|
+
throw err;
|
|
353
360
|
// Server path unavailable — fall through to the local
|
|
354
361
|
// tzdata estimate below, flagged as such.
|
|
355
362
|
}
|
|
@@ -15,7 +15,7 @@ import fs from "node:fs";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
18
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
18
|
+
import { getActiveClient, rethrowBillingOrAuth } from "../../backend/client.js";
|
|
19
19
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
20
20
|
import { TIMEZONE_PROPERTY } from "../datetime.js";
|
|
21
21
|
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
@@ -111,8 +111,16 @@ async function computeMoonData(args) {
|
|
|
111
111
|
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
112
112
|
client.request("GET", "/ephemeris/moon/aspects", { params }),
|
|
113
113
|
]);
|
|
114
|
+
// A credit wall (402), expired auth (401) or tier gate (403) on ANY of the
|
|
115
|
+
// three calls must surface as the thrown error — with its top-up / sign-in
|
|
116
|
+
// link — not as an "Unknown ?%" dial that reads like real data. Only
|
|
117
|
+
// non-billing partial failures (VOC or aspects down) degrade gracefully.
|
|
118
|
+
rethrowBillingOrAuth([phase, voc, aspects]);
|
|
119
|
+
// Without the phase there is no dial to draw; fail rather than render "Unknown".
|
|
120
|
+
if (phase.status === "rejected")
|
|
121
|
+
throw phase.reason;
|
|
114
122
|
const mergedData = {
|
|
115
|
-
phase: phase.
|
|
123
|
+
phase: phase.value,
|
|
116
124
|
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
117
125
|
aspects: aspects.status === "fulfilled" ? aspects.value : null,
|
|
118
126
|
};
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* chart to extract real planetary longitudes, searches predictive transits
|
|
9
9
|
* against those targets, then flattens the (planet@target → hits) result map
|
|
10
10
|
* into a single chronological array the iframe renders as a vertical timeline.
|
|
11
|
+
* The search asks for orb windows (orb_deg) too, so every exact hit carries the
|
|
12
|
+
* in-orb window it belongs to — when the transit starts and ends, and which
|
|
13
|
+
* pass it is when a retrograde brings the planet back over the same point.
|
|
11
14
|
*
|
|
12
15
|
* Also exports resource helpers (getTransitTimelineBundle, etc.) for use in
|
|
13
16
|
* index.ts and server-sse.ts.
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* chart to extract real planetary longitudes, searches predictive transits
|
|
9
9
|
* against those targets, then flattens the (planet@target → hits) result map
|
|
10
10
|
* into a single chronological array the iframe renders as a vertical timeline.
|
|
11
|
+
* The search asks for orb windows (orb_deg) too, so every exact hit carries the
|
|
12
|
+
* in-orb window it belongs to — when the transit starts and ends, and which
|
|
13
|
+
* pass it is when a retrograde brings the planet back over the same point.
|
|
11
14
|
*
|
|
12
15
|
* Also exports resource helpers (getTransitTimelineBundle, etc.) for use in
|
|
13
16
|
* index.ts and server-sse.ts.
|
|
@@ -67,18 +70,32 @@ function normDeg(d) {
|
|
|
67
70
|
return ((d % 360) + 360) % 360;
|
|
68
71
|
}
|
|
69
72
|
// ── Summary builder ──────────────────────────────────────────────────────────
|
|
70
|
-
function
|
|
73
|
+
function shortDate(iso) {
|
|
74
|
+
return iso ? new Date(iso).toISOString().slice(0, 10) : "?";
|
|
75
|
+
}
|
|
76
|
+
function buildSummary(transits, windows, aspectLabel, orbDeg, win) {
|
|
77
|
+
const active = windows.filter((w) => w.phase_at_start);
|
|
78
|
+
const activeLines = active.slice(0, 6).map((w) => " • " + w.planet_name + " " + (w.aspect ?? aspectLabel) + (w.natal_point ? " natal " + w.natal_point : "") +
|
|
79
|
+
" — " + w.phase_at_start + ", " + (Math.round((w.orb_at_start ?? 0) * 100) / 100) + "° from exact, in orb until " +
|
|
80
|
+
shortDate(w.leave));
|
|
81
|
+
const activeBlock = activeLines.length
|
|
82
|
+
? "\n\nAlready in orb (" + orbDeg + "°) at the start:\n" + activeLines.join("\n")
|
|
83
|
+
: "";
|
|
71
84
|
if (transits.length === 0) {
|
|
72
|
-
return "**Transit Timeline** — no exact transit hits matched the search window and criteria.";
|
|
85
|
+
return "**Transit Timeline** — no exact transit hits matched the search window and criteria." + activeBlock;
|
|
73
86
|
}
|
|
74
87
|
const planets = [...new Set(transits.map((t) => t.planet_name).filter(Boolean))];
|
|
75
88
|
const lines = transits.slice(0, 8).map((t) => {
|
|
76
89
|
const asp = t.aspect ?? aspectLabel;
|
|
77
90
|
const natal = t.natal_point ? " natal " + t.natal_point : "";
|
|
78
91
|
const when = new Date(t.date).toISOString().slice(0, 10);
|
|
92
|
+
const passes = t.pass_count > 1 ? ", pass " + t.pass_index + " of " + t.pass_count : "";
|
|
93
|
+
const inOrb = t.window_start !== undefined
|
|
94
|
+
? " — in orb " + shortDate(t.window_start) + " → " + shortDate(t.window_end) + passes
|
|
95
|
+
: "";
|
|
79
96
|
return " • " + when + " — " + t.planet_name + " " + asp + natal +
|
|
80
97
|
(t.zodiac_sign ? " (" + (t.zodiac_degree ?? 0) + "° " + t.zodiac_sign + ")" : "") +
|
|
81
|
-
(t.is_retrograde ? " ℞" : "");
|
|
98
|
+
(t.is_retrograde ? " ℞" : "") + inOrb;
|
|
82
99
|
});
|
|
83
100
|
let summary = "**Transit Timeline** — " + transits.length + (transits.length === 1 ? " event" : " events") +
|
|
84
101
|
" from " + win.start_date + " to " + win.end_date +
|
|
@@ -86,6 +103,7 @@ function buildSummary(transits, aspectLabel, win) {
|
|
|
86
103
|
lines.join("\n");
|
|
87
104
|
if (transits.length > 8)
|
|
88
105
|
summary += "\n … and " + (transits.length - 8) + " more.";
|
|
106
|
+
summary += activeBlock;
|
|
89
107
|
summary += "\n\nClick any transit for details and interpretation.";
|
|
90
108
|
return summary;
|
|
91
109
|
}
|
|
@@ -97,12 +115,13 @@ registerTool({
|
|
|
97
115
|
"astrologically for me in 2027' — a forecast they should SEE. Returns an interactive, " +
|
|
98
116
|
"date-ordered timeline (inline in Claude and ChatGPT; text summary elsewhere) of exact transit " +
|
|
99
117
|
"hits grouped by month — transiting planet glyph, the natal point it contacts, the aspect, zodiac " +
|
|
100
|
-
"position and retrograde markers — each clickable for a focused interpretation
|
|
118
|
+
"position and retrograde markers — each clickable for a focused interpretation. Every hit carries its " +
|
|
119
|
+
"in-orb window (when the transit starts and ends, and which pass it is when a retrograde repeats it), " +
|
|
120
|
+
"and transits already in orb at the start are listed with applying/separating.\n\n" +
|
|
101
121
|
"ASPECT ANGLES: 0 = conjunction/return (default), 180 = opposition, 90 = square, 120 = trine, 60 " +
|
|
102
|
-
"= sextile. EFFICIENCY: specify transiting_planets and natal_points to keep compute fast.
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"CREDIT COST: 6 credits per call (natal chart + predictive transit search).\n\n" +
|
|
122
|
+
"= sextile. EFFICIENCY: specify transiting_planets and natal_points to keep compute fast. " +
|
|
123
|
+
"MAX SPAN and CREDIT COST as " +
|
|
124
|
+
"ephemeris_transits (6 for ≤1 year).\n\n" +
|
|
106
125
|
"Do not use for transit data as JSON — use ephemeris_transits; for the sky today with no birth " +
|
|
107
126
|
"data use electional_moment_analysis; to see one date's transits drawn around the natal wheel use " +
|
|
108
127
|
"explore_bi_wheel (mode='transit').",
|
|
@@ -127,6 +146,10 @@ registerTool({
|
|
|
127
146
|
type: "number",
|
|
128
147
|
description: "Aspect angle in degrees. 0 = conjunction/return (default), 180 = opposition, 90 = square, 120 = trine, 60 = sextile.",
|
|
129
148
|
},
|
|
149
|
+
orb_deg: {
|
|
150
|
+
type: "number",
|
|
151
|
+
description: "Orb in degrees that defines when a transit starts and ends (the in-orb window around each exact hit). Default 1. Greater than 0, at most 15.",
|
|
152
|
+
},
|
|
130
153
|
},
|
|
131
154
|
required: ["natal_datetime", "natal_latitude", "natal_longitude", "start_date", "end_date"],
|
|
132
155
|
additionalProperties: false,
|
|
@@ -222,10 +245,12 @@ registerTool({
|
|
|
222
245
|
let endStr = args.end_date;
|
|
223
246
|
if (/^\d{4}-\d{2}-\d{2}$/.test(endStr))
|
|
224
247
|
endStr += "T23:59:59Z";
|
|
248
|
+
const orbDeg = typeof args.orb_deg === "number" && args.orb_deg > 0 ? Math.min(args.orb_deg, 15) : 1;
|
|
225
249
|
const transitBody = {
|
|
226
250
|
start_date: startStr,
|
|
227
251
|
end_date: endStr,
|
|
228
252
|
target_degrees: effectiveTargets,
|
|
253
|
+
orb_deg: orbDeg,
|
|
229
254
|
};
|
|
230
255
|
if (args.transiting_planets)
|
|
231
256
|
transitBody.planet_names = args.transiting_planets;
|
|
@@ -278,10 +303,53 @@ registerTool({
|
|
|
278
303
|
}
|
|
279
304
|
}
|
|
280
305
|
transits.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
306
|
+
// ── Step 5: orb windows → attach each exact hit to its window ────────────
|
|
307
|
+
// A window groups every pass the planet makes without leaving the orb, so
|
|
308
|
+
// a retrograde triple pass is one window with three exacts. Hits are
|
|
309
|
+
// matched to windows by planet, target degree and exact time.
|
|
310
|
+
const rawWindows = Array.isArray(transitResult?.windows) ? transitResult.windows : [];
|
|
311
|
+
const windows = rawWindows.map((w) => {
|
|
312
|
+
const target = typeof w?.target_longitude === "number" ? w.target_longitude : null;
|
|
313
|
+
return {
|
|
314
|
+
planet: String(w?.planet_name ?? "").toLowerCase(),
|
|
315
|
+
planet_name: String(w?.planet_name ?? ""),
|
|
316
|
+
natal_point: target != null ? nearestPoint(target) : "",
|
|
317
|
+
aspect: aspectLabel,
|
|
318
|
+
target_longitude: target,
|
|
319
|
+
orb_deg: w?.orb_deg ?? orbDeg,
|
|
320
|
+
enter: w?.enter ?? null,
|
|
321
|
+
leave: w?.leave ?? null,
|
|
322
|
+
exacts: Array.isArray(w?.exacts)
|
|
323
|
+
? w.exacts.map((e) => ({ time: e?.time, is_retrograde: Boolean(e?.is_retrograde) }))
|
|
324
|
+
: [],
|
|
325
|
+
duration_days: w?.duration_days ?? null,
|
|
326
|
+
phase_at_start: w?.phase_at_start ?? null,
|
|
327
|
+
orb_at_start: w?.orb_at_start ?? null,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
for (const t of transits) {
|
|
331
|
+
const hitMs = new Date(t.date).getTime();
|
|
332
|
+
for (const w of windows) {
|
|
333
|
+
if (w.planet_name !== t.planet_name || w.target_longitude == null || t.target_longitude == null)
|
|
334
|
+
continue;
|
|
335
|
+
if (Math.abs(w.target_longitude - t.target_longitude) > 0.01)
|
|
336
|
+
continue;
|
|
337
|
+
const idx = w.exacts.findIndex((e) => Math.abs(new Date(e.time).getTime() - hitMs) <= 60_000);
|
|
338
|
+
if (idx < 0)
|
|
339
|
+
continue;
|
|
340
|
+
t.window_start = w.enter;
|
|
341
|
+
t.window_end = w.leave;
|
|
342
|
+
t.pass_index = idx + 1;
|
|
343
|
+
t.pass_count = w.exacts.length;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
281
347
|
const win = { start_date: startStr.slice(0, 10), end_date: endStr.slice(0, 10) };
|
|
282
|
-
const summary = buildSummary(transits, aspectLabel, win);
|
|
348
|
+
const summary = buildSummary(transits, windows, aspectLabel, orbDeg, win);
|
|
283
349
|
const uiPayload = {
|
|
284
350
|
transits,
|
|
351
|
+
windows,
|
|
352
|
+
orb_deg: orbDeg,
|
|
285
353
|
natal_positions: natalPositionMap,
|
|
286
354
|
aspect_angle: aspectAngle,
|
|
287
355
|
aspect_label: aspectLabel,
|
|
@@ -25,6 +25,7 @@ import { getActiveClient } from "../../backend/client.js";
|
|
|
25
25
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
26
26
|
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
27
27
|
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
28
|
+
import { RENDER_TOKEN_PROPERTY, renderTokenFromVisual, renderTokenHeaders } from "./_render-token.js";
|
|
28
29
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
30
|
export const VEDIC_CHART_RESOURCE_URI = "ui://openephemeris/vedic-chart";
|
|
30
31
|
export const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -75,12 +76,14 @@ function buildVedicModelPayload(chartData, birthParams, theme) {
|
|
|
75
76
|
is_retrograde: retrogradeSet.has(String(p.planet ?? "")),
|
|
76
77
|
}));
|
|
77
78
|
const visual = chartData.visual;
|
|
79
|
+
const renderToken = renderTokenFromVisual(visual);
|
|
78
80
|
return {
|
|
79
81
|
ayanamsa: String(meta.ayanamsha ?? "lahiri").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
80
82
|
lagna: lagnaSign,
|
|
81
83
|
planets,
|
|
82
84
|
_svg: visual?.data,
|
|
83
85
|
_theme: theme,
|
|
86
|
+
...(renderToken ? { _render_token: renderToken } : {}),
|
|
84
87
|
_birth_params: birthParams,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
@@ -237,6 +240,7 @@ registerTool({
|
|
|
237
240
|
enum: ["light", "dark"],
|
|
238
241
|
description: "Render palette for the Rashi grid SVG. Mirrors the MCP host's light/dark color scheme.",
|
|
239
242
|
},
|
|
243
|
+
render_token: RENDER_TOKEN_PROPERTY,
|
|
240
244
|
},
|
|
241
245
|
required: ["datetime"],
|
|
242
246
|
},
|
|
@@ -259,7 +263,12 @@ registerTool({
|
|
|
259
263
|
};
|
|
260
264
|
if (ayanamsa)
|
|
261
265
|
body.ayanamsa = ayanamsa;
|
|
262
|
-
|
|
266
|
+
// A theme reconcile of the chart explore_vedic_chart just rendered presents
|
|
267
|
+
// that render's token and is not billed again (same account + inputs).
|
|
268
|
+
const chartData = await client.request("POST", "/vedic/chart", {
|
|
269
|
+
data: body,
|
|
270
|
+
headers: renderTokenHeaders(args.render_token),
|
|
271
|
+
});
|
|
263
272
|
const modelPayload = buildVedicModelPayload(chartData, {
|
|
264
273
|
datetime,
|
|
265
274
|
location: null,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* beats an outage; the server remains the single authority whenever it is
|
|
24
24
|
* reachable.
|
|
25
25
|
*/
|
|
26
|
-
import { getActiveClient } from "../backend/client.js";
|
|
26
|
+
import { getActiveClient, isBillingOrAuthError } from "../backend/client.js";
|
|
27
27
|
import { hasZoneSuffix, isNaiveClockTime, localToUtcIso } from "./datetime.js";
|
|
28
28
|
/** Where IANA tzdata stops being reference-city best effort. */
|
|
29
29
|
export const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
@@ -69,7 +69,12 @@ export async function localToUtcIsoHistorical(field, dt, tz, coords, timezoneFie
|
|
|
69
69
|
return res.resolved_utc;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
catch {
|
|
72
|
+
catch (err) {
|
|
73
|
+
// Out of credits / signed out / tier-gated: surface the credit wall
|
|
74
|
+
// now rather than silently converting with the pre-1970-wrong tzdata
|
|
75
|
+
// offset (the chart call that follows would hit the same wall anyway).
|
|
76
|
+
if (isBillingOrAuthError(err))
|
|
77
|
+
throw err;
|
|
73
78
|
// Server unreachable or rejected the request — fall through to the
|
|
74
79
|
// local conversion rather than failing the chart call.
|
|
75
80
|
}
|
package/dist/tools/datetime.js
CHANGED
|
@@ -81,7 +81,8 @@ export const DATETIME_CONTRACT_INSTRUCTIONS = "Datetime contract: every clock ti
|
|
|
81
81
|
"the naive local time and name the zone in the sibling `timezone` argument (IANA name, " +
|
|
82
82
|
"e.g. `America/Chicago`). A zone-less clock time is a hard 400 — the engine never " +
|
|
83
83
|
"guesses UTC, because an unstated zone shifts the Ascendant by ~15° per hour and moves " +
|
|
84
|
-
"every house placement. A bare date (no clock time) resolves to 12:00 UTC
|
|
84
|
+
"every house placement. A bare date (no clock time) resolves to 12:00 UTC. A pre-1970 " +
|
|
85
|
+
"local time with coordinates adds 1 credit (historical timezone lookup).\n\n" +
|
|
85
86
|
"PREFER the local-time + `timezone` form over converting to UTC yourself. The server " +
|
|
86
87
|
"resolves the IANA zone against the actual date, including historical DST rules — that " +
|
|
87
88
|
"arithmetic (which offset applied on that specific day, in that specific year) is easy " +
|
package/dist/tools/dev.js
CHANGED
|
@@ -46,10 +46,11 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
|
|
|
46
46
|
"Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
|
|
47
47
|
// Prices mirror go-sidecar/internal/api/auth/usage_meter.go; tiers mirror
|
|
48
48
|
// auth/middleware.go requiredTierForPath. /iching/* aliases /human-design/*.
|
|
49
|
-
"CREDIT COST: 1 for most
|
|
50
|
-
"(
|
|
51
|
-
"/
|
|
52
|
-
"
|
|
49
|
+
"CREDIT COST: 1 for most; /predictive/* 5 (transits/search 5-70 by span); /comparative/* 3; " +
|
|
50
|
+
"/calendar/* 10 (moon-phases, lunar-standstill 2; cross-quarter 5); /acg/* 10 (Pro); " +
|
|
51
|
+
"/electional/* 5 (Pro, span-priced searches; moment-analysis, station-tracker free); " +
|
|
52
|
+
"/human-design/*, /iching/* 2 (composite, transit-chart 3; penta 2/member); catalogs 0; " +
|
|
53
|
+
"4xx/5xx refunded. Query {format: 'llm'} = compact output.\n\n";
|
|
53
54
|
const READ_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
54
55
|
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
55
56
|
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
@@ -60,13 +61,13 @@ const READ_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
|
60
61
|
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
61
62
|
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
62
63
|
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
63
|
-
" GET /calendar/astrology/moon-phases —
|
|
64
|
+
" GET /calendar/astrology/moon-phases — next New/Quarter/Full Moon after a date or jd\n" +
|
|
64
65
|
" GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
|
|
65
66
|
" GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
|
|
66
67
|
" GET /catalogs/bodies — List all supported celestial bodies\n";
|
|
67
68
|
const WRITE_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
68
69
|
" POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
|
|
69
|
-
" POST /ephemeris/natal/batch — Up to
|
|
70
|
+
" POST /ephemeris/natal/batch — Up to 100 natal charts in one request (Startup tier)\n" +
|
|
70
71
|
" POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
|
|
71
72
|
" POST /predictive/transits/search — Transit event search over a date range\n" +
|
|
72
73
|
" POST /predictive/returns/solar — Solar return chart\n" +
|
|
@@ -10,7 +10,8 @@ registerTool({
|
|
|
10
10
|
"date range, 'find me a good day next month' or 'pick an auspicious window'. Returns the best " +
|
|
11
11
|
"continuous timing windows in the range, scored hourly on essential dignity, aspect quality, sect " +
|
|
12
12
|
"and void-of-course Moon penalties, with optional filters (avoid_voc, lunar_phase).\n\n" +
|
|
13
|
-
"CREDIT COST: 5
|
|
13
|
+
"CREDIT COST: ≤30 days 5, ≤60 days 8, ≤120 days 12. Pro tier; max window Pro 30 days, Startup " +
|
|
14
|
+
"60, Scale 120.\n\n" +
|
|
14
15
|
"Do not use to score one specific moment or 'right now' — use electional_moment_analysis; for " +
|
|
15
16
|
"retrograde dates use electional_station_tracker.",
|
|
16
17
|
inputSchema: {
|
|
@@ -222,7 +223,7 @@ registerTool({
|
|
|
222
223
|
description: "Find all active aspects between planets at a specific moment. Returns aspect type, " +
|
|
223
224
|
"orb, quality score, and whether it's applying or separating. Great for checking " +
|
|
224
225
|
"the 'weather' of a given day.\n\n" +
|
|
225
|
-
"CREDIT COST: 5 credits per call.\n\n" +
|
|
226
|
+
"CREDIT COST: 5 credits per call. Pro tier.\n\n" +
|
|
226
227
|
"EXAMPLE: What aspects are active on March 21, 2026?\n" +
|
|
227
228
|
" date='2026-03-21T12:00:00Z'",
|
|
228
229
|
inputSchema: {
|
|
@@ -276,7 +277,7 @@ registerTool({
|
|
|
276
277
|
"Sun, Moon, Mercury, Venus, Mars and the lunar nodes — the same body set and formula the CCG hybrid " +
|
|
277
278
|
"transit map (/acg/ccg mode=ccg_hybrid) draws with; every other body stays a transit. Progressed " +
|
|
278
279
|
"crossings carry progressed: true and search_summary.progression names what was progressed.\n\n" +
|
|
279
|
-
"CREDIT COST: 5
|
|
280
|
+
"CREDIT COST: priced by start→end span — ≤1 month 5, ≤3 months 8, ≤6 months 10, ≤1 year 12. Pro tier.\n\n" +
|
|
280
281
|
"EXAMPLE: When does Mercury cross the Descendant over Warsaw on 2026-07-30?\n" +
|
|
281
282
|
" latitude=52.2297, longitude=21.0122, bodies='Mercury', angles='DC',\n" +
|
|
282
283
|
" start='2026-07-30T00:00:00Z', end='2026-07-31T00:00:00Z'",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTool, validateRequired, validateCoordinates } from "../index.js";
|
|
2
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
2
|
+
import { getActiveClient, rethrowBillingOrAuth } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime, toDateTimeInputBody } from "../datetime.js";
|
|
5
5
|
// `field`/`timezoneField` name the caller's own argument path (e.g.
|
|
@@ -134,10 +134,19 @@ registerTool({
|
|
|
134
134
|
}
|
|
135
135
|
// Fan-out: the backend only handles one planet per call; query all 10 in parallel.
|
|
136
136
|
const PLANET_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
137
|
-
const
|
|
137
|
+
const settled = await Promise.allSettled(PLANET_IDS.map((pid) => client.post("/ephemeris/retrograde-status", {
|
|
138
138
|
date_time: toDateTimeInputBody("datetime", args.datetime, args.timezone),
|
|
139
139
|
planet_id: pid,
|
|
140
|
-
})
|
|
140
|
+
})));
|
|
141
|
+
// Out of credits / signed out / tier-gated: throw the credit-wall (or
|
|
142
|
+
// auth) error so its top-up link reaches the assistant. Swallowing it
|
|
143
|
+
// used to return `{planets: {}, success: true}` at zero credits.
|
|
144
|
+
rethrowBillingOrAuth(settled);
|
|
145
|
+
const results = settled.map((s) => (s.status === "fulfilled" ? s.value : null));
|
|
146
|
+
if (results.every((r) => r == null)) {
|
|
147
|
+
// Every planet failed for a non-billing reason — never report success with no data.
|
|
148
|
+
throw settled[0].reason;
|
|
149
|
+
}
|
|
141
150
|
// Merge into a keyed object: { planet_name: {...status} }
|
|
142
151
|
const merged = {};
|
|
143
152
|
for (const res of results) {
|
|
@@ -151,7 +160,13 @@ registerTool({
|
|
|
151
160
|
};
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
|
-
|
|
163
|
+
const failed = PLANET_IDS.filter((_, i) => results[i] == null);
|
|
164
|
+
return {
|
|
165
|
+
planets: merged,
|
|
166
|
+
datetime: args.datetime,
|
|
167
|
+
success: true,
|
|
168
|
+
...(failed.length ? { failed_planet_ids: failed } : {}),
|
|
169
|
+
};
|
|
155
170
|
},
|
|
156
171
|
});
|
|
157
172
|
// POST /ephemeris/midpoints
|
|
@@ -8,7 +8,7 @@ registerTool({
|
|
|
8
8
|
name: "human_design_composite",
|
|
9
9
|
description: "Calculate a Human Design composite chart for two people. Merges both bodygraphs " +
|
|
10
10
|
"to show shared channels, authority dynamics, and relationship type.\n\n" +
|
|
11
|
-
"CREDIT COST:
|
|
11
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
12
12
|
"EXAMPLE (local times + zones):\n" +
|
|
13
13
|
" person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
|
|
14
14
|
" person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles'\n" +
|
|
@@ -74,7 +74,7 @@ registerTool({
|
|
|
74
74
|
name: "human_design_penta",
|
|
75
75
|
description: "Calculate a Human Design Penta (group) chart for 3-5 people. Shows functional " +
|
|
76
76
|
"attributes, leadership dynamics, channels, redundancies, and a group stability score.\n\n" +
|
|
77
|
-
"CREDIT COST:
|
|
77
|
+
"CREDIT COST: 2 credits per member (6-10 for 3-5 people).\n\n" +
|
|
78
78
|
"EXAMPLE (3 people — each datetime states its zone):\n" +
|
|
79
79
|
" group_name='Team Alpha',\n" +
|
|
80
80
|
" members=[\n" +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const NEXT_LUNAR_PHASE_MAX_COUNT = 12;
|