@openephemeris/mcp-server 3.10.4 → 3.13.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/README.md +5 -4
- package/config/dev-allowlist.json +83 -2
- package/dist/index.js +48 -29
- package/dist/server-sse.js +49 -30
- package/dist/tools/apps/bi-wheel-app.d.ts +38 -0
- package/dist/tools/apps/bi-wheel-app.js +459 -0
- package/dist/tools/apps/bodygraph-app.d.ts +22 -0
- package/dist/tools/apps/bodygraph-app.js +633 -0
- package/dist/tools/apps/chart-wheel-app.d.ts +10 -6
- package/dist/tools/apps/chart-wheel-app.js +202 -56
- package/dist/tools/index.d.ts +18 -20
- package/dist/tools/index.js +25 -0
- package/dist/ui/bi-wheel.html +81 -0
- package/dist/ui/bodygraph.html +514 -0
- package/dist/ui/chart-wheel.html +168 -55
- package/glama.json +6 -0
- package/package.json +5 -3
- package/smithery.yaml +50 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_natal_chart
|
|
6
|
-
* •
|
|
7
|
-
* •
|
|
8
|
-
* •
|
|
9
|
-
* •
|
|
4
|
+
* Registers 7 tools:
|
|
5
|
+
* • explore_natal_chart — primary entry point, returns data + UI resource [model]
|
|
6
|
+
* • chart_wheel_fetch_svg — lazy SVG fetcher called by UI on first render [app-only]
|
|
7
|
+
* • chart_wheel_on_planet_click — planet click handler [app-only]
|
|
8
|
+
* • chart_wheel_on_house_click — house click handler [app-only]
|
|
9
|
+
* • chart_wheel_on_aspect_click — aspect click handler [app-only]
|
|
10
|
+
* • chart_wheel_recalculate — house-system switcher [app-only]
|
|
11
|
+
*
|
|
12
|
+
* SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
|
|
13
|
+
* chart_wheel_fetch_svg lazily to keep Claude's context lean.
|
|
10
14
|
*
|
|
11
15
|
* Also exports resource helpers (getChartWheelBundle, etc.) for use in
|
|
12
16
|
* index.ts and server-sse.ts.
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chart-wheel-app.ts — MCP App tool registration for the Chart Wheel Explorer.
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
* • explore_natal_chart
|
|
6
|
-
* •
|
|
7
|
-
* •
|
|
8
|
-
* •
|
|
9
|
-
* •
|
|
4
|
+
* Registers 7 tools:
|
|
5
|
+
* • explore_natal_chart — primary entry point, returns data + UI resource [model]
|
|
6
|
+
* • chart_wheel_fetch_svg — lazy SVG fetcher called by UI on first render [app-only]
|
|
7
|
+
* • chart_wheel_on_planet_click — planet click handler [app-only]
|
|
8
|
+
* • chart_wheel_on_house_click — house click handler [app-only]
|
|
9
|
+
* • chart_wheel_on_aspect_click — aspect click handler [app-only]
|
|
10
|
+
* • chart_wheel_recalculate — house-system switcher [app-only]
|
|
11
|
+
*
|
|
12
|
+
* SVG payloads are NEVER embedded in model-facing text blocks. The UI calls
|
|
13
|
+
* chart_wheel_fetch_svg lazily to keep Claude's context lean.
|
|
10
14
|
*
|
|
11
15
|
* Also exports resource helpers (getChartWheelBundle, etc.) for use in
|
|
12
16
|
* index.ts and server-sse.ts.
|
|
@@ -55,6 +59,22 @@ export function getChartWheelBundle() {
|
|
|
55
59
|
export function clearBundleCache() {
|
|
56
60
|
cachedBundle = null;
|
|
57
61
|
}
|
|
62
|
+
// ── Per-request SVG cache (serves chart_wheel_fetch_svg lazily) ────────────
|
|
63
|
+
// Keyed by "datetime|lat|lon|houseSystem". Bounded to last 50 renders.
|
|
64
|
+
const SVG_CACHE_MAX = 50;
|
|
65
|
+
const cachedSvgMap = new Map();
|
|
66
|
+
function buildSvgCacheKey(datetime, lat, lon, houseSystem) {
|
|
67
|
+
return `${datetime}|${lat ?? 0}|${lon ?? 0}|${houseSystem}`;
|
|
68
|
+
}
|
|
69
|
+
function cacheSetSvg(key, svg) {
|
|
70
|
+
if (cachedSvgMap.size >= SVG_CACHE_MAX) {
|
|
71
|
+
// Evict oldest entry
|
|
72
|
+
const first = cachedSvgMap.keys().next().value;
|
|
73
|
+
if (first !== undefined)
|
|
74
|
+
cachedSvgMap.delete(first);
|
|
75
|
+
}
|
|
76
|
+
cachedSvgMap.set(key, svg);
|
|
77
|
+
}
|
|
58
78
|
// ── House system helpers ───────────────────────────────────────────────────
|
|
59
79
|
const HOUSE_SYSTEM_MAP = {
|
|
60
80
|
placidus: "P", whole_sign: "W", equal: "E", koch: "K",
|
|
@@ -136,39 +156,26 @@ registerTool({
|
|
|
136
156
|
const lat = args.latitude;
|
|
137
157
|
const lon = args.longitude;
|
|
138
158
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Decode SVG from binary response (base64 → UTF-8 string)
|
|
145
|
-
let svgString = null;
|
|
146
|
-
if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
|
|
147
|
-
svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
|
|
148
|
-
}
|
|
149
|
-
// Build the enriched chart payload for the UI
|
|
150
|
-
const uiPayload = {
|
|
151
|
-
...chartData,
|
|
152
|
-
_svg_base: svgString,
|
|
153
|
-
_birth_params: {
|
|
154
|
-
datetime,
|
|
155
|
-
timezone: args.timezone ?? null,
|
|
156
|
-
latitude: lat ?? null,
|
|
157
|
-
longitude: lon ?? null,
|
|
158
|
-
location: args.location ?? null,
|
|
159
|
-
},
|
|
160
|
-
house_system: houseSystem,
|
|
161
|
-
};
|
|
162
|
-
// Build human-readable summary for the LLM context / fallback text
|
|
159
|
+
// Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
|
|
160
|
+
// so we do NOT call the /visualization/chart-wheel endpoint — that was the
|
|
161
|
+
// source of the ~90 second blocking delay.
|
|
162
|
+
const chartData = await client.post("/ephemeris/natal-chart", natalBody);
|
|
163
|
+
// Build human-readable summary for the LLM context
|
|
163
164
|
const summary = buildChartSummary(chartData, String(args.location ?? `${lat}, ${lon}`));
|
|
164
|
-
//
|
|
165
|
+
// Build the UI payload (planets normalised to array, aspects computed)
|
|
166
|
+
const modelPayload = buildModelPayload(chartData, {
|
|
167
|
+
datetime,
|
|
168
|
+
timezone: args.timezone ?? null,
|
|
169
|
+
latitude: lat ?? null,
|
|
170
|
+
longitude: lon ?? null,
|
|
171
|
+
location: args.location ?? null,
|
|
172
|
+
}, houseSystem);
|
|
165
173
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
166
174
|
if (bundleAvailable) {
|
|
167
|
-
// Return: text fallback + JSON payload + resource reference
|
|
168
175
|
return {
|
|
169
176
|
content: [
|
|
170
177
|
{ type: "text", text: summary },
|
|
171
|
-
{ type: "text", text: JSON.stringify(
|
|
178
|
+
{ type: "text", text: JSON.stringify(modelPayload) },
|
|
172
179
|
{
|
|
173
180
|
type: "resource",
|
|
174
181
|
resource: {
|
|
@@ -183,17 +190,14 @@ registerTool({
|
|
|
183
190
|
},
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
|
-
// Fallback: text summary
|
|
187
|
-
|
|
188
|
-
{ type: "text", text: summary },
|
|
189
|
-
];
|
|
190
|
-
if (svgString) {
|
|
191
|
-
const b64 = Buffer.from(svgString).toString("base64");
|
|
192
|
-
content.push({ type: "image", data: b64, mimeType: "image/svg+xml" });
|
|
193
|
-
}
|
|
194
|
-
return { content };
|
|
193
|
+
// Fallback: text summary only (non-App hosts; no SVG available here)
|
|
194
|
+
return { content: [{ type: "text", text: summary }] };
|
|
195
195
|
},
|
|
196
196
|
});
|
|
197
|
+
// ── chart_wheel_fetch_svg removed ────────────────────────────────────────────
|
|
198
|
+
// SVG is now embedded inline in the explore_natal_chart payload (_svg_base).
|
|
199
|
+
// The UI fast-path in main.ts checks _svg_base first and skips the tool call.
|
|
200
|
+
// The SVG cache is retained for chart_wheel_recalculate house system switches.
|
|
197
201
|
// ── Tool: chart_wheel_on_planet_click ──────────────────────────────────────
|
|
198
202
|
registerTool({
|
|
199
203
|
name: "chart_wheel_on_planet_click",
|
|
@@ -214,6 +218,7 @@ registerTool({
|
|
|
214
218
|
required: ["planet", "longitude"],
|
|
215
219
|
},
|
|
216
220
|
annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
221
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
217
222
|
handler: async (args) => {
|
|
218
223
|
const planet = String(args.planet);
|
|
219
224
|
const sign = args.sign ? String(args.sign) : zodSignFromLon(Number(args.longitude));
|
|
@@ -247,6 +252,7 @@ registerTool({
|
|
|
247
252
|
required: ["house_number"],
|
|
248
253
|
},
|
|
249
254
|
annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
255
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
250
256
|
handler: async (args) => {
|
|
251
257
|
const num = Number(args.house_number);
|
|
252
258
|
const sign = args.sign ? String(args.sign) : "";
|
|
@@ -281,6 +287,7 @@ registerTool({
|
|
|
281
287
|
required: ["planet1", "planet2"],
|
|
282
288
|
},
|
|
283
289
|
annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
290
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
284
291
|
handler: async (args) => {
|
|
285
292
|
const p1 = capitalize(String(args.planet1));
|
|
286
293
|
const p2 = capitalize(String(args.planet2));
|
|
@@ -319,6 +326,7 @@ registerTool({
|
|
|
319
326
|
required: ["datetime", "latitude", "longitude", "house_system"],
|
|
320
327
|
},
|
|
321
328
|
annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
|
|
329
|
+
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
322
330
|
handler: async (args) => {
|
|
323
331
|
const client = getActiveClient();
|
|
324
332
|
const houseSystem = String(args.house_system);
|
|
@@ -326,17 +334,11 @@ registerTool({
|
|
|
326
334
|
const lat = args.latitude;
|
|
327
335
|
const lon = args.longitude;
|
|
328
336
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let svgString = null;
|
|
334
|
-
if (svgBinary && typeof svgBinary === "object" && svgBinary.encoding === "base64" && svgBinary.data_base64) {
|
|
335
|
-
svgString = Buffer.from(svgBinary.data_base64, "base64").toString("utf-8");
|
|
336
|
-
}
|
|
337
|
-
const uiPayload = {
|
|
337
|
+
// Fetch updated chart JSON only — chart renders client-side in the iframe
|
|
338
|
+
const chartData = await client.post("/ephemeris/natal-chart", natalBody);
|
|
339
|
+
// Return only chart data (no SVG) — the UI calls chart_wheel_fetch_svg
|
|
340
|
+
const payload = {
|
|
338
341
|
...chartData,
|
|
339
|
-
_svg_base: svgString,
|
|
340
342
|
_birth_params: {
|
|
341
343
|
datetime,
|
|
342
344
|
timezone: args.timezone ?? null,
|
|
@@ -347,7 +349,7 @@ registerTool({
|
|
|
347
349
|
house_system: houseSystem,
|
|
348
350
|
};
|
|
349
351
|
return {
|
|
350
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
352
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
351
353
|
};
|
|
352
354
|
},
|
|
353
355
|
});
|
|
@@ -365,6 +367,97 @@ function zodSignFromLon(lon) {
|
|
|
365
367
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
|
|
366
368
|
return signs[Math.floor(((lon % 360) + 360) % 360 / 30)];
|
|
367
369
|
}
|
|
370
|
+
const CLASSICAL_PLANETS = new Set([
|
|
371
|
+
"sun", "moon", "mercury", "venus", "mars",
|
|
372
|
+
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
373
|
+
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* Build a lean, SVG-free payload for Claude's text context.
|
|
377
|
+
* Strips SVG, filters to classical planets only, and packages birth params.
|
|
378
|
+
* Tokens saved vs. full uiPayload: ~85–95% reduction (no 100KB SVG, no asteroid flood).
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Compute major aspects between a set of planets.
|
|
382
|
+
* Returns a structured AspectData[] array — the natal chart API does not
|
|
383
|
+
* include aspects in its JSON response; we derive them from longitude data.
|
|
384
|
+
*/
|
|
385
|
+
function computeAspects(planets) {
|
|
386
|
+
const ASPECTS = [
|
|
387
|
+
{ type: "conjunction", angle: 0, orb: 8 },
|
|
388
|
+
{ type: "opposition", angle: 180, orb: 8 },
|
|
389
|
+
{ type: "trine", angle: 120, orb: 8 },
|
|
390
|
+
{ type: "square", angle: 90, orb: 7 },
|
|
391
|
+
{ type: "sextile", angle: 60, orb: 6 },
|
|
392
|
+
{ type: "quincunx", angle: 150, orb: 3 },
|
|
393
|
+
{ type: "semisquare", angle: 45, orb: 2 },
|
|
394
|
+
];
|
|
395
|
+
const results = [];
|
|
396
|
+
for (let i = 0; i < planets.length; i++) {
|
|
397
|
+
for (let j = i + 1; j < planets.length; j++) {
|
|
398
|
+
const p1 = planets[i];
|
|
399
|
+
const p2 = planets[j];
|
|
400
|
+
// Angular separation: shortest arc (0–180)
|
|
401
|
+
let diff = Math.abs(p1.longitude - p2.longitude) % 360;
|
|
402
|
+
if (diff > 180)
|
|
403
|
+
diff = 360 - diff;
|
|
404
|
+
for (const asp of ASPECTS) {
|
|
405
|
+
const orb = Math.abs(diff - asp.angle);
|
|
406
|
+
if (orb <= asp.orb) {
|
|
407
|
+
// Applying: combined longitudinal speed closing the orb
|
|
408
|
+
const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
|
|
409
|
+
results.push({
|
|
410
|
+
planet1: p1.name,
|
|
411
|
+
planet2: p2.name,
|
|
412
|
+
type: asp.type,
|
|
413
|
+
angle: asp.angle,
|
|
414
|
+
orb: Math.round(orb * 100) / 100,
|
|
415
|
+
applying,
|
|
416
|
+
});
|
|
417
|
+
break; // Each pair only matches one aspect type
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return results;
|
|
423
|
+
}
|
|
424
|
+
function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
425
|
+
// ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
|
|
426
|
+
// The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
|
|
427
|
+
// The chart wheel UI expects PlanetData[] with canonical field names.
|
|
428
|
+
const rawPlanets = chartData.planets;
|
|
429
|
+
let planetsArray = [];
|
|
430
|
+
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
431
|
+
planetsArray = Object.entries(rawPlanets)
|
|
432
|
+
.filter(([k]) => CLASSICAL_PLANETS.has(k.toLowerCase()))
|
|
433
|
+
.map(([name, p]) => ({
|
|
434
|
+
name: name.toLowerCase(),
|
|
435
|
+
longitude: (p.longitude ?? p.lon ?? 0),
|
|
436
|
+
latitude: (p.latitude ?? p.lat),
|
|
437
|
+
speed: (p.longitude_speed ?? p.speed),
|
|
438
|
+
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
439
|
+
sign: (p.sign_name ?? p.sign),
|
|
440
|
+
house: (p.house),
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
else if (Array.isArray(chartData.planets)) {
|
|
444
|
+
// Already normalised (e.g. house system recalculate path)
|
|
445
|
+
planetsArray = chartData.planets;
|
|
446
|
+
}
|
|
447
|
+
// ── 2. Compute aspects from longitude data (API doesn't return them) ──────
|
|
448
|
+
const aspects = computeAspects(planetsArray);
|
|
449
|
+
return {
|
|
450
|
+
...chartData,
|
|
451
|
+
// Typed array replaces the keyed object — the renderer can now iterate it
|
|
452
|
+
planets: planetsArray,
|
|
453
|
+
// Structured aspect array — powers highlighting and info panel
|
|
454
|
+
aspects,
|
|
455
|
+
// Embed SVG for fast first-render in the iframe.
|
|
456
|
+
_svg_base: svgBase ?? undefined,
|
|
457
|
+
_birth_params: birthParams,
|
|
458
|
+
house_system: houseSystem,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
368
461
|
function buildChartSummary(data, location) {
|
|
369
462
|
// The natal API returns planets as a keyed object { sun: {...}, moon: {...} }.
|
|
370
463
|
// Normalise to an array before processing.
|
|
@@ -373,12 +466,65 @@ function buildChartSummary(data, location) {
|
|
|
373
466
|
? raw
|
|
374
467
|
: Object.entries(raw).map(([name, p]) => ({
|
|
375
468
|
name,
|
|
376
|
-
sign: p.sign,
|
|
469
|
+
sign: (p.sign_name ?? p.sign),
|
|
377
470
|
house: p.house,
|
|
378
|
-
retrograde: Boolean(p.retrograde),
|
|
471
|
+
retrograde: Boolean(p.is_retrograde ?? p.retrograde),
|
|
472
|
+
longitude: p.longitude,
|
|
473
|
+
longitude_speed: p.longitude_speed,
|
|
379
474
|
}));
|
|
380
|
-
const
|
|
475
|
+
const CLASSICAL_ORDER = [
|
|
476
|
+
"sun", "moon", "mercury", "venus", "mars",
|
|
477
|
+
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
478
|
+
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
479
|
+
];
|
|
480
|
+
planetsArray.sort((a, b) => {
|
|
481
|
+
const ai = CLASSICAL_ORDER.indexOf(a.name.toLowerCase());
|
|
482
|
+
const bi = CLASSICAL_ORDER.indexOf(b.name.toLowerCase());
|
|
483
|
+
if (ai !== -1 && bi !== -1)
|
|
484
|
+
return ai - bi;
|
|
485
|
+
if (ai !== -1)
|
|
486
|
+
return -1;
|
|
487
|
+
if (bi !== -1)
|
|
488
|
+
return 1;
|
|
489
|
+
return a.name.localeCompare(b.name);
|
|
490
|
+
});
|
|
491
|
+
const lines = planetsArray.slice(0, 12).map((p) => `• **${capitalize(p.name)}** in ${p.sign ?? "?"} (H${p.house ?? "?"})${p.retrograde ? " ℞" : ""}`);
|
|
492
|
+
// ── Enrichment: sect, element tally, strongest aspect ──────────────────
|
|
493
|
+
const sun = planetsArray.find((p) => p.name.toLowerCase() === "sun");
|
|
494
|
+
const ascLon = data.ascendant;
|
|
495
|
+
const sunLon = sun?.longitude;
|
|
496
|
+
let sectNote = "";
|
|
497
|
+
if (sunLon != null && ascLon != null) {
|
|
498
|
+
// Day chart: Sun above horizon (houses 7–12, i.e. longitude within 180° of ASC going upper half)
|
|
499
|
+
const sunRelative = ((sunLon - ascLon) + 360) % 360;
|
|
500
|
+
sectNote = sunRelative < 180 ? "Day chart ☀️" : "Night chart 🌙";
|
|
501
|
+
}
|
|
502
|
+
const ELEMENT_MAP = {
|
|
503
|
+
aries: "fire", leo: "fire", sagittarius: "fire",
|
|
504
|
+
taurus: "earth", virgo: "earth", capricorn: "earth",
|
|
505
|
+
gemini: "air", libra: "air", aquarius: "air",
|
|
506
|
+
cancer: "water", scorpio: "water", pisces: "water",
|
|
507
|
+
};
|
|
508
|
+
const elementCount = { fire: 0, earth: 0, air: 0, water: 0 };
|
|
509
|
+
for (const p of planetsArray.slice(0, 10)) {
|
|
510
|
+
const sign = (p.sign ?? "").toLowerCase();
|
|
511
|
+
const el = ELEMENT_MAP[sign];
|
|
512
|
+
if (el)
|
|
513
|
+
elementCount[el]++;
|
|
514
|
+
}
|
|
515
|
+
const domElement = Object.entries(elementCount).sort((a, b) => b[1] - a[1])[0];
|
|
516
|
+
// Strongest aspect by tightest orb (use computed aspects from planetsArray)
|
|
517
|
+
const aspects = computeAspects(planetsArray
|
|
518
|
+
.filter((p) => p.longitude != null)
|
|
519
|
+
.map((p) => ({ name: p.name, longitude: p.longitude, speed: p.longitude_speed })));
|
|
520
|
+
const strongest = aspects.sort((a, b) => a.orb - b.orb)[0];
|
|
521
|
+
const strongestNote = strongest
|
|
522
|
+
? `Closest aspect: ${capitalize(strongest.planet1)} ${strongest.type} ${capitalize(strongest.planet2)} (${strongest.orb.toFixed(1)}° orb)`
|
|
523
|
+
: "";
|
|
524
|
+
const enrichment = [sectNote, domElement ? `Dominant element: ${domElement[0]} (${domElement[1]})` : "", strongestNote]
|
|
525
|
+
.filter(Boolean).join(" · ");
|
|
381
526
|
return (`**Natal Chart — ${location}**\n\n` +
|
|
527
|
+
(enrichment ? `${enrichment}\n\n` : "") +
|
|
382
528
|
`Key Placements:\n${lines.join("\n")}\n\n` +
|
|
383
529
|
"Click any planet, house, or aspect line for interpretation.");
|
|
384
530
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -10,8 +10,25 @@ export interface ToolDefinition {
|
|
|
10
10
|
idempotentHint?: boolean;
|
|
11
11
|
openWorldHint?: boolean;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* UI metadata — mirrors the ext-apps SDK _meta.ui shape.
|
|
15
|
+
* When visibility includes "app" only (not "model"), the tool is hidden from
|
|
16
|
+
* Claude's tool list (ListTools) but remains callable by the UI via callServerTool().
|
|
17
|
+
*/
|
|
18
|
+
_meta?: {
|
|
19
|
+
ui?: {
|
|
20
|
+
resourceUri?: string;
|
|
21
|
+
/** SDK visibility array — use ["app"] to hide from model, ["model"] for model-only. */
|
|
22
|
+
visibility?: Array<"app" | "model">;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
13
25
|
handler: (args: any) => Promise<any>;
|
|
14
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
29
|
+
* Filters out tools that have visibility restricted to the UI (app-only).
|
|
30
|
+
*/
|
|
31
|
+
export declare function modelVisibleTools(): ToolDefinition[];
|
|
15
32
|
export declare const toolRegistry: Record<string, ToolDefinition>;
|
|
16
33
|
export declare function registerTool(tool: ToolDefinition): void;
|
|
17
34
|
export type ToolProfile = "dev" | "legacy";
|
|
@@ -37,23 +54,4 @@ export declare function validateCoordinates(args: any, latKey: string, lonKey: s
|
|
|
37
54
|
* Intercepts embedded VisualResult objects (include_visual=true) and returns dual content.
|
|
38
55
|
* Applies a safety limit to JSON payloads to prevent crashing LLM contexts.
|
|
39
56
|
*/
|
|
40
|
-
export declare function formatToolResponse(toolName: string, result: any, durationMs: number):
|
|
41
|
-
content: ({
|
|
42
|
-
type: string;
|
|
43
|
-
data: string;
|
|
44
|
-
mimeType: string;
|
|
45
|
-
text?: undefined;
|
|
46
|
-
} | {
|
|
47
|
-
type: string;
|
|
48
|
-
text: string;
|
|
49
|
-
data?: undefined;
|
|
50
|
-
mimeType?: undefined;
|
|
51
|
-
})[];
|
|
52
|
-
isError?: undefined;
|
|
53
|
-
} | {
|
|
54
|
-
content: {
|
|
55
|
-
type: string;
|
|
56
|
-
text: string;
|
|
57
|
-
}[];
|
|
58
|
-
isError: boolean;
|
|
59
|
-
};
|
|
57
|
+
export declare function formatToolResponse(toolName: string, result: any, durationMs: number): any;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns tools that should be exposed to Claude in the ListTools response.
|
|
3
|
+
* Filters out tools that have visibility restricted to the UI (app-only).
|
|
4
|
+
*/
|
|
5
|
+
export function modelVisibleTools() {
|
|
6
|
+
return Object.values(toolRegistry).filter((t) => {
|
|
7
|
+
const vis = t._meta?.ui?.visibility;
|
|
8
|
+
// If visibility is explicitly ["app"] (no "model"), hide from model
|
|
9
|
+
if (vis && !vis.includes("model") && vis.includes("app"))
|
|
10
|
+
return false;
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
1
14
|
export const toolRegistry = {};
|
|
2
15
|
export function registerTool(tool) {
|
|
3
16
|
toolRegistry[tool.name] = tool;
|
|
@@ -45,6 +58,8 @@ export async function initTools(profile) {
|
|
|
45
58
|
await import("./specialized/acg.js");
|
|
46
59
|
// MCP App tools (interactive UI)
|
|
47
60
|
await import("./apps/chart-wheel-app.js");
|
|
61
|
+
await import("./apps/bodygraph-app.js");
|
|
62
|
+
await import("./apps/bi-wheel-app.js");
|
|
48
63
|
}
|
|
49
64
|
}
|
|
50
65
|
/**
|
|
@@ -77,6 +92,16 @@ export function validateCoordinates(args, latKey, lonKey) {
|
|
|
77
92
|
* Applies a safety limit to JSON payloads to prevent crashing LLM contexts.
|
|
78
93
|
*/
|
|
79
94
|
export function formatToolResponse(toolName, result, durationMs) {
|
|
95
|
+
// Case: handler already returned a pre-formed MCP content array (e.g. app tools like
|
|
96
|
+
// explore_bi_wheel that build { content: [...], _meta: {...} } themselves).
|
|
97
|
+
if (result &&
|
|
98
|
+
typeof result === "object" &&
|
|
99
|
+
Array.isArray(result.content) &&
|
|
100
|
+
result.content.length > 0 &&
|
|
101
|
+
typeof result.content[0]?.type === "string") {
|
|
102
|
+
console.error(`[MCP] ✅ Success (pre-formed): ${toolName} (${durationMs}ms)`);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
80
105
|
// Case: data response with embedded visual (include_visual=true)
|
|
81
106
|
// VisualResult.data for SVG is a raw string; for PNG it is already base64.
|
|
82
107
|
if (result &&
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Bi-Wheel Explorer</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<style>:root{--bg:#faf9f5;--surface:#fff;--text:#1a1a2e;--text-secondary:#5a5a7a;--accent:#6366f1;--accent-hover:#4f46e5;--accent-subtle:#6366f11f;--border:#e8e6f0;--border-strong:#c8c4d8;--shadow:0 4px 16px #00000014;--shadow-md:0 8px 32px #0000001f;--radius:10px;--radius-sm:6px;--inner-glyph:#4a3a8a;--inner-tick:#7060b8;--inner-ring:#f2ede6;--inner-stroke:#c0b8a8;--outer-glyph:#92650a;--outer-tick:#c8860e;--outer-ring:#fdf5e4;--outer-stroke:#d4ac60;--xasp-conjunction:#9a7fd4;--xasp-opposition:#e05252;--xasp-trine:#52a8e0;--xasp-square:#e08a52;--xasp-sextile:#52c07a}@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--surface:#161b22;--text:#e6edf3;--text-secondary:#8b949e;--accent:#818cf8;--accent-hover:#a5b4fc;--accent-subtle:#818cf826;--border:#21262d;--border-strong:#30363d;--shadow:0 4px 16px #0006;--shadow-md:0 8px 32px #0009;--inner-glyph:#b0a0e8;--inner-tick:#9080c8;--outer-glyph:#fbbf24;--outer-tick:#d97706;--inner-ring:#1a1628;--outer-ring:#1c1810;--inner-stroke:#3a3460;--outer-stroke:#6b5210}}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{background:var(--bg);color:var(--text);-webkit-user-select:none;user-select:none;font-family:-apple-system,Inter,Segoe UI,system-ui,sans-serif;font-size:13px;line-height:1.5;overflow:hidden}#app{flex-direction:column;height:100vh;display:flex;position:relative}#loading-state{color:var(--text-secondary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;display:flex}.spinner{border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:32px;height:32px;animation:.8s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}#chart-container{flex:1;justify-content:center;align-items:center;min-height:0;padding:12px;display:flex;position:relative}#chart-container svg{width:auto;max-width:100%;height:auto;max-height:100%}#mode-badge{border-top:1px solid var(--border);background:var(--surface);flex-shrink:0;justify-content:center;align-items:center;gap:10px;padding:6px 14px;font-size:11px;display:flex}.mode-badge-inner{color:var(--inner-glyph);align-items:center;gap:5px;font-weight:600;display:inline-flex}.mode-badge-outer{color:var(--outer-glyph);align-items:center;gap:5px;font-weight:600;display:inline-flex}.mode-badge-sep{color:var(--text-secondary);font-weight:400}.mode-badge-dot-inner{background:var(--inner-tick);border-radius:50%;flex-shrink:0;width:9px;height:9px}.mode-badge-dot-outer{background:var(--outer-tick);border-radius:50%;flex-shrink:0;width:9px;height:9px}.hit-region{cursor:pointer;fill:#0000;stroke:#0000;transition:fill .15s,stroke .15s,opacity .2s}.hit-region:hover{fill:var(--accent-subtle);stroke:var(--accent);stroke-width:1px;stroke-opacity:.5}.hit-region.planet-hit{rx:12px;ry:12px}.hit-region.dim{opacity:.12}.hit-region.aspect-hit{stroke-width:13px}.info-panel{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius);box-shadow:var(--shadow-md);z-index:900;opacity:0;pointer-events:none;width:min(360px,92vw);padding:14px 16px 12px;transition:opacity .18s,transform .18s;position:fixed;bottom:56px;left:50%;transform:translate(-50%)translateY(12px)}.info-panel.visible{opacity:1;pointer-events:all;transform:translate(-50%)translateY(0)}.info-header{align-items:flex-start;gap:10px;margin-bottom:12px;display:flex}.info-glyph{text-align:center;flex-shrink:0;width:32px;font-size:26px;line-height:1}.info-title{color:var(--text);flex:1;font-size:13px;font-weight:700}.info-subtitle{color:var(--text-secondary);margin-top:2px;font-size:11px;font-weight:400}.wheel-badge{white-space:nowrap;border-radius:99px;align-items:center;gap:3px;padding:1px 7px;font-size:10px;font-weight:600;line-height:1.6;display:inline-flex}.wheel-badge.inner{color:var(--inner-glyph);background:#7060b826;border:1px solid #7060b859}.wheel-badge.outer{color:var(--outer-glyph);background:#c8860e26;border:1px solid #c8860e59}.info-close{cursor:pointer;color:var(--text-secondary);background:0 0;border:none;border-radius:4px;flex-shrink:0;padding:2px 4px;font-size:13px;line-height:1;transition:color .12s}.info-close:hover{color:var(--text)}.aspects-title{text-transform:uppercase;letter-spacing:.08em;color:var(--text-secondary);margin-bottom:6px;font-size:10px;font-weight:700}.aspects-list{flex-direction:column;gap:4px;max-height:130px;display:flex;overflow-y:auto}.aspect-row{align-items:center;gap:7px;padding:3px 0;font-size:12px;display:flex}.aspect-dot{border-radius:50%;flex-shrink:0;width:8px;height:8px}.aspect-sym{text-align:center;flex-shrink:0;width:16px;font-size:14px}.aspect-other{color:var(--text);flex:1}.aspect-meta{color:var(--text-secondary);white-space:nowrap;font-size:11px}.no-aspects{color:var(--text-secondary);padding:4px 0;font-size:11px;font-style:italic}.info-hint{color:var(--accent);text-align:center;margin-top:10px;font-size:10px;font-style:italic}.tooltip{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius);pointer-events:none;box-shadow:var(--shadow-md);z-index:1000;opacity:0;max-width:240px;padding:8px 12px;font-size:12px;transition:opacity .12s;position:fixed}.tooltip.visible{opacity:1}.tooltip-title{color:var(--text);margin-bottom:2px;font-weight:700}.tooltip-detail{color:var(--text-secondary);line-height:1.4}.tooltip-hint{color:var(--accent);margin-top:4px;font-size:10px;font-style:italic}.error-state{text-align:center;color:var(--text-secondary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px;display:flex}.error-state .error-icon{margin-bottom:4px;font-size:32px}.error-state .error-msg{color:var(--text-secondary);text-align:center;max-width:280px;font-size:13px;line-height:1.5}#chart-hint{text-align:center;color:var(--text-secondary);letter-spacing:.03em;opacity:.75;flex-shrink:0;padding:4px 12px 6px;font-size:10px}
|
|
10
|
+
/*$vite$:1*/</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app">
|
|
14
|
+
<div id="loading-state">
|
|
15
|
+
<div class="spinner"></div>
|
|
16
|
+
<p>Loading bi-wheel…</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div id="chart-container" hidden></div>
|
|
19
|
+
<div id="mode-badge" hidden></div>
|
|
20
|
+
<div id="tooltip" class="tooltip" aria-live="polite"></div>
|
|
21
|
+
<div id="chart-hint" class="chart-hint">Click any planet or aspect line to explore · Press Esc to close</div>
|
|
22
|
+
</div>
|
|
23
|
+
<script type="module">(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=250,t=250,n=240,r=204,i=190,a=159,o=140,s=134,c=290,l=246,u=264,d=16;function f(n,r,i){let a=-(n-i+180)*Math.PI/180;return{x:e+r*Math.cos(a),y:t+r*Math.sin(a)}}function p(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}var m={sun:`☉`,moon:`☽`,mercury:`☿`,venus:`♀`,mars:`♂`,jupiter:`♃`,saturn:`♄`,uranus:`♅`,neptune:`♆`,pluto:`♇`,"north node":`☊`,"south node":`☋`,chiron:`⚷`,"true node":`☊`,north_node:`☊`,south_node:`☋`,true_node:`☊`,asc:`AC`,mc:`MC`},h=[`♈`,`♉`,`♊`,`♋`,`♌`,`♍`,`♎`,`♏`,`♐`,`♑`,`♒`,`♓`],g=[`Aries`,`Taurus`,`Gemini`,`Cancer`,`Leo`,`Virgo`,`Libra`,`Scorpio`,`Sagittarius`,`Capricorn`,`Aquarius`,`Pisces`],_={conjunction:`☌`,opposition:`☍`,trine:`△`,square:`□`,sextile:`⚹`},v={conjunction:`#9a7fd4`,opposition:`#e05252`,trine:`#52a8e0`,square:`#e08a52`,sextile:`#52c07a`},y=[`#e8854c`,`#7aaa6a`,`#5aaad0`,`#8878c8`,`#e8854c`,`#7aaa6a`,`#5aaad0`,`#8878c8`,`#e8854c`,`#7aaa6a`,`#5aaad0`,`#8878c8`],b=new Set([`sun`,`moon`,`mercury`,`venus`,`mars`,`jupiter`,`saturn`,`uranus`,`neptune`,`pluto`,`chiron`,`north node`,`south node`,`true node`,`north_node`,`south_node`,`true_node`,`asc`,`mc`]);function x(e){let t=(e%360+360)%360,n=Math.floor(t/30),r=Math.floor(t%30);return`${r}°${Math.floor((t%30-r)*60).toString().padStart(2,`0`)}′ ${g[n]}`}function S(e){return e.replace(/\b\w/g,e=>e.toUpperCase())}function C(e){return String(e).replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}function w(e){return Array.isArray(e)?e.filter(e=>e&&typeof e.longitude==`number`&&isFinite(e.longitude)):e&&typeof e==`object`?Object.entries(e).filter(([e])=>b.has(e.toLowerCase())).map(([e,t])=>({name:e.toLowerCase(),longitude:t.longitude??t.lon??0,speed:t.longitude_speed??t.speed,retrograde:!!(t.is_retrograde??t.retrograde),sign:t.sign_name??t.sign,house:t.house})):[]}var T=[{type:`conjunction`,angle:0,orb:8},{type:`opposition`,angle:180,orb:8},{type:`trine`,angle:120,orb:6},{type:`square`,angle:90,orb:5},{type:`sextile`,angle:60,orb:4}];function ee(e,t){let n=[];for(let r of e)for(let e of t){let t=Math.abs(r.longitude-e.longitude)%360;t>180&&(t=360-t);for(let i of T){let a=Math.abs(t-i.angle);if(a<=i.orb){let o=(r.speed??1)-(e.speed??0)<0?t<i.angle:t>i.angle;n.push({planet1:r.name,wheel1:`inner`,planet2:e.name,wheel2:`outer`,type:i.type,angle:i.angle,orb:Math.round(a*100)/100,applying:o});break}}}return n.sort((e,t)=>e.orb-t.orb).slice(0,30)}var E=null;function D(){return E||=document.getElementById(`tooltip`),E}function O(e,t,n,r,i){let a=D();a.innerHTML=`
|
|
24
|
+
<div class="tooltip-title">${n}</div>
|
|
25
|
+
<div class="tooltip-detail">${r}</div>
|
|
26
|
+
${i?`<div class="tooltip-hint">${i}</div>`:``}
|
|
27
|
+
`;let o=e+14,s=t+10;o+240>window.innerWidth&&(o=e-240-8),s+80>window.innerHeight&&(s=t-80-8),a.style.left=`${o}px`,a.style.top=`${s}px`,a.classList.add(`visible`)}function k(){D().classList.remove(`visible`)}var A=null,j=null,M=null,N=new Map;function P(e){return`${e.planet1}|inner|${e.planet2}|outer|${e.type}`}function F(){return A||(A=document.createElement(`div`),A.id=`bi-info-panel`,A.className=`info-panel`,document.body.appendChild(A),A)}function I(){A?.classList.remove(`visible`)}function L(e,t,n,r){let i=F(),a=m[e.name.toLowerCase()]??`★`,o=h[Math.floor((e.longitude%360+360)%360/30)]??``,s=t===`inner`?`Inner · Natal`:r===`transit`?`Outer · Transit`:`Outer · Synastry`,c=t,l=n.filter(n=>t===`inner`&&n.planet1===e.name||t===`outer`&&n.planet2===e.name).sort((e,t)=>e.orb-t.orb),u=l.length?l.map(e=>{let n=t===`inner`?e.planet2:e.planet1,i=m[n.toLowerCase()]??n,a=_[e.type?.toLowerCase()??``]??e.type??`—`,o=v[e.type?.toLowerCase()??``]??`#888`,s=`${e.orb.toFixed(1)}°`,c=r===`transit`?e.applying?`↗`:`↘`:e.orb<=1?`exact`:e.orb<=3?`tight`:`wide`;return`<div class="aspect-row">
|
|
28
|
+
<span class="aspect-dot" style="background:${o}"></span>
|
|
29
|
+
<span class="aspect-sym" style="color:${o}">${a}</span>
|
|
30
|
+
<span class="aspect-other">${C(i)} ${C(S(n))}</span>
|
|
31
|
+
<span class="aspect-meta">${s} ${c}</span>
|
|
32
|
+
</div>`}).join(``):`<div class="no-aspects">No cross-aspects within orb</div>`;i.innerHTML=`
|
|
33
|
+
<div class="info-header">
|
|
34
|
+
<span class="info-glyph">${C(a)}</span>
|
|
35
|
+
<div class="info-title">
|
|
36
|
+
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
|
37
|
+
<strong>${C(S(e.name))}</strong>
|
|
38
|
+
${e.retrograde?`<span style='color:#e05252'>℞</span>`:``}
|
|
39
|
+
<span class="wheel-badge ${c}">${C(s)}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="info-subtitle">${C(o)} ${C(x(e.longitude))}${e.house?` · House ${e.house}`:``}</div>
|
|
42
|
+
</div>
|
|
43
|
+
<button class="info-close" id="bi-info-close-btn">✕</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="aspects-title">Cross-Aspects</div>
|
|
46
|
+
<div class="aspects-list">${u}</div>
|
|
47
|
+
<div class="info-hint">Click a cross-aspect line for full interpretation</div>
|
|
48
|
+
`,i.classList.add(`visible`),document.getElementById(`bi-info-close-btn`)?.addEventListener(`click`,()=>{I(),B()})}function te(e,t){let n=F(),r=m[e.planet1.toLowerCase()]??`★`,i=m[e.planet2.toLowerCase()]??`★`,a=_[e.type?.toLowerCase()??``]??e.type,o=v[e.type?.toLowerCase()??``]??`#888`,s=S(e.planet1),c=t===`transit`?`Tr. ${S(e.planet2)}`:S(e.planet2),l=t===`transit`?e.applying?`↗ Applying`:`↘ Separating`:e.orb<=1?`Exact`:e.orb<=3?`Tight`:`Wide`;n.innerHTML=`
|
|
49
|
+
<div class="info-header">
|
|
50
|
+
<span class="info-glyph" style="color:${o}">${C(String(a))}</span>
|
|
51
|
+
<div class="info-title">
|
|
52
|
+
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;">
|
|
53
|
+
<strong>${C(r)} ${C(s)}</strong>
|
|
54
|
+
<span style="color:${o}">${C(String(a))}</span>
|
|
55
|
+
<strong>${C(i)} ${C(c)}</strong>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="info-subtitle">${e.orb.toFixed(1)}° orb · ${C(l)}</div>
|
|
58
|
+
</div>
|
|
59
|
+
<button class="info-close" id="bi-info-close-btn">✕</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div style="display:flex;gap:6px;margin-bottom:10px;">
|
|
62
|
+
<span class="wheel-badge inner">Inner · ${C(s)}</span>
|
|
63
|
+
<span class="wheel-badge outer">Outer · ${C(c)}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="info-hint">Loading interpretation…</div>
|
|
66
|
+
`,n.classList.add(`visible`),document.getElementById(`bi-info-close-btn`)?.addEventListener(`click`,()=>{I(),B()})}function R(e,t){N.forEach(({vis:n,color:r},i)=>{let a=i.split(`|`);(t===`inner`?a[0]===e:a[2]===e)?(n.style.stroke=r,n.style.strokeOpacity=`0.92`,n.style.strokeWidth=`2`):(n.style.strokeOpacity=`0.05`,n.style.strokeWidth=`0.7`)}),z(e,t)}function ne(e,t){N.forEach(({vis:n,color:r},i)=>{i===e?(n.style.stroke=t,n.style.strokeOpacity=`0.92`,n.style.strokeWidth=`2.5`):(n.style.strokeOpacity=`0.05`,n.style.strokeWidth=`0.7`)})}function z(e,t){M?.querySelectorAll(`.planet-hit`).forEach(n=>{let r=n.dataset.wheel,i=(n.dataset.planet??``)===e&&r===t;n.classList.toggle(`dim`,!i)})}function B(){N.forEach(({vis:e,color:t})=>{e.style.stroke=t,e.style.strokeOpacity=`0.45`,e.style.strokeWidth=`1`}),M?.querySelectorAll(`.planet-hit`).forEach(e=>e.classList.remove(`dim`))}function V(e,t,g,b){let C=t.mode??`synastry`;e.innerHTML=``,N.clear(),I();let T=w(t.inner),E=w(t.outer),D=t.inner_houses??[],A=t.cross_aspects??[];A.length===0&&T.length>0&&E.length>0&&(A=ee(T,E));let F=D[0]?.cusp_lon??t.ascendant??0,z=p(`svg`);z.setAttribute(`viewBox`,`0 0 500 500`),z.setAttribute(`width`,`100%`),z.setAttribute(`height`,`100%`),z.style.display=`block`,e.appendChild(z),M=z;let V=p(`circle`);V.setAttribute(`cx`,`250`),V.setAttribute(`cy`,`250`),V.setAttribute(`r`,`294`),V.style.fill=`var(--bg, #faf9f5)`,V.setAttribute(`stroke`,`#d4cec8`),V.setAttribute(`stroke-width`,`0.5`),V.setAttribute(`pointer-events`,`none`),z.appendChild(V);let H=p(`circle`);H.setAttribute(`cx`,`250`),H.setAttribute(`cy`,`250`),H.setAttribute(`r`,String(c)),H.setAttribute(`fill`,`var(--outer-ring, #fdf5e4)`),H.setAttribute(`stroke`,`var(--outer-stroke, #d4ac60)`),H.setAttribute(`stroke-width`,`0.8`),H.setAttribute(`pointer-events`,`none`),z.appendChild(H);let U=p(`circle`);U.setAttribute(`cx`,`250`),U.setAttribute(`cy`,`250`),U.setAttribute(`r`,String(l)),U.style.fill=`var(--bg, #faf9f5)`,U.setAttribute(`stroke`,`var(--outer-stroke, #d4ac60)`),U.setAttribute(`stroke-width`,`0.8`),U.setAttribute(`pointer-events`,`none`),z.appendChild(U);for(let e=0;e<12;e++){let t=e*30,{x:n,y:r}=f(t,l,F),{x:i,y:a}=f(t,c,F),o=p(`line`);o.setAttribute(`x1`,n.toFixed(1)),o.setAttribute(`y1`,r.toFixed(1)),o.setAttribute(`x2`,i.toFixed(1)),o.setAttribute(`y2`,a.toFixed(1)),o.setAttribute(`stroke`,`var(--outer-stroke, #d4ac60)`),o.setAttribute(`stroke-width`,`0.5`),o.setAttribute(`pointer-events`,`none`),z.appendChild(o);let{x:s,y:u}=f(t+15,(l+c)/2,F),d=p(`text`);d.setAttribute(`x`,s.toFixed(1)),d.setAttribute(`y`,u.toFixed(1)),d.setAttribute(`text-anchor`,`middle`),d.setAttribute(`dominant-baseline`,`central`),d.setAttribute(`font-size`,`12`),d.setAttribute(`fill`,re(y[e])),d.setAttribute(`pointer-events`,`none`),d.style.fontFamily=`serif`,d.textContent=h[e],z.appendChild(d)}let W=p(`circle`);W.setAttribute(`cx`,`250`),W.setAttribute(`cy`,`250`),W.setAttribute(`r`,String(n)),W.setAttribute(`fill`,`var(--inner-ring, #f2ede6)`),W.setAttribute(`stroke`,`var(--inner-stroke, #c0b8a8)`),W.setAttribute(`stroke-width`,`0.5`),W.setAttribute(`pointer-events`,`none`),z.appendChild(W);let G=p(`circle`);G.setAttribute(`cx`,`250`),G.setAttribute(`cy`,`250`),G.setAttribute(`r`,String(r)),G.style.fill=`var(--bg, #faf9f5)`,G.setAttribute(`stroke`,`var(--inner-stroke, #c0b8a8)`),G.setAttribute(`stroke-width`,`0.5`),G.setAttribute(`pointer-events`,`none`),z.appendChild(G);for(let e=0;e<12;e++){let t=e*30,{x:i,y:a}=f(t,r,F),{x:o,y:s}=f(t,n,F),c=p(`line`);c.setAttribute(`x1`,i.toFixed(1)),c.setAttribute(`y1`,a.toFixed(1)),c.setAttribute(`x2`,o.toFixed(1)),c.setAttribute(`y2`,s.toFixed(1)),c.setAttribute(`stroke`,`var(--inner-stroke, #c0b8a8)`),c.setAttribute(`stroke-width`,`0.5`),c.setAttribute(`pointer-events`,`none`),z.appendChild(c);let{x:l,y:u}=f(t+15,(r+n)/2,F),d=p(`text`);d.setAttribute(`x`,l.toFixed(1)),d.setAttribute(`y`,u.toFixed(1)),d.setAttribute(`text-anchor`,`middle`),d.setAttribute(`dominant-baseline`,`central`),d.setAttribute(`font-size`,`13`),d.setAttribute(`fill`,y[e]),d.setAttribute(`pointer-events`,`none`),d.style.fontFamily=`serif`,d.textContent=h[e],z.appendChild(d)}let K=p(`circle`);if(K.setAttribute(`cx`,`250`),K.setAttribute(`cy`,`250`),K.setAttribute(`r`,String(o)),K.style.fill=`var(--bg, #faf9f5)`,K.setAttribute(`stroke`,`var(--inner-stroke, #c0b8a8)`),K.setAttribute(`stroke-width`,`0.5`),K.setAttribute(`pointer-events`,`none`),z.appendChild(K),D.length>0)for(let e=0;e<D.length;e++){let t=D[e],n=D[(e+1)%D.length],i=[1,4,7,10].includes(t.number),{x:s,y:c}=f(t.cusp_lon,o,F),{x:l,y:u}=f(t.cusp_lon,r,F),d=p(`line`);d.setAttribute(`x1`,s.toFixed(1)),d.setAttribute(`y1`,c.toFixed(1)),d.setAttribute(`x2`,l.toFixed(1)),d.setAttribute(`y2`,u.toFixed(1)),d.setAttribute(`stroke`,i?`#7060b8`:`#a89898`),d.setAttribute(`stroke-width`,i?`1.5`:`0.7`),d.setAttribute(`pointer-events`,`none`),z.appendChild(d);let m=(n.cusp_lon-t.cusp_lon+360)%360,{x:h,y:g}=f(t.cusp_lon+m/2,a,F),_=p(`text`);_.setAttribute(`x`,h.toFixed(1)),_.setAttribute(`y`,g.toFixed(1)),_.setAttribute(`text-anchor`,`middle`),_.setAttribute(`dominant-baseline`,`central`),_.setAttribute(`font-size`,`9`),_.setAttribute(`fill`,i?`#7060b8`:`#90808a`),_.setAttribute(`pointer-events`,`none`),_.textContent=String(t.number),z.appendChild(_)}let q=D.find(e=>e.number===1),J=D.find(e=>e.number===4),Y=D.find(e=>e.number===7),X=D.find(e=>e.number===10),Z=[{lon:q?.cusp_lon??F,label:`ASC`},{lon:Y?.cusp_lon??(F+180)%360,label:`DSC`},{lon:X?.cusp_lon??(F+270)%360,label:`MC`},{lon:J?.cusp_lon??(F+90)%360,label:`IC`}];for(let e of Z){let{x:t,y:n}=f(e.lon,c+12,F),r=p(`text`);r.setAttribute(`x`,t.toFixed(1)),r.setAttribute(`y`,n.toFixed(1)),r.setAttribute(`text-anchor`,`middle`),r.setAttribute(`dominant-baseline`,`central`),r.setAttribute(`font-size`,`8`),r.setAttribute(`font-weight`,`700`),r.setAttribute(`fill`,`#7060b8`),r.setAttribute(`pointer-events`,`none`),r.textContent=e.label,z.appendChild(r)}for(let e of A){let t=T.find(t=>t.name===e.planet1),n=E.find(t=>t.name===e.planet2);if(!t||!n)continue;let r=v[e.type?.toLowerCase()??``]??`#aaa`,{x:i,y:a}=f(t.longitude,s,F),{x:o,y:c}=f(n.longitude,l,F),u=p(`line`);u.setAttribute(`x1`,i.toFixed(1)),u.setAttribute(`y1`,a.toFixed(1)),u.setAttribute(`x2`,o.toFixed(1)),u.setAttribute(`y2`,c.toFixed(1)),u.setAttribute(`stroke-dasharray`,`4 3`),u.setAttribute(`pointer-events`,`none`),u.style.stroke=r,u.style.strokeOpacity=`0.60`,u.style.strokeWidth=`1.3`,z.appendChild(u);let d=p(`line`);d.setAttribute(`x1`,i.toFixed(1)),d.setAttribute(`y1`,a.toFixed(1)),d.setAttribute(`x2`,o.toFixed(1)),d.setAttribute(`y2`,c.toFixed(1)),d.setAttribute(`stroke`,`transparent`),d.setAttribute(`stroke-width`,`13`),d.setAttribute(`fill`,`none`),d.setAttribute(`class`,`hit-region aspect-hit`),d.style.pointerEvents=`all`;let h=m[t.name.toLowerCase()]??t.name,g=m[n.name.toLowerCase()]??n.name,y=_[e.type?.toLowerCase()??``]??e.type??``,x=C===`transit`?e.applying?` (applying)`:` (separating)`:e.orb<=1?` (exact)`:e.orb<=3?` (tight)`:` (wide)`,w=`${h} ${y} ${g} · ${e.orb.toFixed(1)}° orb${x}`;d.addEventListener(`mouseenter`,t=>O(t.clientX,t.clientY,S(e.type??`Cross-Aspect`),w,`Click for interpretation`)),d.addEventListener(`mousemove`,t=>O(t.clientX,t.clientY,S(e.type??`Cross-Aspect`),w,`Click for interpretation`)),d.addEventListener(`mouseleave`,k),d.addEventListener(`click`,()=>{k(),ne(P(e),r),te(e,C),b(e)}),N.set(P(e),{vis:u,color:r}),z.appendChild(d)}for(let e of E){let t=m[e.name.toLowerCase()]??`★`,{x:n,y:r}=f(e.longitude,u,F),{x:i,y:a}=f(e.longitude,l+5,F),o=p(`circle`);o.setAttribute(`cx`,i.toFixed(1)),o.setAttribute(`cy`,a.toFixed(1)),o.setAttribute(`r`,`2.5`),o.setAttribute(`fill`,`var(--outer-tick, #c8860e)`),o.setAttribute(`opacity`,`0.75`),o.setAttribute(`pointer-events`,`none`),z.appendChild(o);let s=p(`text`);s.setAttribute(`x`,n.toFixed(1)),s.setAttribute(`y`,r.toFixed(1)),s.setAttribute(`text-anchor`,`middle`),s.setAttribute(`dominant-baseline`,`central`),s.setAttribute(`font-size`,`14`),s.setAttribute(`pointer-events`,`none`),s.style.fill=`var(--outer-glyph, #92650a)`,s.style.fontFamily=`serif`,s.textContent=t,z.appendChild(s);let c=Math.floor((e.longitude%360+360)%360%30),{x:h,y:_}=f(e.longitude,u-14,F),v=p(`text`);if(v.setAttribute(`x`,h.toFixed(1)),v.setAttribute(`y`,_.toFixed(1)),v.setAttribute(`text-anchor`,`middle`),v.setAttribute(`dominant-baseline`,`central`),v.setAttribute(`font-size`,`7`),v.setAttribute(`fill`,`#b08030`),v.setAttribute(`pointer-events`,`none`),v.textContent=`${c}°`,z.appendChild(v),e.retrograde){let{x:t,y:n}=f(e.longitude,u+13,F),r=p(`text`);r.setAttribute(`x`,t.toFixed(1)),r.setAttribute(`y`,n.toFixed(1)),r.setAttribute(`text-anchor`,`middle`),r.setAttribute(`dominant-baseline`,`central`),r.setAttribute(`font-size`,`8`),r.setAttribute(`fill`,`#e05252`),r.setAttribute(`pointer-events`,`none`),r.textContent=`℞`,z.appendChild(r)}let y=p(`circle`);y.setAttribute(`cx`,n.toFixed(1)),y.setAttribute(`cy`,r.toFixed(1)),y.setAttribute(`r`,String(d)),y.setAttribute(`fill`,`transparent`),y.setAttribute(`class`,`hit-region planet-hit`),y.dataset.planet=e.name.toLowerCase(),y.dataset.wheel=`outer`,y.style.pointerEvents=`all`;let b=`${x(e.longitude)}${e.house?` · H${e.house}`:``}${e.retrograde?` ℞`:``}`;y.addEventListener(`mouseenter`,n=>O(n.clientX,n.clientY,`${t} ${S(e.name)} (outer)`,b,`Click to explore`)),y.addEventListener(`mousemove`,n=>O(n.clientX,n.clientY,`${t} ${S(e.name)} (outer)`,b,`Click to explore`)),y.addEventListener(`mouseleave`,k),y.addEventListener(`click`,()=>{k(),R(e.name,`outer`),L(e,`outer`,A,C),g(e,`outer`)}),z.appendChild(y)}for(let e of T){let t=m[e.name.toLowerCase()]??`★`,{x:n,y:a}=f(e.longitude,i,F),{x:o,y:s}=f(e.longitude,r-5,F),c=p(`circle`);c.setAttribute(`cx`,o.toFixed(1)),c.setAttribute(`cy`,s.toFixed(1)),c.setAttribute(`r`,`2.5`),c.setAttribute(`fill`,`var(--inner-tick, #7060b8)`),c.setAttribute(`opacity`,`0.65`),c.setAttribute(`pointer-events`,`none`),z.appendChild(c);let l=p(`text`);l.setAttribute(`x`,n.toFixed(1)),l.setAttribute(`y`,a.toFixed(1)),l.setAttribute(`text-anchor`,`middle`),l.setAttribute(`dominant-baseline`,`central`),l.setAttribute(`font-size`,`15`),l.setAttribute(`pointer-events`,`none`),l.style.fill=`var(--inner-glyph, #4a3a8a)`,l.style.fontFamily=`serif`,l.textContent=t,z.appendChild(l);let u=Math.floor((e.longitude%360+360)%360%30),{x:h,y:_}=f(e.longitude,i-17,F),v=p(`text`);if(v.setAttribute(`x`,h.toFixed(1)),v.setAttribute(`y`,_.toFixed(1)),v.setAttribute(`text-anchor`,`middle`),v.setAttribute(`dominant-baseline`,`central`),v.setAttribute(`font-size`,`7`),v.setAttribute(`fill`,`#9080a0`),v.setAttribute(`pointer-events`,`none`),v.textContent=`${u}°`,z.appendChild(v),e.retrograde){let{x:t,y:n}=f(e.longitude,i+14,F),r=p(`text`);r.setAttribute(`x`,t.toFixed(1)),r.setAttribute(`y`,n.toFixed(1)),r.setAttribute(`text-anchor`,`middle`),r.setAttribute(`dominant-baseline`,`central`),r.setAttribute(`font-size`,`8`),r.setAttribute(`fill`,`#e05252`),r.setAttribute(`pointer-events`,`none`),r.textContent=`℞`,z.appendChild(r)}let y=p(`circle`);y.setAttribute(`cx`,n.toFixed(1)),y.setAttribute(`cy`,a.toFixed(1)),y.setAttribute(`r`,String(d)),y.setAttribute(`fill`,`transparent`),y.setAttribute(`class`,`hit-region planet-hit`),y.dataset.planet=e.name.toLowerCase(),y.dataset.wheel=`inner`,y.style.pointerEvents=`all`;let b=`${x(e.longitude)}${e.house?` · H${e.house}`:``}${e.retrograde?` ℞`:``}`;y.addEventListener(`mouseenter`,n=>O(n.clientX,n.clientY,`${t} ${S(e.name)} (inner)`,b,`Click to explore`)),y.addEventListener(`mousemove`,n=>O(n.clientX,n.clientY,`${t} ${S(e.name)} (inner)`,b,`Click to explore`)),y.addEventListener(`mouseleave`,k),y.addEventListener(`click`,()=>{k(),R(e.name,`inner`),L(e,`inner`,A,C),g(e,`inner`)}),z.appendChild(y)}j&&document.removeEventListener(`click`,j,!1),j=e=>{let t=e.target;!t.closest(`.hit-region`)&&!t.closest(`#bi-info-panel`)&&(B(),I())},document.addEventListener(`click`,j,!1);let Q=e=>{e.key===`Escape`&&(B(),I())};document.removeEventListener(`keydown`,window.__biWheelEscHandler,!1),window.__biWheelEscHandler=Q,document.addEventListener(`keydown`,Q,!1)}function re(e){let t=parseInt(e.slice(1,3),16),n=parseInt(e.slice(3,5),16),r=parseInt(e.slice(5,7),16),i=.6;return`rgb(${Math.round(t*(1-i)+208*i)},${Math.round(n*(1-i)+144*i)},${Math.round(r*(1-i)+32*i)})`}var H=null,U=!1,W=document.getElementById(`chart-container`),G=document.getElementById(`loading-state`),K=document.getElementById(`mode-badge`);function q(){G.hidden=!1,W.hidden=!0,K.hidden=!0}function J(){G.hidden=!0,W.hidden=!1,K.hidden=!1}function Y(e){G.hidden=!0,W.innerHTML=`
|
|
67
|
+
<div class="error-state">
|
|
68
|
+
<div class="error-icon">⚠️</div>
|
|
69
|
+
<p class="error-msg">${e}</p>
|
|
70
|
+
</div>`,W.hidden=!1}function X(e){for(let t of e.content??[]){if(t.type!==`text`||!t.text)continue;let e=t.text.trim();if(e.startsWith(`{`))try{let t=JSON.parse(e);if(`inner`in t&&`outer`in t)return t}catch{}}return null}function Z(e){let t=e.mode??`synastry`,n=e._birth_params_inner?.location??`Person 1`,r=e._birth_params_outer?.location??(t===`transit`?`Transits`:`Person 2`),i=t===`synastry`?`${n}`:`Natal · ${n}`,a=t===`synastry`?`${r}`:`Transit · ${r}`;K.innerHTML=`
|
|
71
|
+
<span class="mode-badge-inner">
|
|
72
|
+
<span class="mode-badge-dot-inner"></span>
|
|
73
|
+
${Q(i)}
|
|
74
|
+
</span>
|
|
75
|
+
<span class="mode-badge-sep">·</span>
|
|
76
|
+
<span class="mode-badge-outer">
|
|
77
|
+
<span class="mode-badge-dot-outer"></span>
|
|
78
|
+
${Q(a)}
|
|
79
|
+
</span>`}function Q(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`)}function $(e){H=e,Z(e),V(W,e,ie,ae),J()}function ie(e,t){if(U||!window.mcpBridge)return;let n=Math.floor((e.longitude%360+360)%360/30),r=e.sign??[`Aries`,`Taurus`,`Gemini`,`Cancer`,`Leo`,`Virgo`,`Libra`,`Scorpio`,`Sagittarius`,`Capricorn`,`Aquarius`,`Pisces`][n]??`Unknown`;window.mcpBridge.callServerTool(`bi_wheel_on_planet_click`,{planet:e.name,wheel:t,longitude:e.longitude,sign:r,house:e.house??void 0,retrograde:e.retrograde??!1,mode:H?.mode??`synastry`},e=>{})}function ae(e){U||!window.mcpBridge||window.mcpBridge.callServerTool(`bi_wheel_on_cross_aspect_click`,{planet1:e.planet1,wheel1:e.wheel1,planet2:e.planet2,wheel2:e.wheel2,aspect_type:e.type,orb:e.orb,applying:e.applying,mode:H?.mode??`synastry`},e=>{})}function oe(){if(!window.mcpBridge){$(se());return}q(),U=!0,window.mcpBridge.onToolResult(e=>{U=!1;let t=X(e);if(!t){Y(`Ask your AI assistant to generate a bi-wheel chart to get started.`);return}$(t)})}function se(){return{mode:`synastry`,inner:[{name:`sun`,longitude:25.4,house:1,retrograde:!1},{name:`moon`,longitude:110.8,house:4,retrograde:!1},{name:`mercury`,longitude:345.2,house:12,retrograde:!1},{name:`venus`,longitude:58.7,house:2,retrograde:!1},{name:`mars`,longitude:220.1,house:8,retrograde:!1},{name:`jupiter`,longitude:80.6,house:3,retrograde:!1},{name:`saturn`,longitude:175.3,house:7,retrograde:!0},{name:`asc`,longitude:0,house:1,retrograde:!1},{name:`mc`,longitude:270,house:10,retrograde:!1}],outer:[{name:`sun`,longitude:67.1,house:2,retrograde:!1},{name:`moon`,longitude:202.4,house:8,retrograde:!1},{name:`mercury`,longitude:52.8,house:2,retrograde:!0},{name:`venus`,longitude:88.3,house:3,retrograde:!1},{name:`mars`,longitude:301.7,house:11,retrograde:!1},{name:`jupiter`,longitude:256.9,house:9,retrograde:!1},{name:`saturn`,longitude:355.2,house:12,retrograde:!1}],inner_houses:Array.from({length:12},(e,t)=>({number:t+1,cusp_lon:t*30,sign:[`Aries`,`Taurus`,`Gemini`,`Cancer`,`Leo`,`Virgo`,`Libra`,`Scorpio`,`Sagittarius`,`Capricorn`,`Aquarius`,`Pisces`][t]})),cross_aspects:[{planet1:`sun`,wheel1:`inner`,planet2:`moon`,wheel2:`outer`,type:`trine`,angle:120,orb:2.5,applying:!0},{planet1:`mars`,wheel1:`inner`,planet2:`sun`,wheel2:`outer`,type:`opposition`,angle:180,orb:1.8,applying:!1},{planet1:`moon`,wheel1:`inner`,planet2:`venus`,wheel2:`outer`,type:`sextile`,angle:60,orb:3.4,applying:!0}],_birth_params_inner:{datetime:`1990-05-10T14:00:00`,location:`New York, NY`,latitude:40.71,longitude:-74.01,timezone:`America/New_York`},_birth_params_outer:{datetime:`1992-08-22T09:30:00`,location:`Los Angeles, CA`,latitude:34.05,longitude:-118.24,timezone:`America/Los_Angeles`}}}window.addEventListener(`DOMContentLoaded`,()=>{setTimeout(oe,80)});</script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|