@openephemeris/mcp-server 3.13.9 → 3.14.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 +39 -0
- package/README.md +3 -3
- package/config/dev-allowlist.json +28 -4
- package/dist/index.js +20 -13
- package/dist/oauth/dcr.d.ts +11 -0
- package/dist/oauth/dcr.js +49 -0
- package/dist/oauth/discovery.d.ts +12 -0
- package/dist/oauth/discovery.js +45 -0
- package/dist/oauth/pkce.d.ts +18 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/rate-limit.d.ts +20 -0
- package/dist/oauth/rate-limit.js +65 -0
- package/dist/oauth/store.d.ts +88 -0
- package/dist/oauth/store.js +361 -0
- package/dist/oauth/supabase-jwt.d.ts +39 -0
- package/dist/oauth/supabase-jwt.js +194 -0
- package/dist/oauth/token.d.ts +21 -0
- package/dist/oauth/token.js +215 -0
- package/dist/prompts.js +180 -159
- package/dist/server-sse.js +154 -49
- package/dist/tools/apps/bazi-app.d.ts +23 -0
- package/dist/tools/apps/bazi-app.js +226 -0
- package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
- package/dist/tools/apps/bi-wheel-app.js +347 -113
- package/dist/tools/apps/bodygraph-app.js +106 -13
- package/dist/tools/apps/chart-wheel-app.js +53 -4
- package/dist/tools/apps/location-tools.js +3 -0
- package/dist/tools/apps/moon-phase-app.d.ts +19 -0
- package/dist/tools/apps/moon-phase-app.js +244 -0
- package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
- package/dist/tools/apps/transit-timeline-app.js +295 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
- package/dist/tools/apps/vedic-chart-app.js +228 -0
- package/dist/tools/auth.js +4 -0
- package/dist/tools/dev.js +3 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +9 -4
- package/dist/tools/output-schemas.d.ts +37 -0
- package/dist/tools/output-schemas.js +130 -0
- package/dist/tools/specialized/acg.js +5 -2
- package/dist/tools/specialized/bazi.js +375 -44
- package/dist/tools/specialized/bi_wheel.js +3 -1
- package/dist/tools/specialized/chart_wheel.js +3 -1
- package/dist/tools/specialized/comparative.js +9 -4
- package/dist/tools/specialized/eclipse.js +3 -1
- package/dist/tools/specialized/electional.js +9 -4
- package/dist/tools/specialized/ephemeris_core.js +5 -2
- package/dist/tools/specialized/ephemeris_extended.js +17 -8
- package/dist/tools/specialized/hd_bodygraph.js +3 -1
- package/dist/tools/specialized/hd_cycles.js +5 -2
- package/dist/tools/specialized/hd_group.js +5 -2
- package/dist/tools/specialized/human_design.js +3 -1
- package/dist/tools/specialized/moon.js +5 -2
- package/dist/tools/specialized/natal.js +3 -1
- package/dist/tools/specialized/progressed.js +3 -1
- package/dist/tools/specialized/relocation.js +3 -1
- package/dist/tools/specialized/returns.js +7 -3
- package/dist/tools/specialized/synastry.js +3 -1
- package/dist/tools/specialized/transits.js +53 -44
- package/dist/tools/specialized/vedic.js +3 -1
- package/dist/tools/specialized/venus_star_points.js +13 -6
- package/dist/ui/bazi.html +213 -0
- package/dist/ui/bi-wheel.html +3714 -3048
- package/dist/ui/bodygraph.html +1952 -1766
- package/dist/ui/chart-wheel.html +3431 -2964
- package/dist/ui/moon-phase.html +6764 -0
- package/dist/ui/transit-timeline.html +6874 -0
- package/dist/ui/vedic-chart.html +210 -0
- package/package.json +15 -12
- package/smithery.yaml +16 -1
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bi-wheel-app.ts — MCP App tool registration for the Bi-Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_bi_wheel — primary entry point
|
|
6
|
-
*
|
|
4
|
+
* Registers 6 tools:
|
|
5
|
+
* • explore_bi_wheel — primary entry point [model + app]
|
|
6
|
+
* • bi_wheel_recalculate — re-fetch with new params [app-only]
|
|
7
7
|
* • bi_wheel_on_cross_aspect_click — cross-aspect click handler [app-only]
|
|
8
|
-
* • bi_wheel_on_planet_click — planet click handler
|
|
8
|
+
* • bi_wheel_on_planet_click — planet click handler [app-only]
|
|
9
|
+
* • bi_wheel_on_house_click — house cusp click handler [app-only]
|
|
10
|
+
* • bi_wheel_synopsis — full reading overview [model + app]
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
* synastry
|
|
12
|
-
* transit
|
|
12
|
+
* Supported modes (BiWheelMode):
|
|
13
|
+
* synastry — two natal charts (person1 inner, person2 outer)
|
|
14
|
+
* transit — natal inner + transiting planets outer
|
|
15
|
+
* progressed — natal inner + secondary progressions outer (POST /ephemeris/progressed)
|
|
16
|
+
* solar_return — natal inner + solar return chart outer (POST /predictive/returns/solar)
|
|
17
|
+
* lunar_return — natal inner + lunar return chart outer (POST /predictive/returns/lunar)
|
|
18
|
+
* solar_arc — natal inner + solar arc directions outer (client-side Naibod approximation)
|
|
13
19
|
*
|
|
14
20
|
* Cross-aspects are computed client-side (UI) AND server-side (for summary/fallback).
|
|
15
21
|
* The payload cross_aspects array is capped at 30 tightest-orb aspects.
|
|
@@ -19,6 +25,7 @@ import path from "node:path";
|
|
|
19
25
|
import { fileURLToPath } from "node:url";
|
|
20
26
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
21
27
|
import { getActiveClient } from "../../backend/client.js";
|
|
28
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
22
29
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
23
30
|
export const BI_WHEEL_RESOURCE_URI = "ui://openephemeris/bi-wheel";
|
|
24
31
|
export const BI_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -42,12 +49,59 @@ export function getBiWheelBundle() {
|
|
|
42
49
|
export function clearBiWheelBundleCache() {
|
|
43
50
|
cachedBiWheelBundle = null;
|
|
44
51
|
}
|
|
52
|
+
const ALL_MODES = [
|
|
53
|
+
"synastry", "transit", "progressed", "solar_return", "lunar_return", "solar_arc",
|
|
54
|
+
];
|
|
55
|
+
const MODE_META = {
|
|
56
|
+
synastry: {
|
|
57
|
+
outerLabel: (_, name) => name ?? "Person 2",
|
|
58
|
+
innerRole: "Person 1 (inner)",
|
|
59
|
+
outerRole: "Person 2 (outer)",
|
|
60
|
+
credits: 1,
|
|
61
|
+
modeLabel: "Synastry",
|
|
62
|
+
},
|
|
63
|
+
transit: {
|
|
64
|
+
outerLabel: (dt) => `Transit · ${dt.slice(0, 10)}`,
|
|
65
|
+
innerRole: "Natal (inner)",
|
|
66
|
+
outerRole: "Transits (outer)",
|
|
67
|
+
credits: 1,
|
|
68
|
+
modeLabel: "Transit",
|
|
69
|
+
},
|
|
70
|
+
progressed: {
|
|
71
|
+
outerLabel: (dt) => `Progressions · ${dt.slice(0, 10)}`,
|
|
72
|
+
innerRole: "Natal (inner)",
|
|
73
|
+
outerRole: "Progressed (outer)",
|
|
74
|
+
credits: 1,
|
|
75
|
+
modeLabel: "Secondary Progressions",
|
|
76
|
+
},
|
|
77
|
+
solar_return: {
|
|
78
|
+
outerLabel: (dt) => `Solar Return · ${dt.slice(0, 4)}`,
|
|
79
|
+
innerRole: "Natal (inner)",
|
|
80
|
+
outerRole: "Solar Return (outer)",
|
|
81
|
+
credits: 5,
|
|
82
|
+
modeLabel: "Solar Return",
|
|
83
|
+
},
|
|
84
|
+
lunar_return: {
|
|
85
|
+
outerLabel: (dt) => `Lunar Return · ${dt.slice(0, 10)}`,
|
|
86
|
+
innerRole: "Natal (inner)",
|
|
87
|
+
outerRole: "Lunar Return (outer)",
|
|
88
|
+
credits: 5,
|
|
89
|
+
modeLabel: "Lunar Return",
|
|
90
|
+
},
|
|
91
|
+
solar_arc: {
|
|
92
|
+
outerLabel: (dt) => `Solar Arc · ${dt.slice(0, 10)}`,
|
|
93
|
+
innerRole: "Natal (inner)",
|
|
94
|
+
outerRole: "Solar Arc Directions (outer)",
|
|
95
|
+
credits: 1,
|
|
96
|
+
modeLabel: "Solar Arc Directions",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
45
99
|
// ── House system helpers (shared with chart-wheel) ────────────────────────────
|
|
46
100
|
const HOUSE_SYSTEM_MAP = {
|
|
47
101
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
48
102
|
campanus: "C", regiomontanus: "R",
|
|
49
103
|
};
|
|
50
|
-
function buildNatalBody(datetime, lat, lon, timezone) {
|
|
104
|
+
function buildNatalBody(datetime, lat, lon, timezone, houseSystem = "placidus") {
|
|
51
105
|
const body = {
|
|
52
106
|
subject: {
|
|
53
107
|
name: "MCP Request",
|
|
@@ -58,7 +112,7 @@ function buildNatalBody(datetime, lat, lon, timezone) {
|
|
|
58
112
|
},
|
|
59
113
|
},
|
|
60
114
|
configuration: {
|
|
61
|
-
house_system: HOUSE_SYSTEM_MAP["placidus"],
|
|
115
|
+
house_system: HOUSE_SYSTEM_MAP[houseSystem] ?? HOUSE_SYSTEM_MAP["placidus"],
|
|
62
116
|
},
|
|
63
117
|
};
|
|
64
118
|
if (timezone) {
|
|
@@ -81,6 +135,108 @@ function buildTransitBody(transitDate, natalDatetime, lat, lon, timezone) {
|
|
|
81
135
|
configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
|
|
82
136
|
};
|
|
83
137
|
}
|
|
138
|
+
/** POST /ephemeris/progressed — secondary progressions */
|
|
139
|
+
function buildProgressedBody(natalDatetime, lat, lon, timezone, targetDatetime) {
|
|
140
|
+
const body = {
|
|
141
|
+
subject: {
|
|
142
|
+
name: "MCP Request",
|
|
143
|
+
birth_datetime: { iso: natalDatetime },
|
|
144
|
+
birth_location: {
|
|
145
|
+
latitude: { decimal: lat ?? 0 },
|
|
146
|
+
longitude: { decimal: lon ?? 0 },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
configuration: {
|
|
150
|
+
target_datetime: { iso: targetDatetime },
|
|
151
|
+
method: "secondary",
|
|
152
|
+
house_system: HOUSE_SYSTEM_MAP["placidus"],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
if (timezone) {
|
|
156
|
+
body.subject.birth_location.timezone = { iana_name: timezone };
|
|
157
|
+
}
|
|
158
|
+
return body;
|
|
159
|
+
}
|
|
160
|
+
/** POST /predictive/returns/solar or /predictive/returns/lunar */
|
|
161
|
+
function buildReturnBody(natalDatetime, natalLat, natalLon, timezone, targetDatetime, returnLat, returnLon) {
|
|
162
|
+
const body = {
|
|
163
|
+
subject: {
|
|
164
|
+
name: "MCP Request",
|
|
165
|
+
birth_datetime: { iso: natalDatetime },
|
|
166
|
+
birth_location: {
|
|
167
|
+
latitude: { decimal: natalLat ?? 0 },
|
|
168
|
+
longitude: { decimal: natalLon ?? 0 },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
return_location: {
|
|
172
|
+
latitude: { decimal: returnLat ?? natalLat ?? 0 },
|
|
173
|
+
longitude: { decimal: returnLon ?? natalLon ?? 0 },
|
|
174
|
+
},
|
|
175
|
+
target_datetime: { iso: targetDatetime },
|
|
176
|
+
configuration: { house_system: HOUSE_SYSTEM_MAP["placidus"] },
|
|
177
|
+
};
|
|
178
|
+
if (timezone) {
|
|
179
|
+
body.subject.birth_location.timezone = { iana_name: timezone };
|
|
180
|
+
}
|
|
181
|
+
return body;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Client-side solar arc: advance every natal planet longitude by elapsed years in degrees.
|
|
185
|
+
* Uses the Naibod rate (1 tropical year ≈ 0.9856°). The natal data object is cloned and
|
|
186
|
+
* the planets array/map is offset — houses are left at natal positions (standard practice).
|
|
187
|
+
*/
|
|
188
|
+
function applySolarArcOffset(natalData, natalDatetime, targetDatetime) {
|
|
189
|
+
const natalYear = new Date(natalDatetime).getFullYear() +
|
|
190
|
+
new Date(natalDatetime).getMonth() / 12;
|
|
191
|
+
const targetYear = new Date(targetDatetime).getFullYear() +
|
|
192
|
+
new Date(targetDatetime).getMonth() / 12;
|
|
193
|
+
const arcDegrees = (targetYear - natalYear) * 0.9856; // Naibod rate
|
|
194
|
+
const offsetPlanets = (raw) => {
|
|
195
|
+
if (Array.isArray(raw)) {
|
|
196
|
+
return raw.map((p) => ({
|
|
197
|
+
...p,
|
|
198
|
+
longitude: ((p.longitude ?? 0) + arcDegrees + 360) % 360,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
202
|
+
const out = {};
|
|
203
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
204
|
+
if (v && typeof v === "object" && "longitude" in v) {
|
|
205
|
+
out[k] = { ...v, longitude: ((v.longitude ?? 0) + arcDegrees + 360) % 360 };
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
out[k] = v;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
return raw;
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
...natalData,
|
|
217
|
+
planets: offsetPlanets(natalData.planets),
|
|
218
|
+
_solar_arc_degrees: Math.round(arcDegrees * 1000) / 1000,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** Route outer-wheel fetch to the correct API endpoint based on mode. */
|
|
222
|
+
async function fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2) {
|
|
223
|
+
switch (mode) {
|
|
224
|
+
case "synastry":
|
|
225
|
+
return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2));
|
|
226
|
+
case "transit":
|
|
227
|
+
return client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1));
|
|
228
|
+
case "progressed":
|
|
229
|
+
return client.post("/ephemeris/progressed", buildProgressedBody(dt1, lat1, lon1, tz1, dt2));
|
|
230
|
+
case "solar_return":
|
|
231
|
+
return client.post("/predictive/returns/solar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
|
|
232
|
+
case "lunar_return":
|
|
233
|
+
return client.post("/predictive/returns/lunar", buildReturnBody(dt1, lat1, lon1, tz1, dt2, lat2 ?? lat1, lon2 ?? lon1));
|
|
234
|
+
case "solar_arc": {
|
|
235
|
+
const natal = await client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1));
|
|
236
|
+
return applySolarArcOffset(natal, dt1, dt2);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
84
240
|
const CROSS_ASPECT_DEFS = [
|
|
85
241
|
{ type: "conjunction", angle: 0, orb: 8 },
|
|
86
242
|
{ type: "opposition", angle: 180, orb: 8 },
|
|
@@ -120,19 +276,39 @@ export function computeCrossAspects(planets1, planets2) {
|
|
|
120
276
|
return results.sort((a, b) => a.orb - b.orb).slice(0, 30);
|
|
121
277
|
}
|
|
122
278
|
// ── Planet normalisation ───────────────────────────────────────────────────────
|
|
279
|
+
// Canonical underscore names accepted by the renderer.
|
|
123
280
|
const CLASSICAL_PLANETS = new Set([
|
|
124
281
|
"sun", "moon", "mercury", "venus", "mars",
|
|
125
282
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
126
283
|
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
127
284
|
]);
|
|
285
|
+
/**
|
|
286
|
+
* Normalise the Go backend's verbose planet key (e.g. "North Node (Mean)")
|
|
287
|
+
* to the canonical lowercase underscore form the renderer expects.
|
|
288
|
+
*/
|
|
289
|
+
function canonicalizePlanetName(raw) {
|
|
290
|
+
const lower = raw.toLowerCase();
|
|
291
|
+
// Various node/chiron aliases the Go PlanetName() function emits
|
|
292
|
+
if (lower === "north node (mean)" || lower === "north node (true)" || lower === "mean node" || lower === "true node")
|
|
293
|
+
return "north_node";
|
|
294
|
+
if (lower === "south node (mean)" || lower === "south node (true)")
|
|
295
|
+
return "south_node";
|
|
296
|
+
if (lower === "mean apogee" || lower === "black moon lilith" || lower === "lilith (mean)" || lower === "lilith (true)")
|
|
297
|
+
return "lilith";
|
|
298
|
+
// Strip parenthetical qualifiers for any other bodies just in case
|
|
299
|
+
return lower.replace(/\s*\(.*?\)/g, "").replace(/\s+/g, "_");
|
|
300
|
+
}
|
|
128
301
|
function normalizePlanets(raw) {
|
|
129
302
|
if (Array.isArray(raw))
|
|
130
303
|
return raw;
|
|
131
304
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
132
305
|
return Object.entries(raw)
|
|
133
|
-
.filter(([k]) =>
|
|
306
|
+
.filter(([k]) => {
|
|
307
|
+
const canonical = canonicalizePlanetName(k);
|
|
308
|
+
return CLASSICAL_PLANETS.has(canonical);
|
|
309
|
+
})
|
|
134
310
|
.map(([name, p]) => ({
|
|
135
|
-
name: name
|
|
311
|
+
name: canonicalizePlanetName(name),
|
|
136
312
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
137
313
|
speed: (p.longitude_speed ?? p.speed),
|
|
138
314
|
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
@@ -154,14 +330,30 @@ function normalizeHouses(raw) {
|
|
|
154
330
|
return [];
|
|
155
331
|
}
|
|
156
332
|
// ── Payload builder ────────────────────────────────────────────────────────────
|
|
333
|
+
function extractAngles(data) {
|
|
334
|
+
const rawAngles = data.angles;
|
|
335
|
+
const rawHouseObj = data.houses;
|
|
336
|
+
const ascendant = rawAngles?.ascendant ??
|
|
337
|
+
data.ascendant ??
|
|
338
|
+
rawHouseObj?.cusps?.[0] ??
|
|
339
|
+
0;
|
|
340
|
+
const midheaven = rawAngles?.midheaven ??
|
|
341
|
+
data.midheaven ??
|
|
342
|
+
rawHouseObj?.cusps?.[9] ??
|
|
343
|
+
0;
|
|
344
|
+
return { ascendant, midheaven };
|
|
345
|
+
}
|
|
157
346
|
function buildBiWheelPayload(innerData, outerData, mode, params1, params2) {
|
|
158
347
|
const innerPlanets = normalizePlanets(innerData.planets);
|
|
159
348
|
const outerPlanets = normalizePlanets(outerData.planets);
|
|
160
349
|
const innerHouses = normalizeHouses(innerData.houses);
|
|
161
350
|
const outerHouses = normalizeHouses(outerData.houses);
|
|
162
351
|
const crossAspects = computeCrossAspects(innerPlanets, outerPlanets);
|
|
352
|
+
const { ascendant, midheaven } = extractAngles(innerData);
|
|
163
353
|
return {
|
|
164
354
|
mode,
|
|
355
|
+
ascendant,
|
|
356
|
+
midheaven,
|
|
165
357
|
inner: innerPlanets,
|
|
166
358
|
outer: outerPlanets,
|
|
167
359
|
inner_houses: innerHouses,
|
|
@@ -196,10 +388,10 @@ function buildBiWheelSummary(innerPlanets, outerPlanets, crossAspects, mode, loc
|
|
|
196
388
|
square: "□", sextile: "⚹",
|
|
197
389
|
};
|
|
198
390
|
const top5 = crossAspects.slice(0, 5).map((ca) => `• ${capitalize(ca.planet1)} ${ASPECT_SYMBOL[ca.type] ?? ca.type} ${capitalize(ca.planet2)} (${ca.orb.toFixed(1)}° orb${ca.applying ? ", applying" : ""})`);
|
|
199
|
-
const
|
|
200
|
-
const label1 =
|
|
201
|
-
const label2 =
|
|
202
|
-
return (`**${modeLabel} Bi-Wheel**\n\n` +
|
|
391
|
+
const meta = MODE_META[mode];
|
|
392
|
+
const label1 = meta.innerRole;
|
|
393
|
+
const label2 = meta.outerRole;
|
|
394
|
+
return (`**${meta.modeLabel} Bi-Wheel**\n\n` +
|
|
203
395
|
`${label1}: ${location1} — ${innerBasics || "chart loaded"}\n` +
|
|
204
396
|
`${label2}: ${location2}\n\n` +
|
|
205
397
|
`**Top Cross-Aspects:**\n${top5.join("\n") || "No major cross-aspects within orb."}\n\n` +
|
|
@@ -208,12 +400,16 @@ function buildBiWheelSummary(innerPlanets, outerPlanets, crossAspects, mode, loc
|
|
|
208
400
|
// ── Tool: explore_bi_wheel ─────────────────────────────────────────────────────
|
|
209
401
|
registerTool({
|
|
210
402
|
name: "explore_bi_wheel",
|
|
211
|
-
description: "Generate an interactive bi-wheel chart
|
|
212
|
-
"
|
|
213
|
-
"
|
|
214
|
-
"
|
|
403
|
+
description: "Generate an interactive bi-wheel chart comparing two astrological chart positions. " +
|
|
404
|
+
"The inner wheel is always Person 1's natal chart. The outer ring depends on mode:\n" +
|
|
405
|
+
"• synastry — Person 2's natal chart (two-person compatibility). 1 credit.\n" +
|
|
406
|
+
"• transit — Transiting planets for a given date. 1 credit.\n" +
|
|
407
|
+
"• progressed — Secondary progressed positions (person2_datetime = target date). 1 credit.\n" +
|
|
408
|
+
"• solar_return — Nearest solar return chart (person2_datetime = year to return for). 5 credits.\n" +
|
|
409
|
+
"• lunar_return — Nearest lunar return chart (person2_datetime = target month). 5 credits.\n" +
|
|
410
|
+
"• solar_arc — Solar arc directed positions (person2_datetime = target date). 1 credit.\n" +
|
|
215
411
|
"Cross-aspects between both wheels are computed and displayed as coloured dashed lines. " +
|
|
216
|
-
"Click any planet
|
|
412
|
+
"Click any planet, aspect line, or house cusp for interpretation.",
|
|
217
413
|
inputSchema: {
|
|
218
414
|
type: "object",
|
|
219
415
|
properties: {
|
|
@@ -230,14 +426,16 @@ registerTool({
|
|
|
230
426
|
},
|
|
231
427
|
person2_datetime: {
|
|
232
428
|
type: "string",
|
|
233
|
-
description: "ISO 8601 datetime for
|
|
429
|
+
description: "ISO 8601 datetime for the outer wheel. Meaning depends on mode: " +
|
|
430
|
+
"synastry = Person 2 birth datetime; transit/progressed/solar_arc = target date; " +
|
|
431
|
+
"solar_return/lunar_return = any date within the target year/month.",
|
|
234
432
|
},
|
|
235
|
-
person2_latitude: { type: "number", description: "Birth latitude for Person 2 /
|
|
236
|
-
person2_longitude: { type: "number", description: "Birth longitude for Person 2 /
|
|
237
|
-
person2_timezone: { type: "string", description: "IANA timezone for Person 2 /
|
|
433
|
+
person2_latitude: { type: "number", description: "Birth latitude for Person 2 (synastry) or return/transit location. Defaults to Person 1 coords for single-person modes." },
|
|
434
|
+
person2_longitude: { type: "number", description: "Birth longitude for Person 2 (synastry) or return/transit location. Defaults to Person 1 coords for single-person modes." },
|
|
435
|
+
person2_timezone: { type: "string", description: "IANA timezone for Person 2 / outer chart (e.g. 'America/Chicago')." },
|
|
238
436
|
person2_name: {
|
|
239
437
|
type: "string",
|
|
240
|
-
description: "Display name for Person 2
|
|
438
|
+
description: "Display name for Person 2 (synastry mode). Used in the badge and summary.",
|
|
241
439
|
},
|
|
242
440
|
location: {
|
|
243
441
|
type: "string",
|
|
@@ -245,14 +443,14 @@ registerTool({
|
|
|
245
443
|
},
|
|
246
444
|
mode: {
|
|
247
445
|
type: "string",
|
|
248
|
-
enum: ["synastry", "transit"],
|
|
249
|
-
description: "Chart mode
|
|
446
|
+
enum: ["synastry", "transit", "progressed", "solar_return", "lunar_return", "solar_arc"],
|
|
447
|
+
description: "Chart comparison mode. Defaults to 'synastry'.",
|
|
250
448
|
},
|
|
251
449
|
},
|
|
252
450
|
required: ["person1_datetime", "person2_datetime",
|
|
253
|
-
"person1_latitude", "person1_longitude",
|
|
254
|
-
"person2_latitude", "person2_longitude"],
|
|
451
|
+
"person1_latitude", "person1_longitude"],
|
|
255
452
|
},
|
|
453
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
256
454
|
annotations: {
|
|
257
455
|
title: "Interactive Bi-Wheel Explorer",
|
|
258
456
|
readOnlyHint: true,
|
|
@@ -278,25 +476,12 @@ registerTool({
|
|
|
278
476
|
const lon2 = args.person2_longitude;
|
|
279
477
|
const tz2 = args.person2_timezone;
|
|
280
478
|
const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
|
|
281
|
-
const loc2 = mode
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (mode === "synastry") {
|
|
288
|
-
[innerData, outerData] = await Promise.all([
|
|
289
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
290
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2)),
|
|
291
|
-
]);
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
// Transit mode: inner = natal (dt1), outer = transits for dt2 at natal location
|
|
295
|
-
[innerData, outerData] = await Promise.all([
|
|
296
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
297
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1)),
|
|
298
|
-
]);
|
|
299
|
-
}
|
|
479
|
+
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
480
|
+
// Fetch inner (natal) and outer (mode-dependent) charts in parallel where possible
|
|
481
|
+
const [innerData, outerData] = await Promise.all([
|
|
482
|
+
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
483
|
+
fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
|
|
484
|
+
]);
|
|
300
485
|
const innerPlanets = normalizePlanets(innerData.planets);
|
|
301
486
|
const outerPlanets = normalizePlanets(outerData.planets);
|
|
302
487
|
const crossAspects = computeCrossAspects(innerPlanets, outerPlanets);
|
|
@@ -324,11 +509,11 @@ registerTool({
|
|
|
324
509
|
// ── Tool: bi_wheel_recalculate ──────────────────────────────────────────────
|
|
325
510
|
registerTool({
|
|
326
511
|
name: "bi_wheel_recalculate",
|
|
327
|
-
description: "Recalculates a bi-wheel chart
|
|
512
|
+
description: "Recalculates a bi-wheel chart with new parameters. Supports all modes: synastry, transit, progressed, solar_return, lunar_return, solar_arc.",
|
|
328
513
|
inputSchema: {
|
|
329
514
|
type: "object",
|
|
330
515
|
properties: {
|
|
331
|
-
mode: { type: "string", enum:
|
|
516
|
+
mode: { type: "string", enum: ALL_MODES },
|
|
332
517
|
person1_datetime: { type: "string" },
|
|
333
518
|
person1_latitude: { type: "number" },
|
|
334
519
|
person1_longitude: { type: "number" },
|
|
@@ -339,15 +524,22 @@ registerTool({
|
|
|
339
524
|
person2_longitude: { type: "number" },
|
|
340
525
|
person2_timezone: { type: "string" },
|
|
341
526
|
person2_name: { type: "string" },
|
|
342
|
-
location: { type: "string" },
|
|
527
|
+
location: { type: "string" },
|
|
528
|
+
house_system: {
|
|
529
|
+
type: "string",
|
|
530
|
+
enum: Object.keys(HOUSE_SYSTEM_MAP),
|
|
531
|
+
description: "House system for the inner (natal) wheel. Defaults to placidus.",
|
|
532
|
+
},
|
|
343
533
|
},
|
|
344
534
|
required: ["mode", "person1_datetime", "person1_latitude", "person1_longitude", "person2_datetime"],
|
|
345
535
|
},
|
|
536
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
346
537
|
annotations: { title: "Recalculate Bi-Wheel", readOnlyHint: true, openWorldHint: false },
|
|
347
538
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
348
539
|
handler: async (args) => {
|
|
349
540
|
const client = getActiveClient();
|
|
350
541
|
const mode = args.mode ?? "synastry";
|
|
542
|
+
const houseSystem = args.house_system ?? "placidus";
|
|
351
543
|
const dt1 = String(args.person1_datetime);
|
|
352
544
|
const lat1 = args.person1_latitude;
|
|
353
545
|
const lon1 = args.person1_longitude;
|
|
@@ -357,35 +549,16 @@ registerTool({
|
|
|
357
549
|
const lon2 = args.person2_longitude;
|
|
358
550
|
const tz2 = args.person2_timezone;
|
|
359
551
|
const loc1 = String(args.person1_name ?? args.location ?? `${lat1 ?? "?"},${lon1 ?? "?"}`);
|
|
360
|
-
const loc2 = mode
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
366
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2, lon2, tz2)),
|
|
367
|
-
]);
|
|
368
|
-
}
|
|
369
|
-
else if (mode === "progressed") {
|
|
370
|
-
[innerData, outerData] = await Promise.all([
|
|
371
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
372
|
-
client.post("/predictive/progressed", {
|
|
373
|
-
subject: { birth_datetime: { iso: dt1 }, birth_location: { latitude: { decimal: lat1 ?? 0 }, longitude: { decimal: lon1 ?? 0 }, timezone: tz1 ? { iana_name: tz1 } : undefined } },
|
|
374
|
-
configuration: { target_datetime: { iso: dt2 }, method: "secondary" }
|
|
375
|
-
}),
|
|
376
|
-
]);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
[innerData, outerData] = await Promise.all([
|
|
380
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1)),
|
|
381
|
-
client.post("/ephemeris/natal-chart", buildNatalBody(dt2, lat2 ?? lat1, lon2 ?? lon1, tz2 ?? tz1)),
|
|
382
|
-
]);
|
|
383
|
-
}
|
|
552
|
+
const loc2 = MODE_META[mode].outerLabel(dt2, args.person2_name);
|
|
553
|
+
const [innerData, outerData] = await Promise.all([
|
|
554
|
+
client.post("/ephemeris/natal-chart", buildNatalBody(dt1, lat1, lon1, tz1, houseSystem)),
|
|
555
|
+
fetchOuterChart(client, mode, dt1, lat1, lon1, tz1, dt2, lat2, lon2, tz2),
|
|
556
|
+
]);
|
|
384
557
|
const params1 = { datetime: dt1, timezone: tz1 ?? null, latitude: lat1 ?? null, longitude: lon1 ?? null, location: loc1 };
|
|
385
558
|
const params2 = { datetime: dt2, timezone: tz2 ?? null, latitude: lat2 ?? null, longitude: lon2 ?? null, location: loc2 };
|
|
386
559
|
const payload = buildBiWheelPayload(innerData, outerData, mode, params1, params2);
|
|
387
560
|
return {
|
|
388
|
-
content: [{ type: "text", text: JSON.stringify({ ...payload, server_version: SERVER_VERSION }) }],
|
|
561
|
+
content: [{ type: "text", text: JSON.stringify({ ...payload, house_system: houseSystem, server_version: SERVER_VERSION }) }],
|
|
389
562
|
};
|
|
390
563
|
},
|
|
391
564
|
});
|
|
@@ -393,22 +566,23 @@ registerTool({
|
|
|
393
566
|
registerTool({
|
|
394
567
|
name: "bi_wheel_on_cross_aspect_click",
|
|
395
568
|
description: "Event handler called when the user clicks a cross-aspect line in the bi-wheel. " +
|
|
396
|
-
"Returns
|
|
569
|
+
"Returns an interpretation of that aspect contextualised by chart mode (synastry, transit, progressed, etc.). " +
|
|
397
570
|
"This tool is called automatically by the bi-wheel UI; you do not need to call it directly.",
|
|
398
571
|
inputSchema: {
|
|
399
572
|
type: "object",
|
|
400
573
|
properties: {
|
|
401
574
|
planet1: { type: "string", description: "Planet name from the inner (natal) wheel" },
|
|
402
575
|
wheel1: { type: "string", enum: ["inner", "outer"], description: "Which wheel planet1 belongs to" },
|
|
403
|
-
planet2: { type: "string", description: "Planet name from the outer
|
|
576
|
+
planet2: { type: "string", description: "Planet name from the outer wheel" },
|
|
404
577
|
wheel2: { type: "string", enum: ["inner", "outer"], description: "Which wheel planet2 belongs to" },
|
|
405
578
|
aspect_type: { type: "string", description: "Aspect type: conjunction, opposition, trine, square, sextile" },
|
|
406
579
|
orb: { type: "number", description: "Orb in degrees" },
|
|
407
580
|
applying: { type: "boolean", description: "True if the aspect is applying (tightening)" },
|
|
408
|
-
mode: { type: "string", enum:
|
|
581
|
+
mode: { type: "string", enum: ALL_MODES, description: "Chart mode. Determines interpretation style." },
|
|
409
582
|
},
|
|
410
583
|
required: ["planet1", "planet2", "aspect_type"],
|
|
411
584
|
},
|
|
585
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
412
586
|
annotations: { title: "Cross-Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
413
587
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
414
588
|
handler: async (args) => {
|
|
@@ -438,30 +612,32 @@ registerTool({
|
|
|
438
612
|
type: "object",
|
|
439
613
|
properties: {
|
|
440
614
|
planet: { type: "string", description: "Planet name (e.g. 'saturn')" },
|
|
441
|
-
wheel: { type: "string", enum: ["inner", "outer"], description: "Which wheel (inner=natal/person1, outer=
|
|
615
|
+
wheel: { type: "string", enum: ["inner", "outer"], description: "Which wheel (inner=natal/person1, outer=mode-dependent)" },
|
|
442
616
|
longitude: { type: "number", description: "Ecliptic longitude in degrees" },
|
|
443
617
|
sign: { type: "string", description: "Zodiac sign" },
|
|
444
618
|
house: { type: "number", description: "House number (1–12) if available" },
|
|
445
619
|
retrograde: { type: "boolean", description: "Whether the planet is retrograde" },
|
|
446
|
-
mode: { type: "string", enum:
|
|
620
|
+
mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
|
|
447
621
|
},
|
|
448
622
|
required: ["planet", "wheel", "longitude"],
|
|
449
623
|
},
|
|
624
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
450
625
|
annotations: { title: "Bi-Wheel Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
451
626
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
452
627
|
handler: async (args) => {
|
|
453
628
|
const planet = String(args.planet);
|
|
454
629
|
const wheel = args.wheel ?? "inner";
|
|
630
|
+
const mode = args.mode ?? "synastry";
|
|
455
631
|
const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
|
|
456
632
|
const houseNum = args.house != null ? Number(args.house) : null;
|
|
457
633
|
const house = houseNum != null && houseNum > 0 ? ` in the ${ordinal(houseNum)} house` : "";
|
|
458
|
-
const wheelLabel = wheel === "inner" ?
|
|
634
|
+
const wheelLabel = wheel === "inner" ? MODE_META[mode].innerRole : MODE_META[mode].outerRole;
|
|
459
635
|
const deg = `${Math.floor(Number(args.longitude) % 30)}° ${sign}`;
|
|
460
636
|
return {
|
|
461
637
|
content: [{
|
|
462
638
|
type: "text",
|
|
463
639
|
text: `**${capitalize(planet)} at ${deg}${house}** *(${wheelLabel})*\n\n` +
|
|
464
|
-
interpretBiWheelPlanet(planet, sign, houseNum ?? 0, wheel, Boolean(args.retrograde)),
|
|
640
|
+
interpretBiWheelPlanet(planet, sign, houseNum ?? 0, wheel, Boolean(args.retrograde), mode),
|
|
465
641
|
}],
|
|
466
642
|
};
|
|
467
643
|
},
|
|
@@ -555,7 +731,24 @@ function interpretBiWheelPlanet(planet, sign, house, wheel, retrograde = false,
|
|
|
555
731
|
return (`**Transiting Planet:** Transiting ${capitalize(planet)} represents ${theme}. Moving through ${capitalize(sign)}, it carries the quality of ${signQuality}.${houseCtx}${retroCtx}\n\n` +
|
|
556
732
|
`As a **transit**, this planet is temporarily activating the natal chart. Its influence is current and time-limited — watch for events, shifts in awareness, or opportunities related to its themes in the houses and planets it touches.`);
|
|
557
733
|
}
|
|
734
|
+
else if (mode === "progressed") {
|
|
735
|
+
return (`**Progressed Planet:** Progressed ${capitalize(planet)} in ${capitalize(sign)} shows how the natal ${capitalize(planet)} archetype has evolved through lived experience.${houseCtx}${retroCtx}\n\n` +
|
|
736
|
+
`Unlike transits, progressed planets move slowly — a sign ingress can take years to unfold. This position describes a chapter of inner development: the themes of ${capitalize(planet)} are now being expressed with the colouring of ${signQuality}. Aspects from this progressed position to natal points mark turning points in that developmental arc.`);
|
|
737
|
+
}
|
|
738
|
+
else if (mode === "solar_arc") {
|
|
739
|
+
return (`**Solar Arc ${capitalize(planet)}:** This directed position represents natal ${capitalize(planet)} advanced by approximately 1° per year of life.${houseCtx}${retroCtx}\n\n` +
|
|
740
|
+
`Solar arc directions symbolise the symbolic maturation of each natal archetype. Where the directed ${capitalize(planet)} now falls — and especially which natal planets or angles it contacts — marks a significant chapter in the long development of ${theme}. Conjunctions, squares, and oppositions to natal points are the most potent activation signals.`);
|
|
741
|
+
}
|
|
742
|
+
else if (mode === "solar_return") {
|
|
743
|
+
return (`**Solar Return ${capitalize(planet)}:** In this solar return chart, ${capitalize(planet)} falls at ${Math.floor((Number(sign) || 0) % 30)}° ${capitalize(sign)}.${houseCtx}${retroCtx}\n\n` +
|
|
744
|
+
`Solar return planets describe themes that will be especially prominent during this **solar year** — from this birthday to the next. ${capitalize(planet)} in the return chart highlights where the energy of ${theme} will be most active, especially in the house it occupies and through contacts to natal planets.`);
|
|
745
|
+
}
|
|
746
|
+
else if (mode === "lunar_return") {
|
|
747
|
+
return (`**Lunar Return ${capitalize(planet)}:** In this lunar return chart, ${capitalize(planet)} sets the tone for the coming **lunar month**.${houseCtx}${retroCtx}\n\n` +
|
|
748
|
+
`Lunar returns reset monthly as the Moon returns to its natal degree. ${capitalize(planet)}'s position here — through ${signQuality} — shapes the emotional and circumstantial flavour of the next four weeks. Contacts to natal planets or angles sharpen its influence considerably.`);
|
|
749
|
+
}
|
|
558
750
|
else {
|
|
751
|
+
// synastry fallback
|
|
559
752
|
return (`**Synastry Planet:** ${capitalize(planet)} represents ${theme}. In ${capitalize(sign)}, this manifests through ${signQuality}.${houseCtx}${retroCtx}\n\n` +
|
|
560
753
|
`This is the **outer person's natal ${capitalize(planet)}** — when this planet makes contact with the inner chart, it activates the natal themes of both parties simultaneously, creating a dynamic of mutual recognition and activation.`);
|
|
561
754
|
}
|
|
@@ -584,8 +777,11 @@ const CROSS_ASPECT_NATURE = {
|
|
|
584
777
|
};
|
|
585
778
|
function interpretCrossAspect(p1, p2, type, applying, mode) {
|
|
586
779
|
const natures = CROSS_ASPECT_NATURE[type.toLowerCase()];
|
|
780
|
+
// Single-person time-based modes share the transit nature text; synastry/solar arc use synastry
|
|
781
|
+
const isTimeBased = mode === "transit" || mode === "progressed" || mode === "solar_arc" ||
|
|
782
|
+
mode === "solar_return" || mode === "lunar_return";
|
|
587
783
|
const nature = natures
|
|
588
|
-
? (
|
|
784
|
+
? (isTimeBased ? natures.transit : natures.synastry)
|
|
589
785
|
: "creates a notable connection across the two charts.";
|
|
590
786
|
let timing;
|
|
591
787
|
if (mode === "transit") {
|
|
@@ -593,6 +789,17 @@ function interpretCrossAspect(p1, p2, type, applying, mode) {
|
|
|
593
789
|
? "This aspect is **applying** — its influence is building and moving toward maximum expression. The themes involved are intensifying now."
|
|
594
790
|
: "This aspect is **separating** — its peak has passed; the energy is waning and the lessons of this transit are moving into integration.";
|
|
595
791
|
}
|
|
792
|
+
else if (mode === "progressed" || mode === "solar_arc") {
|
|
793
|
+
timing = applying
|
|
794
|
+
? "This aspect is **applying** — progressed and directed aspects move slowly, often unfolding over months or years before reaching exactitude. The themes it represents are gradually intensifying."
|
|
795
|
+
: "This aspect is **separating** — it has passed its peak of exactitude and is now in a phase of integration. The defining moment of this developmental aspect lies in the recent past.";
|
|
796
|
+
}
|
|
797
|
+
else if (mode === "solar_return") {
|
|
798
|
+
timing = "As a **solar return aspect**, this connection is active for the entire solar year from this birthday to the next. It describes a consistent theme or dynamic that will colour the whole year rather than arriving as a dated event.";
|
|
799
|
+
}
|
|
800
|
+
else if (mode === "lunar_return") {
|
|
801
|
+
timing = "As a **lunar return aspect**, this connection is active for the coming lunar month. It shapes the emotional atmosphere and events of the next four weeks before the next lunar return resets the cycle.";
|
|
802
|
+
}
|
|
596
803
|
else {
|
|
597
804
|
timing = "As a **synastry aspect**, this connection exists as a permanent feature of this relationship dynamic — it neither builds nor fades, but remains as a constant undercurrent in how these two people interact.";
|
|
598
805
|
}
|
|
@@ -611,10 +818,11 @@ registerTool({
|
|
|
611
818
|
house_number: { type: "number", description: "House number (1–12)" },
|
|
612
819
|
sign: { type: "string", description: "Sign on the house cusp (inner natal wheel)" },
|
|
613
820
|
cusp_longitude: { type: "number", description: "Cusp longitude in degrees" },
|
|
614
|
-
mode: { type: "string", enum:
|
|
821
|
+
mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
|
|
615
822
|
},
|
|
616
823
|
required: ["house_number"],
|
|
617
824
|
},
|
|
825
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
618
826
|
annotations: { title: "Bi-Wheel House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
619
827
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
620
828
|
handler: async (args) => {
|
|
@@ -625,14 +833,19 @@ registerTool({
|
|
|
625
833
|
const signFlavor = sign
|
|
626
834
|
? `\n\n${capitalize(sign)} on this cusp emphasizes ${SIGN_HOUSE_FLAVOR[sign.toLowerCase()] ?? "its natural qualities"} in the matters of this house.`
|
|
627
835
|
: "";
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
: "\n\
|
|
836
|
+
const HOUSE_MODE_NOTES = {
|
|
837
|
+
synastry: "\n\nIn synastry, planets from the outer natal chart falling in this house create experiences related to these themes in the relationship — often powerfully felt by the person whose house is being activated.",
|
|
838
|
+
transit: "\n\nWatch for transiting planets moving through this house — they will activate and bring events related to these life areas during their passage.",
|
|
839
|
+
progressed: "\n\nAs progressed planets move through this house, they gradually shift the inner developmental focus toward these life themes. Natal house boundaries mark important life-chapter transitions in secondary progression work.",
|
|
840
|
+
solar_arc: "\n\nSolar arc directed planets entering this house signal a new chapter of life experience in these domains. The ingress year often corresponds to a major shift in the themes of this house.",
|
|
841
|
+
solar_return: "\n\nPlanets in this house in the solar return chart will be emphasised life areas for the coming year. A concentration of solar return planets here intensifies activity and attention in this domain.",
|
|
842
|
+
lunar_return: "\n\nPlanets in this house in the lunar return chart highlight the month's dominant emotional themes and practical focus areas. The lunar return ascendant and its house emphasis set the tone for the entire lunar cycle.",
|
|
843
|
+
};
|
|
631
844
|
return {
|
|
632
845
|
content: [{
|
|
633
846
|
type: "text",
|
|
634
847
|
text: `**House ${num}${sign ? ` — ${capitalize(sign)} on the cusp` : ""}** *(Inner Natal Wheel)*\n\n` +
|
|
635
|
-
meaning + signFlavor +
|
|
848
|
+
meaning + signFlavor + HOUSE_MODE_NOTES[mode],
|
|
636
849
|
}],
|
|
637
850
|
};
|
|
638
851
|
},
|
|
@@ -648,9 +861,9 @@ registerTool({
|
|
|
648
861
|
inputSchema: {
|
|
649
862
|
type: "object",
|
|
650
863
|
properties: {
|
|
651
|
-
mode: { type: "string", enum:
|
|
652
|
-
person1_name: { type: "string", description: "Name or label for the inner wheel (natal/person 1)" },
|
|
653
|
-
person2_name: { type: "string", description: "Name or label for the outer wheel (
|
|
864
|
+
mode: { type: "string", enum: ALL_MODES, description: "Chart mode" },
|
|
865
|
+
person1_name: { type: "string", description: "Name or label for the inner wheel (natal / person 1)" },
|
|
866
|
+
person2_name: { type: "string", description: "Name or label for the outer wheel (person 2, transit date, progression target, etc.)" },
|
|
654
867
|
cross_aspects: {
|
|
655
868
|
type: "array",
|
|
656
869
|
description: "Array of cross-aspects from the bi-wheel payload (up to 30, sorted by orb)",
|
|
@@ -669,12 +882,14 @@ registerTool({
|
|
|
669
882
|
},
|
|
670
883
|
required: ["mode"],
|
|
671
884
|
},
|
|
885
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
672
886
|
annotations: { title: "Bi-Wheel Synopsis", readOnlyHint: true, openWorldHint: false },
|
|
673
887
|
_meta: { ui: { resourceUri: BI_WHEEL_RESOURCE_URI, visibility: ["app", "model"] } },
|
|
674
888
|
handler: async (args) => {
|
|
675
889
|
const mode = args.mode ?? "synastry";
|
|
676
|
-
const
|
|
677
|
-
const
|
|
890
|
+
const meta = MODE_META[mode];
|
|
891
|
+
const label1 = args.person1_name ? String(args.person1_name) : meta.innerRole;
|
|
892
|
+
const label2 = args.person2_name ? String(args.person2_name) : meta.outerRole;
|
|
678
893
|
const topN = args.top_n ? Number(args.top_n) : 8;
|
|
679
894
|
const rawAspects = args.cross_aspects ?? [];
|
|
680
895
|
const aspects = rawAspects.slice(0, topN);
|
|
@@ -684,36 +899,55 @@ registerTool({
|
|
|
684
899
|
const harmonious = aspects.filter((a) => ["trine", "sextile"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
|
|
685
900
|
const dynamic = aspects.filter((a) => ["square", "opposition"].includes((a.aspect_type ?? a.type ?? "").toLowerCase()));
|
|
686
901
|
const fusing = aspects.filter((a) => (a.aspect_type ?? a.type ?? "").toLowerCase() === "conjunction");
|
|
902
|
+
// For time-based modes, show applying/separating; for relational modes show tightness
|
|
903
|
+
const isTimeBased = mode !== "synastry";
|
|
687
904
|
const lines = aspects.map((ca) => {
|
|
688
905
|
const type = ca.aspect_type ?? ca.type ?? "aspect";
|
|
689
906
|
const sym = ASPECT_SYM[type.toLowerCase()] ?? type;
|
|
690
|
-
const dir =
|
|
907
|
+
const dir = isTimeBased
|
|
691
908
|
? (ca.applying ? "↗ applying" : "↘ separating")
|
|
692
909
|
: (ca.orb <= 1 ? "exact" : ca.orb <= 3 ? "tight" : "wide");
|
|
693
910
|
return `• ${capitalize(ca.planet1)} ${sym} ${capitalize(ca.planet2)} (${ca.orb.toFixed(1)}° — ${dir})`;
|
|
694
911
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
: `**
|
|
912
|
+
// Theme summary copy varies by mode
|
|
913
|
+
const synastryTheme = [
|
|
914
|
+
fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — areas of deep merging and fusion between the two charts.` : "",
|
|
915
|
+
harmonious.length > 0 ? `**${harmonious.length} flowing aspect${harmonious.length > 1 ? "s" : ""}** (trines/sextiles) — natural ease, support, and compatibility.` : "",
|
|
916
|
+
dynamic.length > 0 ? `**${dynamic.length} dynamic aspect${dynamic.length > 1 ? "s" : ""}** (squares/oppositions) — areas of growth, friction, and mutual challenge.` : "",
|
|
917
|
+
].filter(Boolean).join("\n");
|
|
918
|
+
const timeBasedTheme = [
|
|
919
|
+
fusing.length > 0 ? `**${fusing.length} conjunction${fusing.length > 1 ? "s" : ""}** — direct activation of natal points; peak influence for this ${meta.modeLabel.toLowerCase()}.` : "",
|
|
920
|
+
harmonious.length > 0 ? `**${harmonious.length} flowing aspect${harmonious.length > 1 ? "s" : ""}** (trines/sextiles) — periods of ease, opportunity, and natural support.` : "",
|
|
921
|
+
dynamic.length > 0 ? `**${dynamic.length} dynamic aspect${dynamic.length > 1 ? "s" : ""}** (squares/oppositions) — active pressure points requiring conscious engagement.` : "",
|
|
922
|
+
].filter(Boolean).join("\n");
|
|
923
|
+
const themeSummary = mode === "synastry" ? synastryTheme : timeBasedTheme;
|
|
924
|
+
const SYNOPSIS_INTRO = {
|
|
925
|
+
synastry: `**Synastry Overview: ${label1} × ${label2}**\n\n` +
|
|
926
|
+
`This synastry bi-wheel compares two natal charts to reveal the dynamics of interaction and connection between these two individuals. The cross-aspects show how their planetary energies align, challenge, and transform each other.`,
|
|
927
|
+
transit: `**Transit Overview: ${label1} × ${label2}**\n\n` +
|
|
928
|
+
`This transit bi-wheel overlays current planetary positions onto the natal chart, revealing which natal points are being activated right now and the nature of those influences.`,
|
|
929
|
+
progressed: `**Secondary Progressions Overview: ${label1} × ${label2}**\n\n` +
|
|
930
|
+
`This progression bi-wheel places the natal chart inside and secondary progressed positions in the outer ring. Progressed planets reflect the gradual inner maturation of natal archetypal themes using the day-for-a-year method. Cross-aspects from progressed to natal points mark the defining chapters of inner development.`,
|
|
931
|
+
solar_arc: `**Solar Arc Directions Overview: ${label1} × ${label2}**\n\n` +
|
|
932
|
+
`This solar arc bi-wheel shows natal positions inside and solar arc directed positions in the outer ring. Solar arc direction advances every natal planet by approximately 1° per year of life (Naibod rate). Contacts from directed positions to natal points — especially conjunctions — mark pivotal life moments.`,
|
|
933
|
+
solar_return: `**Solar Return Overview: ${label1} × ${label2}**\n\n` +
|
|
934
|
+
`This solar return bi-wheel compares the natal chart against the solar return for the target year. The outer ring shows where planets fall at the exact moment the Sun returns to its natal degree, describing the dominant themes and developmental focus of the coming solar year.`,
|
|
935
|
+
lunar_return: `**Lunar Return Overview: ${label1} × ${label2}**\n\n` +
|
|
936
|
+
`This lunar return bi-wheel compares the natal chart against the lunar return. The outer ring shows planetary positions when the Moon returned to its natal degree, setting the emotional tone and circumstantial focus of the coming lunar month.`,
|
|
937
|
+
};
|
|
938
|
+
const SYNOPSIS_CLOSING = {
|
|
939
|
+
synastry: "\n\nClick any planet or aspect line in the bi-wheel for a deeper interpretation of individual connections.",
|
|
940
|
+
transit: "\n\nClick any planet or cross-aspect line for a detailed transit interpretation. Navigate the date forward or backward using the step controls to track how transits evolve.",
|
|
941
|
+
progressed: "\n\nClick any progressed planet or cross-aspect line for a detailed interpretation. The most significant turning points occur when a progressed planet reaches exactitude with a natal planet or angle.",
|
|
942
|
+
solar_arc: "\n\nClick any directed planet or cross-aspect line for interpretation. Solar arc contacts to natal angles (ASC, MC, IC, DSC) are among the most powerful triggers in predictive astrology.",
|
|
943
|
+
solar_return: "\n\nClick any solar return planet or cross-aspect line for interpretation. The solar return ascendant and its ruler are the master theme-setters for the year.",
|
|
944
|
+
lunar_return: "\n\nClick any lunar return planet or cross-aspect line for interpretation. The lunar return Moon's house placement describes this month's emotional centre of gravity.",
|
|
945
|
+
};
|
|
709
946
|
const body = lines.length > 0
|
|
710
947
|
? `\n\n**Top Cross-Aspects by Orb:**\n${lines.join("\n")}\n\n**Thematic Summary:**\n${themeSummary || "No major aspects within standard orbs."}`
|
|
711
948
|
: "\n\nNo major cross-aspects are within orb at this time.";
|
|
712
|
-
const closing = mode === "synastry"
|
|
713
|
-
? "\n\nClick any planet or aspect line in the bi-wheel for a deeper interpretation of individual connections."
|
|
714
|
-
: "\n\nClick any planet or cross-aspect line for a detailed transit interpretation. Navigate the date forward or backward using the step controls to track how transits evolve.";
|
|
715
949
|
return {
|
|
716
|
-
content: [{ type: "text", text:
|
|
950
|
+
content: [{ type: "text", text: SYNOPSIS_INTRO[mode] + body + SYNOPSIS_CLOSING[mode] }],
|
|
717
951
|
};
|
|
718
952
|
},
|
|
719
953
|
});
|