@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
|
@@ -19,6 +19,7 @@ import path from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
21
21
|
import { getActiveClient } from "../../backend/client.js";
|
|
22
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
22
23
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
23
24
|
export const BODYGRAPH_RESOURCE_URI = "ui://openephemeris/bodygraph";
|
|
24
25
|
export const BODYGRAPH_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -65,6 +66,40 @@ function ensureTimezone(dt) {
|
|
|
65
66
|
return dt;
|
|
66
67
|
return dt + "Z";
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Fetch the canonical bodygraph SVG from the Go renderer.
|
|
71
|
+
*
|
|
72
|
+
* The Go engine is the source of truth for bodygraph visuals (Phase 4 of the
|
|
73
|
+
* renderer unification). This SVG is inlined directly into the iframe and
|
|
74
|
+
* bound to interaction handlers via its data-* attributes (data-center,
|
|
75
|
+
* data-gate, data-channel, data-hanging, data-highlighted, etc.).
|
|
76
|
+
*
|
|
77
|
+
* Returns null on failure so the caller can fall back to the legacy client-side
|
|
78
|
+
* renderer in bodygraph-renderer.ts. Non-fatal: a missing SVG should never
|
|
79
|
+
* block the tool result.
|
|
80
|
+
*/
|
|
81
|
+
async function fetchGoBodygraphSVG(body) {
|
|
82
|
+
try {
|
|
83
|
+
const client = getActiveClient();
|
|
84
|
+
// /visualization/bodygraph is registered as binary in BINARY_ENDPOINT_PREFIXES.
|
|
85
|
+
// The client wraps it as { content_type, encoding: "base64", data_base64 } —
|
|
86
|
+
// we decode back to UTF-8 SVG text for inlining.
|
|
87
|
+
const resp = await client.post("/visualization/bodygraph?format=svg&size=800", body);
|
|
88
|
+
if (typeof resp === "string")
|
|
89
|
+
return resp;
|
|
90
|
+
if (resp && typeof resp === "object") {
|
|
91
|
+
const r = resp;
|
|
92
|
+
if (typeof r.data_base64 === "string") {
|
|
93
|
+
return Buffer.from(r.data_base64, "base64").toString("utf-8");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn("[bodygraph] Go SVG fetch failed, will fall back to client render:", err);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
68
103
|
/**
|
|
69
104
|
* Convert a local datetime string (no offset) to a UTC ISO 8601 string.
|
|
70
105
|
* Uses the IANA timezone to compute the correct UTC offset.
|
|
@@ -234,9 +269,44 @@ function buildHdModelPayload(data, birthParams) {
|
|
|
234
269
|
const dGateList = Array.isArray(rawD) ? rawD : Object.values(rawD);
|
|
235
270
|
const personality_gates = [...new Set(extractGateNums(pGateList))].sort((a, b) => a - b);
|
|
236
271
|
const design_gates = [...new Set(extractGateNums(dGateList))].sort((a, b) => a - b);
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
// Convert activation arrays or maps → planet-keyed Record<string, ActivationSummary>
|
|
273
|
+
// The Go API returns arrays like [{ planet: "Sun", gate: 62, line: 1, color: 3, tone: 4 }].
|
|
274
|
+
// The renderer needs a map keyed by planet name: { "Sun": { gate, line, color, tone } }.
|
|
275
|
+
function toActivationMap(raw) {
|
|
276
|
+
if (!raw || (typeof raw !== 'object'))
|
|
277
|
+
return undefined;
|
|
278
|
+
// Already a planet-keyed map (non-array object)
|
|
279
|
+
if (!Array.isArray(raw)) {
|
|
280
|
+
const obj = raw;
|
|
281
|
+
// Make sure it looks like planet-keyed (values have 'gate' property)
|
|
282
|
+
const first = Object.values(obj)[0];
|
|
283
|
+
if (first && typeof first === 'object' && 'gate' in first) {
|
|
284
|
+
return obj;
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
// Array of activation objects — key by the 'planet' field
|
|
289
|
+
const arr = raw;
|
|
290
|
+
if (arr.length === 0)
|
|
291
|
+
return undefined;
|
|
292
|
+
const map = {};
|
|
293
|
+
for (const item of arr) {
|
|
294
|
+
if (!item || typeof item !== 'object')
|
|
295
|
+
continue;
|
|
296
|
+
const planetName = String(item.planet ?? item.name ?? '');
|
|
297
|
+
if (!planetName)
|
|
298
|
+
continue;
|
|
299
|
+
map[planetName] = {
|
|
300
|
+
gate: Number(item.gate ?? 0),
|
|
301
|
+
line: Number(item.line ?? 0),
|
|
302
|
+
color: Number(item.color ?? 0),
|
|
303
|
+
tone: Number(item.tone ?? 0),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return Object.keys(map).length > 0 ? map : undefined;
|
|
307
|
+
}
|
|
308
|
+
const personality_activations = toActivationMap(rawP);
|
|
309
|
+
const design_activations = toActivationMap(rawD);
|
|
240
310
|
return {
|
|
241
311
|
type,
|
|
242
312
|
profile,
|
|
@@ -314,6 +384,7 @@ registerTool({
|
|
|
314
384
|
},
|
|
315
385
|
required: ["datetime"],
|
|
316
386
|
},
|
|
387
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
317
388
|
annotations: {
|
|
318
389
|
title: "Interactive Human Design Bodygraph Explorer",
|
|
319
390
|
readOnlyHint: true,
|
|
@@ -341,9 +412,18 @@ registerTool({
|
|
|
341
412
|
body.latitude = lat;
|
|
342
413
|
if (lon != null)
|
|
343
414
|
body.longitude = lon;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
415
|
+
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
416
|
+
// Fetch chart data and Go-rendered SVG in parallel. SVG only fetched when
|
|
417
|
+
// the iframe bundle is available (text-fallback hosts have no use for it,
|
|
418
|
+
// and the visualization endpoint costs a credit reservation).
|
|
419
|
+
const [chartData, svg] = await Promise.all([
|
|
420
|
+
client.request("POST", "/human-design/chart", {
|
|
421
|
+
data: body,
|
|
422
|
+
}),
|
|
423
|
+
bundleAvailable
|
|
424
|
+
? fetchGoBodygraphSVG(body)
|
|
425
|
+
: Promise.resolve(null),
|
|
426
|
+
]);
|
|
347
427
|
// API returns { chart: {...}, metadata: {...} } — unwrap the inner chart object
|
|
348
428
|
const chart = chartData.chart ?? chartData;
|
|
349
429
|
// Build payload once — summary and model data both derive from the same object
|
|
@@ -354,8 +434,9 @@ registerTool({
|
|
|
354
434
|
latitude: lat ?? null,
|
|
355
435
|
longitude: lon ?? null,
|
|
356
436
|
});
|
|
437
|
+
if (svg)
|
|
438
|
+
modelPayload._svg = svg;
|
|
357
439
|
const summary = buildHdSummary(modelPayload, location);
|
|
358
|
-
const bundleAvailable = Boolean(getBodygraphBundle());
|
|
359
440
|
if (bundleAvailable) {
|
|
360
441
|
return {
|
|
361
442
|
content: [
|
|
@@ -390,6 +471,7 @@ registerTool({
|
|
|
390
471
|
},
|
|
391
472
|
required: ["datetime"],
|
|
392
473
|
},
|
|
474
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
393
475
|
annotations: { title: "Recalculate Bodygraph", readOnlyHint: true, openWorldHint: false },
|
|
394
476
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
395
477
|
handler: async (args) => {
|
|
@@ -406,9 +488,13 @@ registerTool({
|
|
|
406
488
|
body.latitude = lat;
|
|
407
489
|
if (lon != null)
|
|
408
490
|
body.longitude = lon;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
491
|
+
// Recalculate is iframe-only (visibility: ["app"]) — always co-fetch the SVG.
|
|
492
|
+
const [chartData, svg] = await Promise.all([
|
|
493
|
+
client.request("POST", "/human-design/chart", {
|
|
494
|
+
data: body,
|
|
495
|
+
}),
|
|
496
|
+
fetchGoBodygraphSVG(body),
|
|
497
|
+
]);
|
|
412
498
|
const chart = chartData.chart ?? chartData;
|
|
413
499
|
const modelPayload = buildHdModelPayload(chart, {
|
|
414
500
|
datetime,
|
|
@@ -417,6 +503,8 @@ registerTool({
|
|
|
417
503
|
latitude: lat ?? null,
|
|
418
504
|
longitude: lon ?? null,
|
|
419
505
|
});
|
|
506
|
+
if (svg)
|
|
507
|
+
modelPayload._svg = svg;
|
|
420
508
|
return {
|
|
421
509
|
content: [{ type: "text", text: JSON.stringify({ ...modelPayload, server_version: SERVER_VERSION }) }],
|
|
422
510
|
};
|
|
@@ -443,6 +531,7 @@ registerTool({
|
|
|
443
531
|
},
|
|
444
532
|
required: ["center", "defined"],
|
|
445
533
|
},
|
|
534
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
446
535
|
annotations: { title: "Center Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
447
536
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
448
537
|
handler: async (args) => {
|
|
@@ -500,6 +589,7 @@ registerTool({
|
|
|
500
589
|
},
|
|
501
590
|
required: ["gate"],
|
|
502
591
|
},
|
|
592
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
503
593
|
annotations: { title: "Gate Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
504
594
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
505
595
|
handler: async (args) => {
|
|
@@ -554,6 +644,7 @@ registerTool({
|
|
|
554
644
|
},
|
|
555
645
|
required: ["channel"],
|
|
556
646
|
},
|
|
647
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
557
648
|
annotations: { title: "Channel Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
558
649
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
559
650
|
handler: async (args) => {
|
|
@@ -699,7 +790,7 @@ const HD_GATE_DATA = {
|
|
|
699
790
|
38: { name: "The Fighter", hexagram: "Hex 38 — Opposition", keynote: "Struggle in the search for purpose", gift: "Perseverance — fighting for what is meaningful", shadow: "Struggle — fighting against everything without purpose", circuit: "Individual (Knowing)" },
|
|
700
791
|
39: { name: "Provocation", hexagram: "Hex 39 — Obstruction", keynote: "Freeing the spirit through provocation", gift: "Dynamism — provocateur that liberates", shadow: "Provocation — stirring trouble unconsciously", circuit: "Individual (Knowing)" },
|
|
701
792
|
40: { name: "Aloneness", hexagram: "Hex 40 — Deliverance", keynote: "The demand for aloneness as restoration", gift: "Resolve — willpower renewed in solitude", shadow: "Exhaustion — depleted by giving without replenishment", circuit: "Tribal (Ego)" },
|
|
702
|
-
41: { name: "Contraction", hexagram: "Hex 41 — Decrease", keynote: "The beginning of all experience — fantasy and desire", gift: "Fantasy — imagination that seeds new experience", shadow: "
|
|
793
|
+
41: { name: "Contraction", hexagram: "Hex 41 — Decrease", keynote: "The beginning of all experience — fantasy and desire", gift: "Fantasy — imagination that seeds new experience", shadow: "Escapism — escapism", circuit: "Collective (Abstract)" },
|
|
703
794
|
42: { name: "Growth", hexagram: "Hex 42 — Increase", keynote: "The ability to perpetuate and complete growth cycles", gift: "Detachment — completing cycles with grace", shadow: "Incompletion — abandoning cycles before they end", circuit: "Collective (Abstract)" },
|
|
704
795
|
43: { name: "Insight", hexagram: "Hex 43 — Breakthrough", keynote: "Breakthrough from within", gift: "Insight — inner knowing that arrives as genius", shadow: "Deafness — genius unrecognised or unheard", circuit: "Individual (Knowing)" },
|
|
705
796
|
44: { name: "Alertness", hexagram: "Hex 44 — Coming to Meet", keynote: "Instinctive memory and pattern recognition", gift: "Teamwork — bringing the right people together", shadow: "Interference — driven by karmic patterns from the past", circuit: "Tribal (Defence)" },
|
|
@@ -813,8 +904,8 @@ const HD_PLANET_HD_THEMES = {
|
|
|
813
904
|
"to act before the mind has a chance to deliberate. This energy can surprise even yourself.",
|
|
814
905
|
},
|
|
815
906
|
Jupiter: {
|
|
816
|
-
conscious: "**Jupiter** represents the law of your Personality — the karmic gifts and the
|
|
817
|
-
"higher wisdom
|
|
907
|
+
conscious: "**Jupiter** represents the law of your Personality — the karmic gifts and the principled framework through which you expand beyond " +
|
|
908
|
+
"higher wisdom and justice flow. It shows where you move beyond the self-limiting " +
|
|
818
909
|
"patterns of your conditioning.",
|
|
819
910
|
unconscious: "**Design Jupiter** shows the unconscious grace embedded in your design — the karmic correction " +
|
|
820
911
|
"your body naturally applies when you are living true to your nature.",
|
|
@@ -897,6 +988,7 @@ registerTool({
|
|
|
897
988
|
},
|
|
898
989
|
required: ["planet", "column"],
|
|
899
990
|
},
|
|
991
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
900
992
|
annotations: { title: "Planet Sidebar Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
901
993
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
902
994
|
handler: async (args) => {
|
|
@@ -965,6 +1057,7 @@ registerTool({
|
|
|
965
1057
|
},
|
|
966
1058
|
required: ["variable", "direction"],
|
|
967
1059
|
},
|
|
1060
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
968
1061
|
annotations: { title: "Variable Arrow Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
969
1062
|
_meta: { ui: { resourceUri: BODYGRAPH_RESOURCE_URI, visibility: ["app"] } },
|
|
970
1063
|
handler: async (args) => {
|
|
@@ -20,6 +20,7 @@ import path from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { registerTool, SERVER_VERSION } from "../index.js";
|
|
22
22
|
import { getActiveClient } from "../../backend/client.js";
|
|
23
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
23
24
|
// ── Constants ─────────────────────────────────────────────────────────────
|
|
24
25
|
export const CHART_WHEEL_RESOURCE_URI = "ui://openephemeris/chart-wheel";
|
|
25
26
|
export const CHART_WHEEL_MIME_TYPE = "text/html;profile=mcp-app";
|
|
@@ -139,9 +140,18 @@ registerTool({
|
|
|
139
140
|
enum: ["placidus", "whole_sign", "equal", "koch", "regiomontanus", "campanus"],
|
|
140
141
|
description: "House system to use (default: placidus).",
|
|
141
142
|
},
|
|
143
|
+
bodies: {
|
|
144
|
+
type: "array",
|
|
145
|
+
items: { type: "string" },
|
|
146
|
+
description: "Optional list of body names to include. " +
|
|
147
|
+
"Defaults to 13 classical bodies (Sun through Pluto + Chiron + Nodes). " +
|
|
148
|
+
"Use 'all' as a single item to include every available body (Lilith, Ceres, Juno, Vesta, Pallas, Vertex, etc.). " +
|
|
149
|
+
"Example: ['sun','moon','lilith','vertex'].",
|
|
150
|
+
},
|
|
142
151
|
},
|
|
143
152
|
required: ["datetime", "latitude", "longitude"],
|
|
144
153
|
},
|
|
154
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
145
155
|
annotations: {
|
|
146
156
|
title: "Interactive Chart Wheel Explorer",
|
|
147
157
|
readOnlyHint: true,
|
|
@@ -162,6 +172,17 @@ registerTool({
|
|
|
162
172
|
const lat = args.latitude;
|
|
163
173
|
const lon = args.longitude;
|
|
164
174
|
const natalBody = buildNatalBody(datetime, lat, lon, houseSystem, args.timezone);
|
|
175
|
+
// Determine which bodies are requested
|
|
176
|
+
const requestedBodies = args.bodies;
|
|
177
|
+
const wantsAll = requestedBodies?.some(b => b.toLowerCase() === "all");
|
|
178
|
+
const wantsExtended = wantsAll || requestedBodies?.some(b => EXTENDED_BODIES.has(b.toLowerCase()));
|
|
179
|
+
// Enable extended body computation on the API side when needed
|
|
180
|
+
if (wantsExtended) {
|
|
181
|
+
const config = natalBody.configuration;
|
|
182
|
+
config.include_asteroids = true;
|
|
183
|
+
config.include_lilith = true;
|
|
184
|
+
config.include_chiron = true;
|
|
185
|
+
}
|
|
165
186
|
// Fetch natal chart JSON. The chart is rendered client-side in the UI iframe,
|
|
166
187
|
// so we do NOT call the /visualization/chart-wheel endpoint — that was the
|
|
167
188
|
// source of the ~90 second blocking delay.
|
|
@@ -175,7 +196,7 @@ registerTool({
|
|
|
175
196
|
latitude: lat ?? null,
|
|
176
197
|
longitude: lon ?? null,
|
|
177
198
|
location: args.location ?? null,
|
|
178
|
-
}, houseSystem);
|
|
199
|
+
}, houseSystem, undefined, requestedBodies);
|
|
179
200
|
const bundleAvailable = Boolean(getChartWheelBundle());
|
|
180
201
|
if (bundleAvailable) {
|
|
181
202
|
return {
|
|
@@ -216,6 +237,7 @@ registerTool({
|
|
|
216
237
|
},
|
|
217
238
|
required: ["planet", "longitude"],
|
|
218
239
|
},
|
|
240
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
219
241
|
annotations: { title: "Planet Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
220
242
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
221
243
|
handler: async (args) => {
|
|
@@ -250,6 +272,7 @@ registerTool({
|
|
|
250
272
|
},
|
|
251
273
|
required: ["house_number"],
|
|
252
274
|
},
|
|
275
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
253
276
|
annotations: { title: "House Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
254
277
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
255
278
|
handler: async (args) => {
|
|
@@ -285,6 +308,7 @@ registerTool({
|
|
|
285
308
|
},
|
|
286
309
|
required: ["planet1", "planet2"],
|
|
287
310
|
},
|
|
311
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
288
312
|
annotations: { title: "Aspect Interpretation", readOnlyHint: true, openWorldHint: false },
|
|
289
313
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
290
314
|
handler: async (args) => {
|
|
@@ -333,6 +357,7 @@ registerTool({
|
|
|
333
357
|
},
|
|
334
358
|
required: ["datetime", "latitude", "longitude", "house_system"],
|
|
335
359
|
},
|
|
360
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
336
361
|
annotations: { title: "Recalculate Chart", readOnlyHint: true, openWorldHint: false },
|
|
337
362
|
_meta: { ui: { resourceUri: CHART_WHEEL_RESOURCE_URI, visibility: ["app"] } },
|
|
338
363
|
handler: async (args) => {
|
|
@@ -391,6 +416,13 @@ const CLASSICAL_PLANETS = new Set([
|
|
|
391
416
|
"jupiter", "saturn", "uranus", "neptune", "pluto",
|
|
392
417
|
"chiron", "north_node", "south_node", "true_node", "asc", "mc",
|
|
393
418
|
]);
|
|
419
|
+
const EXTENDED_BODIES = new Set([
|
|
420
|
+
"mean_lilith", "lilith", "true_lilith",
|
|
421
|
+
"ceres", "juno", "vesta", "pallas", "pholus",
|
|
422
|
+
"vertex", "part_of_fortune",
|
|
423
|
+
"eris", "sedna",
|
|
424
|
+
]);
|
|
425
|
+
const ALL_KNOWN_BODIES = new Set([...CLASSICAL_PLANETS, ...EXTENDED_BODIES]);
|
|
394
426
|
/**
|
|
395
427
|
* Build a lean, SVG-free payload for Claude's text context.
|
|
396
428
|
* Strips SVG, filters to classical planets only, and packages birth params.
|
|
@@ -422,7 +454,13 @@ function computeAspects(planets) {
|
|
|
422
454
|
diff = 360 - diff;
|
|
423
455
|
for (const asp of ASPECTS) {
|
|
424
456
|
const orb = Math.abs(diff - asp.angle);
|
|
425
|
-
|
|
457
|
+
// Tighten orbs when one or both bodies are extended (asteroids, etc.)
|
|
458
|
+
const p1Extended = EXTENDED_BODIES.has(p1.name.toLowerCase());
|
|
459
|
+
const p2Extended = EXTENDED_BODIES.has(p2.name.toLowerCase());
|
|
460
|
+
const effectiveOrb = (p1Extended || p2Extended)
|
|
461
|
+
? Math.min(asp.orb * 0.5, 3) // half standard, capped at 3°
|
|
462
|
+
: asp.orb;
|
|
463
|
+
if (orb <= effectiveOrb) {
|
|
426
464
|
// Applying: combined longitudinal speed closing the orb
|
|
427
465
|
const applying = ((p1.speed ?? 1) - (p2.speed ?? 0)) < 0 ? diff < asp.angle : diff > asp.angle;
|
|
428
466
|
results.push({
|
|
@@ -440,7 +478,7 @@ function computeAspects(planets) {
|
|
|
440
478
|
}
|
|
441
479
|
return results;
|
|
442
480
|
}
|
|
443
|
-
function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
481
|
+
function buildModelPayload(chartData, birthParams, houseSystem, svgBase, bodyFilter) {
|
|
444
482
|
// ── 1. Normalize planets: keyed object → typed PlanetData array ──────────
|
|
445
483
|
// The natal API returns { sun: { longitude, sign_name, is_retrograde, ... }, moon: {...} }.
|
|
446
484
|
// The chart wheel UI expects PlanetData[] with canonical field names.
|
|
@@ -448,7 +486,18 @@ function buildModelPayload(chartData, birthParams, houseSystem, svgBase) {
|
|
|
448
486
|
let planetsArray = [];
|
|
449
487
|
if (rawPlanets && typeof rawPlanets === "object" && !Array.isArray(rawPlanets)) {
|
|
450
488
|
planetsArray = Object.entries(rawPlanets)
|
|
451
|
-
.filter(([k]) =>
|
|
489
|
+
.filter(([k]) => {
|
|
490
|
+
const key = k.toLowerCase();
|
|
491
|
+
// If specific bodies were requested, use that filter
|
|
492
|
+
if (bodyFilter) {
|
|
493
|
+
const wantsAll = bodyFilter.some(b => b.toLowerCase() === "all");
|
|
494
|
+
if (wantsAll)
|
|
495
|
+
return ALL_KNOWN_BODIES.has(key);
|
|
496
|
+
return bodyFilter.some(b => b.toLowerCase() === key);
|
|
497
|
+
}
|
|
498
|
+
// Default: classical planets only
|
|
499
|
+
return CLASSICAL_PLANETS.has(key);
|
|
500
|
+
})
|
|
452
501
|
.map(([name, p]) => ({
|
|
453
502
|
name: name.toLowerCase(),
|
|
454
503
|
longitude: (p.longitude ?? p.lon ?? 0),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerTool, validateRequired } from "../index.js";
|
|
2
2
|
import { getActiveClient } from "../../backend/client.js";
|
|
3
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
3
4
|
registerTool({
|
|
4
5
|
name: "location_search",
|
|
5
6
|
description: "App-only tool for getting location autocomplete suggestions. Returns latitude, longitude, and IANA timezone.",
|
|
@@ -14,6 +15,7 @@ registerTool({
|
|
|
14
15
|
required: ["query"],
|
|
15
16
|
additionalProperties: false,
|
|
16
17
|
},
|
|
18
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
17
19
|
_meta: {
|
|
18
20
|
ui: {
|
|
19
21
|
visibility: ["app"],
|
|
@@ -38,6 +40,7 @@ registerTool({
|
|
|
38
40
|
required: ["latitude", "longitude"],
|
|
39
41
|
additionalProperties: false,
|
|
40
42
|
},
|
|
43
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
41
44
|
_meta: {
|
|
42
45
|
ui: {
|
|
43
46
|
visibility: ["app"],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moon-phase-app.ts — MCP App tool registration for the Moon Phase Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_moon_phase — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The dial is rendered client-side from structured JSON. The tool calls the
|
|
8
|
+
* existing moon phase and VOC endpoints, merges the results, and returns them
|
|
9
|
+
* with a UI resource link so the iframe can render a circular phase dial.
|
|
10
|
+
*
|
|
11
|
+
* Also exports resource helpers (getMoonPhaseBundle, etc.) for use in
|
|
12
|
+
* index.ts and server-sse.ts.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
15
|
+
export declare const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
16
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
17
|
+
export declare function getMoonPhaseBundle(): string | null;
|
|
18
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
19
|
+
export declare function clearMoonPhaseBundleCache(): void;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moon-phase-app.ts — MCP App tool registration for the Moon Phase Explorer.
|
|
3
|
+
*
|
|
4
|
+
* Registers 1 tool:
|
|
5
|
+
* • explore_moon_phase — primary entry point, returns data + UI resource [model]
|
|
6
|
+
*
|
|
7
|
+
* The dial is rendered client-side from structured JSON. The tool calls the
|
|
8
|
+
* existing moon phase and VOC endpoints, merges the results, and returns them
|
|
9
|
+
* with a UI resource link so the iframe can render a circular phase dial.
|
|
10
|
+
*
|
|
11
|
+
* Also exports resource helpers (getMoonPhaseBundle, etc.) for use in
|
|
12
|
+
* index.ts and server-sse.ts.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { registerTool, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
18
|
+
import { getActiveClient } from "../../backend/client.js";
|
|
19
|
+
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
20
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
|
+
export const MOON_PHASE_RESOURCE_URI = "ui://openephemeris/moon-phase";
|
|
22
|
+
export const MOON_PHASE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
23
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const BUNDLE_PATHS = [
|
|
25
|
+
path.resolve(here, "..", "..", "..", "dist", "ui", "moon-phase.html"),
|
|
26
|
+
];
|
|
27
|
+
function findBundlePath() {
|
|
28
|
+
for (const p of BUNDLE_PATHS) {
|
|
29
|
+
if (fs.existsSync(p))
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
let cachedBundle = null;
|
|
35
|
+
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
36
|
+
export function getMoonPhaseBundle() {
|
|
37
|
+
if (cachedBundle)
|
|
38
|
+
return cachedBundle;
|
|
39
|
+
const bundlePath = findBundlePath();
|
|
40
|
+
if (!bundlePath)
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
cachedBundle = fs.readFileSync(bundlePath, "utf-8");
|
|
44
|
+
return cachedBundle;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Reset cache (useful in dev watch mode or tests). */
|
|
51
|
+
export function clearMoonPhaseBundleCache() {
|
|
52
|
+
cachedBundle = null;
|
|
53
|
+
}
|
|
54
|
+
// ── Summary builder ──────────────────────────────────────────────────────────
|
|
55
|
+
function buildMoonSummary(data) {
|
|
56
|
+
const phase = data.phase;
|
|
57
|
+
const voc = data.void_of_course;
|
|
58
|
+
const meta = (phase?.metadata ?? {});
|
|
59
|
+
const phaseName = String(phase?.phase_name ?? phase?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
|
60
|
+
const illumination = typeof phase?.illumination === "number" ? Math.round(phase.illumination) : "?";
|
|
61
|
+
const moonSign = String(meta?.moon_sign ?? phase?.moon_sign ?? phase?.sign ?? voc?.current_sign ?? "");
|
|
62
|
+
const isVoc = Boolean(voc?.is_void ?? voc?.is_void_of_course ?? false);
|
|
63
|
+
const speed = typeof meta?.longitude_speed === "number" ? meta.longitude_speed : null;
|
|
64
|
+
const daysToFull = typeof meta?.days_to_full === "number" ? meta.days_to_full :
|
|
65
|
+
typeof phase?.days_to_full === "number" ? phase.days_to_full : null;
|
|
66
|
+
let summary = "**Moon Phase** — " + phaseName + "\n\n" +
|
|
67
|
+
"Illumination: **" + illumination + "%**\n";
|
|
68
|
+
if (moonSign) {
|
|
69
|
+
summary += "Moon Sign: **" + moonSign + "**\n";
|
|
70
|
+
}
|
|
71
|
+
if (speed !== null) {
|
|
72
|
+
const tempo = speed > 14.5 ? "fast" : speed < 12.0 ? "slow" : "average";
|
|
73
|
+
summary += "Speed: **" + speed.toFixed(2) + "°/day** (" + tempo + ")\n";
|
|
74
|
+
}
|
|
75
|
+
if (daysToFull !== null) {
|
|
76
|
+
summary += "Full Moon in: **" + daysToFull.toFixed(1) + " days**\n";
|
|
77
|
+
}
|
|
78
|
+
if (isVoc) {
|
|
79
|
+
summary += "\n⚠️ **Void of Course** — The Moon is currently VOC.";
|
|
80
|
+
if (voc?.ends)
|
|
81
|
+
summary += " Ends at " + String(voc.ends) + ".";
|
|
82
|
+
if (voc?.next_sign)
|
|
83
|
+
summary += " Next sign: " + String(voc.next_sign) + ".";
|
|
84
|
+
summary += "\n";
|
|
85
|
+
}
|
|
86
|
+
summary += "\nClick cards on the dial for details and interpretation.";
|
|
87
|
+
return summary;
|
|
88
|
+
}
|
|
89
|
+
// ── Tool: explore_moon_phase ─────────────────────────────────────────────────
|
|
90
|
+
registerTool({
|
|
91
|
+
name: "explore_moon_phase",
|
|
92
|
+
description: "Generate an interactive Moon Phase dial showing the current lunar illumination, " +
|
|
93
|
+
"phase name, zodiac sign, and void-of-course status as a beautiful circular visualization.\n\n" +
|
|
94
|
+
"Returns a visual dial with:\n" +
|
|
95
|
+
" • SVG crescent Moon showing real-time illumination percentage\n" +
|
|
96
|
+
" • Phase name and waxing/waning indicator\n" +
|
|
97
|
+
" • Current Moon sign with degree\n" +
|
|
98
|
+
" • Void-of-Course status with timing details\n" +
|
|
99
|
+
" • Lunar age (days in the synodic cycle)\n" +
|
|
100
|
+
" • Upcoming New Moon and Full Moon dates\n\n" +
|
|
101
|
+
"Use this for a rich, interactive lunar phase experience in MCP Apps-capable hosts (Claude Desktop). " +
|
|
102
|
+
"Falls back to a text summary in other hosts.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
datetime: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "ISO 8601 datetime to query. If omitted, returns the current live moon phase (UTC now).",
|
|
109
|
+
},
|
|
110
|
+
latitude: {
|
|
111
|
+
type: "number",
|
|
112
|
+
description: "Observer latitude (optional, used for local void-of-course calculations).",
|
|
113
|
+
},
|
|
114
|
+
longitude: {
|
|
115
|
+
type: "number",
|
|
116
|
+
description: "Observer longitude (optional, used for local void-of-course calculations).",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: [],
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
},
|
|
122
|
+
outputSchema: OUTPUT_SCHEMA_JSON,
|
|
123
|
+
annotations: {
|
|
124
|
+
title: "Interactive Moon Phase Explorer",
|
|
125
|
+
readOnlyHint: true,
|
|
126
|
+
destructiveHint: false,
|
|
127
|
+
idempotentHint: true,
|
|
128
|
+
openWorldHint: false,
|
|
129
|
+
},
|
|
130
|
+
_meta: {
|
|
131
|
+
ui: {
|
|
132
|
+
resourceUri: MOON_PHASE_RESOURCE_URI,
|
|
133
|
+
visibility: ["model", "app"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
handler: async (args) => {
|
|
137
|
+
validateCoordinates(args, "latitude", "longitude");
|
|
138
|
+
const client = getActiveClient();
|
|
139
|
+
const params = {};
|
|
140
|
+
params.datetime = args.datetime ?? new Date().toISOString();
|
|
141
|
+
if (args.latitude != null)
|
|
142
|
+
params.latitude = args.latitude;
|
|
143
|
+
if (args.longitude != null)
|
|
144
|
+
params.longitude = args.longitude;
|
|
145
|
+
// Fetch phase, VOC, and moon aspects in parallel
|
|
146
|
+
const [phase, voc, aspects] = await Promise.allSettled([
|
|
147
|
+
client.request("GET", "/ephemeris/moon/phase", { params }),
|
|
148
|
+
client.request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
149
|
+
client.request("GET", "/ephemeris/moon/aspects", { params }),
|
|
150
|
+
]);
|
|
151
|
+
const mergedData = {
|
|
152
|
+
phase: phase.status === "fulfilled" ? phase.value : { error: phase.reason?.message },
|
|
153
|
+
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
154
|
+
aspects: aspects.status === "fulfilled" ? aspects.value : null,
|
|
155
|
+
};
|
|
156
|
+
// Flatten for the UI — merge phase fields to top level
|
|
157
|
+
const phaseData = mergedData.phase;
|
|
158
|
+
const vocData = mergedData.void_of_course;
|
|
159
|
+
const aspectsData = mergedData.aspects;
|
|
160
|
+
// Extract enriched metadata fields from the GET response
|
|
161
|
+
const meta = (phaseData?.metadata ?? {});
|
|
162
|
+
const moonSign = String(meta?.moon_sign ?? phaseData?.moon_sign ?? phaseData?.sign ?? vocData?.current_sign ?? "");
|
|
163
|
+
const moonSpeedRaw = meta?.longitude_speed;
|
|
164
|
+
const moonSpeed = typeof moonSpeedRaw === "number" ? moonSpeedRaw : null;
|
|
165
|
+
const angularDiameter = typeof meta?.angular_diameter === "number" ? meta.angular_diameter : null;
|
|
166
|
+
const distanceKm = typeof meta?.moon_distance_km === "number" ? meta.moon_distance_km : null;
|
|
167
|
+
const moonLat = typeof meta?.moon_lat === "number" ? meta.moon_lat : null;
|
|
168
|
+
const ageDays = typeof meta?.age_days === "number" ? meta.age_days :
|
|
169
|
+
(typeof phaseData?.age_days === "number" ? phaseData.age_days : null);
|
|
170
|
+
// days_to_new / days_to_full can come as the union value or from the top-level field
|
|
171
|
+
const extractDays = (field) => {
|
|
172
|
+
if (typeof field === "number")
|
|
173
|
+
return field;
|
|
174
|
+
if (field && typeof field === "object") {
|
|
175
|
+
const v = Object.values(field)[0];
|
|
176
|
+
if (typeof v === "number")
|
|
177
|
+
return v;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
const daysToNew = extractDays(phaseData?.days_to_new);
|
|
182
|
+
const daysToFull = extractDays(phaseData?.days_to_full);
|
|
183
|
+
// Top 3 applying aspects by tightest orb
|
|
184
|
+
const rawAspects = (aspectsData?.aspects ?? aspectsData?.data ?? []);
|
|
185
|
+
const applyingAspects = Array.isArray(rawAspects)
|
|
186
|
+
? rawAspects
|
|
187
|
+
.filter((a) => a.applying === true || a.type === "applying")
|
|
188
|
+
.sort((a, b) => Math.abs(a.orb ?? 99) - Math.abs(b.orb ?? 99))
|
|
189
|
+
.slice(0, 3)
|
|
190
|
+
.map((a) => ({
|
|
191
|
+
planet: a.planet ?? a.body ?? a.planet_name ?? "",
|
|
192
|
+
aspect: a.aspect ?? a.aspect_name ?? a.type_name ?? "",
|
|
193
|
+
orb: typeof a.orb === "number" ? Math.abs(a.orb) : null,
|
|
194
|
+
}))
|
|
195
|
+
: [];
|
|
196
|
+
const uiPayload = {
|
|
197
|
+
phase_name: String(phaseData?.phase_name ?? phaseData?.name ?? "Unknown").replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()),
|
|
198
|
+
illumination: typeof phaseData?.illumination === "number" ? phaseData.illumination / 100.0 : 0,
|
|
199
|
+
phase_angle: typeof phaseData?.phase_angle === "number" ? phaseData.phase_angle : null,
|
|
200
|
+
age_days: ageDays,
|
|
201
|
+
days_to_new: daysToNew,
|
|
202
|
+
days_to_full: daysToFull,
|
|
203
|
+
waxing: phaseData?.waxing ??
|
|
204
|
+
// Fallback chain: elongation < 180° = waxing, else age_days < 14.75 = waxing.
|
|
205
|
+
(typeof phaseData?.phase_angle === "number"
|
|
206
|
+
? phaseData.phase_angle < 180
|
|
207
|
+
: typeof ageDays === "number"
|
|
208
|
+
? ageDays < 14.75
|
|
209
|
+
: true), // safe default: assume waxing
|
|
210
|
+
moon_sign: moonSign,
|
|
211
|
+
moon_longitude: meta?.moon_lon ?? phaseData?.moon_longitude ?? phaseData?.longitude ?? null,
|
|
212
|
+
moon_latitude: moonLat,
|
|
213
|
+
moon_distance_km: distanceKm,
|
|
214
|
+
angular_diameter: angularDiameter,
|
|
215
|
+
moon_speed: moonSpeed,
|
|
216
|
+
sun_longitude: meta?.sun_lon ?? phaseData?.sun_longitude ?? null,
|
|
217
|
+
applying_aspects: applyingAspects,
|
|
218
|
+
voc_status: {
|
|
219
|
+
is_void: Boolean(vocData?.is_void ?? vocData?.is_void_of_course ?? false),
|
|
220
|
+
started: vocData?.started ?? vocData?.start ?? null,
|
|
221
|
+
ends: vocData?.ends ?? vocData?.end ?? null,
|
|
222
|
+
next_sign: vocData?.next_sign ?? null,
|
|
223
|
+
},
|
|
224
|
+
datetime: params.datetime,
|
|
225
|
+
server_version: SERVER_VERSION,
|
|
226
|
+
};
|
|
227
|
+
const summary = buildMoonSummary(mergedData);
|
|
228
|
+
const bundleAvailable = Boolean(getMoonPhaseBundle());
|
|
229
|
+
if (bundleAvailable) {
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{ type: "text", text: summary },
|
|
233
|
+
{ type: "text", text: JSON.stringify(uiPayload) },
|
|
234
|
+
],
|
|
235
|
+
_meta: {
|
|
236
|
+
"ui/resourceUri": MOON_PHASE_RESOURCE_URI,
|
|
237
|
+
ui: { resourceUri: MOON_PHASE_RESOURCE_URI },
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Fallback: text summary only
|
|
242
|
+
return { content: [{ type: "text", text: summary }] };
|
|
243
|
+
},
|
|
244
|
+
});
|