@openephemeris/mcp-server 4.17.0 → 4.19.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 +52 -0
- package/README.md +10 -10
- package/dist/backend/client.d.ts +57 -1
- package/dist/backend/client.js +125 -15
- package/dist/index.js +8 -110
- package/dist/prompts.js +26 -24
- package/dist/server-sse.d.ts +18 -0
- package/dist/server-sse.js +48 -120
- package/dist/tools/apps/_render-token.d.ts +34 -0
- package/dist/tools/apps/_render-token.js +50 -0
- package/dist/tools/apps/bazi-app.d.ts +1 -1
- package/dist/tools/apps/bazi-app.js +39 -14
- package/dist/tools/apps/bi-wheel-app.d.ts +10 -3
- package/dist/tools/apps/bi-wheel-app.js +79 -119
- package/dist/tools/apps/bodygraph-app.d.ts +7 -4
- package/dist/tools/apps/bodygraph-app.js +101 -46
- package/dist/tools/apps/chart-wheel-app.d.ts +1 -1
- package/dist/tools/apps/chart-wheel-app.js +59 -18
- package/dist/tools/apps/location-tools.js +10 -3
- package/dist/tools/apps/moon-phase-app.d.ts +1 -1
- package/dist/tools/apps/moon-phase-app.js +13 -4
- package/dist/tools/apps/transit-timeline-app.d.ts +1 -1
- package/dist/tools/apps/transit-timeline-app.js +6 -6
- package/dist/tools/apps/ui-resource.d.ts +43 -0
- package/dist/tools/apps/ui-resource.js +66 -0
- package/dist/tools/apps/ui-resources.d.ts +22 -0
- package/dist/tools/apps/ui-resources.js +105 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +1 -1
- package/dist/tools/apps/vedic-chart-app.js +13 -3
- package/dist/tools/datetime-historical.js +7 -2
- package/dist/tools/datetime.js +2 -1
- package/dist/tools/dev.js +7 -6
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/ephemeris_extended.js +19 -4
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/moon.d.ts +1 -1
- package/dist/tools/specialized/moon.js +51 -43
- package/dist/tools/specialized/progressed.js +2 -25
- package/dist/tools/specialized/transits.js +5 -5
- package/dist/ui/bazi.html +1199 -1161
- package/dist/ui/bi-wheel.html +573 -504
- package/dist/ui/bodygraph.html +437 -337
- package/dist/ui/chart-wheel.html +654 -584
- package/dist/ui/moon-phase.html +190 -155
- package/dist/ui/transit-timeline.html +865 -830
- package/dist/ui/vedic-chart.html +971 -934
- package/package.json +2 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
2
|
+
/**
|
|
3
|
+
* Versioned resource URI: `ui://openephemeris/<slug>?v=<server version>`.
|
|
4
|
+
*
|
|
5
|
+
* ChatGPT caches a `ui://` resource per connector and never re-reads it for a
|
|
6
|
+
* URI it has seen — a deploy alone does not refresh the widget. Stamping the
|
|
7
|
+
* package version into the URI makes every release a new URI, so hosts fetch
|
|
8
|
+
* the new bundle on their own. Caveat: a widget change must ship with a
|
|
9
|
+
* version bump to bust the cache (same version ⇒ same URI).
|
|
10
|
+
*
|
|
11
|
+
* The SAME string is used in the tool `_meta` (ui.resourceUri and its aliases),
|
|
12
|
+
* in resources/list and in resources/read, on both transports.
|
|
13
|
+
*/
|
|
14
|
+
export declare function uiResourceUri(slug: string): string;
|
|
15
|
+
/** The URI without its `?v=` query — what resources/read matches on. */
|
|
16
|
+
export declare function uiResourceBase(uri: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* CSP the bundles actually need: none. Every bundle is a single self-contained
|
|
19
|
+
* HTML file (JS/CSS inlined, images and icons as data: URIs), and the apps talk
|
|
20
|
+
* to the host over postMessage only — no fetch, no remote script, style, image
|
|
21
|
+
* or font. Empty lists are the spec's secure default; declaring them explicitly
|
|
22
|
+
* documents that this is deliberate, not forgotten.
|
|
23
|
+
*/
|
|
24
|
+
export declare const UI_RESOURCE_CSP: {
|
|
25
|
+
connectDomains: string[];
|
|
26
|
+
resourceDomains: string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* `_meta` for a `ui://` resource (resources/list entry and resources/read
|
|
30
|
+
* content item). Two wire forms, same values:
|
|
31
|
+
*
|
|
32
|
+
* ui.* — MCP Apps (SEP-1865) McpUiResourceMeta.
|
|
33
|
+
* openai/* — ChatGPT's aliases for the same fields.
|
|
34
|
+
*
|
|
35
|
+
* prefersBorder: true — the host draws the card border and background; the apps
|
|
36
|
+
* therefore paint no card, border or glow of their own (shared/tokens.css).
|
|
37
|
+
*
|
|
38
|
+
* ui.domain / openai/widgetDomain are deliberately NOT set: their format is
|
|
39
|
+
* host-specific (a hash subdomain on Claude, a derived *.oaiusercontent.com on
|
|
40
|
+
* ChatGPT) and a wrong value can stop the widget rendering. Omitted = the
|
|
41
|
+
* host's default sandbox origin, which is what every live render uses today.
|
|
42
|
+
*/
|
|
43
|
+
export declare function uiResourceMeta(widgetDescription: string): Record<string, unknown>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-resource.ts — identity and `_meta` of the seven MCP App `ui://` resources.
|
|
3
|
+
*
|
|
4
|
+
* Imported by every `*-app.ts` (for its resource URI) and by ui-resources.ts
|
|
5
|
+
* (the list/read registry both transports serve). Kept free of app imports so
|
|
6
|
+
* the app modules can depend on it without a cycle.
|
|
7
|
+
*/
|
|
8
|
+
import { SERVER_VERSION } from "../index.js";
|
|
9
|
+
export const UI_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
10
|
+
/**
|
|
11
|
+
* Versioned resource URI: `ui://openephemeris/<slug>?v=<server version>`.
|
|
12
|
+
*
|
|
13
|
+
* ChatGPT caches a `ui://` resource per connector and never re-reads it for a
|
|
14
|
+
* URI it has seen — a deploy alone does not refresh the widget. Stamping the
|
|
15
|
+
* package version into the URI makes every release a new URI, so hosts fetch
|
|
16
|
+
* the new bundle on their own. Caveat: a widget change must ship with a
|
|
17
|
+
* version bump to bust the cache (same version ⇒ same URI).
|
|
18
|
+
*
|
|
19
|
+
* The SAME string is used in the tool `_meta` (ui.resourceUri and its aliases),
|
|
20
|
+
* in resources/list and in resources/read, on both transports.
|
|
21
|
+
*/
|
|
22
|
+
export function uiResourceUri(slug) {
|
|
23
|
+
return `ui://openephemeris/${slug}?v=${SERVER_VERSION}`;
|
|
24
|
+
}
|
|
25
|
+
/** The URI without its `?v=` query — what resources/read matches on. */
|
|
26
|
+
export function uiResourceBase(uri) {
|
|
27
|
+
const q = uri.indexOf("?");
|
|
28
|
+
return q === -1 ? uri : uri.slice(0, q);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* CSP the bundles actually need: none. Every bundle is a single self-contained
|
|
32
|
+
* HTML file (JS/CSS inlined, images and icons as data: URIs), and the apps talk
|
|
33
|
+
* to the host over postMessage only — no fetch, no remote script, style, image
|
|
34
|
+
* or font. Empty lists are the spec's secure default; declaring them explicitly
|
|
35
|
+
* documents that this is deliberate, not forgotten.
|
|
36
|
+
*/
|
|
37
|
+
export const UI_RESOURCE_CSP = {
|
|
38
|
+
connectDomains: [],
|
|
39
|
+
resourceDomains: [],
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* `_meta` for a `ui://` resource (resources/list entry and resources/read
|
|
43
|
+
* content item). Two wire forms, same values:
|
|
44
|
+
*
|
|
45
|
+
* ui.* — MCP Apps (SEP-1865) McpUiResourceMeta.
|
|
46
|
+
* openai/* — ChatGPT's aliases for the same fields.
|
|
47
|
+
*
|
|
48
|
+
* prefersBorder: true — the host draws the card border and background; the apps
|
|
49
|
+
* therefore paint no card, border or glow of their own (shared/tokens.css).
|
|
50
|
+
*
|
|
51
|
+
* ui.domain / openai/widgetDomain are deliberately NOT set: their format is
|
|
52
|
+
* host-specific (a hash subdomain on Claude, a derived *.oaiusercontent.com on
|
|
53
|
+
* ChatGPT) and a wrong value can stop the widget rendering. Omitted = the
|
|
54
|
+
* host's default sandbox origin, which is what every live render uses today.
|
|
55
|
+
*/
|
|
56
|
+
export function uiResourceMeta(widgetDescription) {
|
|
57
|
+
return {
|
|
58
|
+
ui: {
|
|
59
|
+
prefersBorder: true,
|
|
60
|
+
csp: UI_RESOURCE_CSP,
|
|
61
|
+
},
|
|
62
|
+
"openai/widgetPrefersBorder": true,
|
|
63
|
+
"openai/widgetCSP": { connect_domains: [], resource_domains: [] },
|
|
64
|
+
"openai/widgetDescription": widgetDescription,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface UiResourceEntry {
|
|
2
|
+
uri: string;
|
|
3
|
+
/** Short name used in the "bundle not found" error. */
|
|
4
|
+
label: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
/** Tells the model what the widget already shows (ChatGPT openai/widgetDescription). */
|
|
8
|
+
widgetDescription: string;
|
|
9
|
+
getBundle: () => string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare const UI_RESOURCES: UiResourceEntry[];
|
|
12
|
+
/** resources/list — only bundles that are actually built. */
|
|
13
|
+
export declare function listUiResources(): Array<Record<string, unknown>>;
|
|
14
|
+
/**
|
|
15
|
+
* resources/read. Matches on the URI WITHOUT its `?v=` so a host holding an
|
|
16
|
+
* older (or unversioned) URI from a cached tools/list still gets the current
|
|
17
|
+
* bundle; the content item echoes the URI that was asked for. Returns null for
|
|
18
|
+
* a URI that is not one of ours.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readUiResource(uri: string): {
|
|
21
|
+
contents: Array<Record<string, unknown>>;
|
|
22
|
+
} | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-resources.ts — the resources/list + resources/read registry for the seven
|
|
3
|
+
* MCP App bundles, shared by BOTH transports (src/index.ts stdio and
|
|
4
|
+
* src/server-sse.ts HTTP). They used to carry copy-paste twins of the same
|
|
5
|
+
* seven if-blocks; one table keeps URI, name, description and `_meta` in step.
|
|
6
|
+
*/
|
|
7
|
+
import { UI_RESOURCE_MIME_TYPE, uiResourceBase, uiResourceMeta } from "./ui-resource.js";
|
|
8
|
+
import { CHART_WHEEL_RESOURCE_URI, getChartWheelBundle } from "./chart-wheel-app.js";
|
|
9
|
+
import { BODYGRAPH_RESOURCE_URI, getBodygraphBundle } from "./bodygraph-app.js";
|
|
10
|
+
import { BI_WHEEL_RESOURCE_URI, getBiWheelBundle } from "./bi-wheel-app.js";
|
|
11
|
+
import { MOON_PHASE_RESOURCE_URI, getMoonPhaseBundle } from "./moon-phase-app.js";
|
|
12
|
+
import { TRANSIT_TIMELINE_RESOURCE_URI, getTransitTimelineBundle } from "./transit-timeline-app.js";
|
|
13
|
+
import { VEDIC_CHART_RESOURCE_URI, getVedicChartBundle } from "./vedic-chart-app.js";
|
|
14
|
+
import { BAZI_RESOURCE_URI, getBaziBundle } from "./bazi-app.js";
|
|
15
|
+
export const UI_RESOURCES = [
|
|
16
|
+
{
|
|
17
|
+
uri: CHART_WHEEL_RESOURCE_URI,
|
|
18
|
+
label: "Chart Wheel",
|
|
19
|
+
name: "Chart Wheel Explorer",
|
|
20
|
+
description: "Interactive natal chart wheel with clickable planets, houses, and aspects.",
|
|
21
|
+
widgetDescription: "Shows the full natal chart wheel — every planet, house cusp and aspect line — and lets the user click any of them for a reading. Don't repeat the placements as a list; highlight what stands out and invite them to explore the wheel.",
|
|
22
|
+
getBundle: getChartWheelBundle,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
uri: BODYGRAPH_RESOURCE_URI,
|
|
26
|
+
label: "Bodygraph",
|
|
27
|
+
name: "Human Design Bodygraph Explorer",
|
|
28
|
+
description: "Interactive Human Design Bodygraph with clickable centers, gates, and channels.",
|
|
29
|
+
widgetDescription: "Shows the Human Design bodygraph with type, profile and authority, and lets the user click centers, channels and gates for their meaning. Don't redraw or list the whole chart; interpret the key points and point them to the graph.",
|
|
30
|
+
getBundle: getBodygraphBundle,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
uri: BI_WHEEL_RESOURCE_URI,
|
|
34
|
+
label: "Bi-Wheel",
|
|
35
|
+
name: "Bi-Wheel Explorer",
|
|
36
|
+
description: "Interactive synastry or transit bi-wheel with cross-aspect lines and clickable planets.",
|
|
37
|
+
widgetDescription: "Shows two charts as a bi-wheel (natal inside, transits or a second person outside) with the cross-aspects drawn, each clickable. Don't list every cross-aspect; summarize the strongest contacts.",
|
|
38
|
+
getBundle: getBiWheelBundle,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
uri: MOON_PHASE_RESOURCE_URI,
|
|
42
|
+
label: "Moon Phase",
|
|
43
|
+
name: "Moon Phase Explorer",
|
|
44
|
+
description: "Interactive lunar phase dial showing illumination, sign, and void-of-course status.",
|
|
45
|
+
widgetDescription: "Shows the current moon: phase, illumination, sign, void-of-course status, speed and the next new and full moon. Don't restate those numbers; add meaning or answer the user's question.",
|
|
46
|
+
getBundle: getMoonPhaseBundle,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
50
|
+
label: "Transit Timeline",
|
|
51
|
+
name: "Transit Timeline Explorer",
|
|
52
|
+
description: "Interactive vertical timeline of upcoming transit hits grouped by month, with clickable events.",
|
|
53
|
+
widgetDescription: "Shows a dated timeline of upcoming transits to the natal chart, grouped by month, each clickable. Don't re-list the dates; call out the few that matter most.",
|
|
54
|
+
getBundle: getTransitTimelineBundle,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
uri: VEDIC_CHART_RESOURCE_URI,
|
|
58
|
+
label: "Vedic Chart",
|
|
59
|
+
name: "Vedic Chart Explorer",
|
|
60
|
+
description: "Interactive Vedic (Jyotish) South Indian Rashi grid with clickable sign placements.",
|
|
61
|
+
widgetDescription: "Shows the sidereal South Indian Rashi chart with every graha in its sign, each clickable. Don't re-list placements; interpret the notable ones.",
|
|
62
|
+
getBundle: getVedicChartBundle,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
uri: BAZI_RESOURCE_URI,
|
|
66
|
+
label: "BaZi",
|
|
67
|
+
name: "BaZi Four Pillars Explorer",
|
|
68
|
+
description: "Interactive BaZi (Four Pillars of Destiny) chart with clickable Year/Month/Day/Hour pillars.",
|
|
69
|
+
widgetDescription: "Shows the Four Pillars (year, month, day, hour stems and branches) with the Day Master, each pillar clickable. Don't re-list the pillars; interpret the balance.",
|
|
70
|
+
getBundle: getBaziBundle,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
/** resources/list — only bundles that are actually built. */
|
|
74
|
+
export function listUiResources() {
|
|
75
|
+
return UI_RESOURCES.filter((r) => r.getBundle()).map((r) => ({
|
|
76
|
+
uri: r.uri,
|
|
77
|
+
name: r.name,
|
|
78
|
+
description: r.description,
|
|
79
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
80
|
+
_meta: uiResourceMeta(r.widgetDescription),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* resources/read. Matches on the URI WITHOUT its `?v=` so a host holding an
|
|
85
|
+
* older (or unversioned) URI from a cached tools/list still gets the current
|
|
86
|
+
* bundle; the content item echoes the URI that was asked for. Returns null for
|
|
87
|
+
* a URI that is not one of ours.
|
|
88
|
+
*/
|
|
89
|
+
export function readUiResource(uri) {
|
|
90
|
+
const base = uiResourceBase(uri);
|
|
91
|
+
const entry = UI_RESOURCES.find((r) => uiResourceBase(r.uri) === base);
|
|
92
|
+
if (!entry)
|
|
93
|
+
return null;
|
|
94
|
+
const bundle = entry.getBundle();
|
|
95
|
+
if (!bundle)
|
|
96
|
+
throw new Error(`${entry.label} UI bundle not found. Run \`npm run build:ui\` to build it.`);
|
|
97
|
+
return {
|
|
98
|
+
contents: [{
|
|
99
|
+
uri,
|
|
100
|
+
mimeType: UI_RESOURCE_MIME_TYPE,
|
|
101
|
+
text: bundle,
|
|
102
|
+
_meta: uiResourceMeta(entry.widgetDescription),
|
|
103
|
+
}],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Also exports resource helpers (getVedicChartBundle, etc.) for use in
|
|
18
18
|
* index.ts and server-sse.ts.
|
|
19
19
|
*/
|
|
20
|
-
export declare const VEDIC_CHART_RESOURCE_URI
|
|
20
|
+
export declare const VEDIC_CHART_RESOURCE_URI: string;
|
|
21
21
|
export declare const VEDIC_CHART_MIME_TYPE = "text/html;profile=mcp-app";
|
|
22
22
|
/** Read the pre-built HTML bundle. Returns null if not yet built. */
|
|
23
23
|
export declare function getVedicChartBundle(): string | null;
|
|
@@ -21,13 +21,15 @@ import fs from "node:fs";
|
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { registerTool, validateRequired, validateCoordinates, SERVER_VERSION } from "../index.js";
|
|
24
|
+
import { UI_RESOURCE_MIME_TYPE, uiResourceUri } from "./ui-resource.js";
|
|
24
25
|
import { getActiveClient } from "../../backend/client.js";
|
|
25
26
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
26
27
|
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
27
28
|
import { coordsFromArgsOrLocation } from "./_location-resolver.js";
|
|
29
|
+
import { RENDER_TOKEN_PROPERTY, renderTokenFromVisual, renderTokenHeaders } from "./_render-token.js";
|
|
28
30
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
|
-
export const VEDIC_CHART_RESOURCE_URI = "
|
|
30
|
-
export const VEDIC_CHART_MIME_TYPE =
|
|
31
|
+
export const VEDIC_CHART_RESOURCE_URI = uiResourceUri("vedic-chart");
|
|
32
|
+
export const VEDIC_CHART_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
|
|
31
33
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
34
|
const BUNDLE_PATHS = [
|
|
33
35
|
path.resolve(here, "..", "..", "..", "dist", "ui", "vedic-chart.html"),
|
|
@@ -75,12 +77,14 @@ function buildVedicModelPayload(chartData, birthParams, theme) {
|
|
|
75
77
|
is_retrograde: retrogradeSet.has(String(p.planet ?? "")),
|
|
76
78
|
}));
|
|
77
79
|
const visual = chartData.visual;
|
|
80
|
+
const renderToken = renderTokenFromVisual(visual);
|
|
78
81
|
return {
|
|
79
82
|
ayanamsa: String(meta.ayanamsha ?? "lahiri").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
80
83
|
lagna: lagnaSign,
|
|
81
84
|
planets,
|
|
82
85
|
_svg: visual?.data,
|
|
83
86
|
_theme: theme,
|
|
87
|
+
...(renderToken ? { _render_token: renderToken } : {}),
|
|
84
88
|
_birth_params: birthParams,
|
|
85
89
|
};
|
|
86
90
|
}
|
|
@@ -237,6 +241,7 @@ registerTool({
|
|
|
237
241
|
enum: ["light", "dark"],
|
|
238
242
|
description: "Render palette for the Rashi grid SVG. Mirrors the MCP host's light/dark color scheme.",
|
|
239
243
|
},
|
|
244
|
+
render_token: RENDER_TOKEN_PROPERTY,
|
|
240
245
|
},
|
|
241
246
|
required: ["datetime"],
|
|
242
247
|
},
|
|
@@ -259,7 +264,12 @@ registerTool({
|
|
|
259
264
|
};
|
|
260
265
|
if (ayanamsa)
|
|
261
266
|
body.ayanamsa = ayanamsa;
|
|
262
|
-
|
|
267
|
+
// A theme reconcile of the chart explore_vedic_chart just rendered presents
|
|
268
|
+
// that render's token and is not billed again (same account + inputs).
|
|
269
|
+
const chartData = await client.request("POST", "/vedic/chart", {
|
|
270
|
+
data: body,
|
|
271
|
+
headers: renderTokenHeaders(args.render_token),
|
|
272
|
+
});
|
|
263
273
|
const modelPayload = buildVedicModelPayload(chartData, {
|
|
264
274
|
datetime,
|
|
265
275
|
location: null,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* beats an outage; the server remains the single authority whenever it is
|
|
24
24
|
* reachable.
|
|
25
25
|
*/
|
|
26
|
-
import { getActiveClient } from "../backend/client.js";
|
|
26
|
+
import { getActiveClient, isBillingOrAuthError } from "../backend/client.js";
|
|
27
27
|
import { hasZoneSuffix, isNaiveClockTime, localToUtcIso } from "./datetime.js";
|
|
28
28
|
/** Where IANA tzdata stops being reference-city best effort. */
|
|
29
29
|
export const TZDATA_AUTHORITATIVE_FROM_YEAR = 1970;
|
|
@@ -69,7 +69,12 @@ export async function localToUtcIsoHistorical(field, dt, tz, coords, timezoneFie
|
|
|
69
69
|
return res.resolved_utc;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
catch {
|
|
72
|
+
catch (err) {
|
|
73
|
+
// Out of credits / signed out / tier-gated: surface the credit wall
|
|
74
|
+
// now rather than silently converting with the pre-1970-wrong tzdata
|
|
75
|
+
// offset (the chart call that follows would hit the same wall anyway).
|
|
76
|
+
if (isBillingOrAuthError(err))
|
|
77
|
+
throw err;
|
|
73
78
|
// Server unreachable or rejected the request — fall through to the
|
|
74
79
|
// local conversion rather than failing the chart call.
|
|
75
80
|
}
|
package/dist/tools/datetime.js
CHANGED
|
@@ -81,7 +81,8 @@ export const DATETIME_CONTRACT_INSTRUCTIONS = "Datetime contract: every clock ti
|
|
|
81
81
|
"the naive local time and name the zone in the sibling `timezone` argument (IANA name, " +
|
|
82
82
|
"e.g. `America/Chicago`). A zone-less clock time is a hard 400 — the engine never " +
|
|
83
83
|
"guesses UTC, because an unstated zone shifts the Ascendant by ~15° per hour and moves " +
|
|
84
|
-
"every house placement. A bare date (no clock time) resolves to 12:00 UTC
|
|
84
|
+
"every house placement. A bare date (no clock time) resolves to 12:00 UTC. A pre-1970 " +
|
|
85
|
+
"local time with coordinates adds 1 credit (historical timezone lookup).\n\n" +
|
|
85
86
|
"PREFER the local-time + `timezone` form over converting to UTC yourself. The server " +
|
|
86
87
|
"resolves the IANA zone against the actual date, including historical DST rules — that " +
|
|
87
88
|
"arithmetic (which offset applied on that specific day, in that specific year) is easy " +
|
package/dist/tools/dev.js
CHANGED
|
@@ -46,10 +46,11 @@ const DEV_API_REFERENCE = "Target API: Open Ephemeris REST API (https://api.open
|
|
|
46
46
|
"Call dev_list_allowed to see all currently available endpoint paths.\n\n" +
|
|
47
47
|
// Prices mirror go-sidecar/internal/api/auth/usage_meter.go; tiers mirror
|
|
48
48
|
// auth/middleware.go requiredTierForPath. /iching/* aliases /human-design/*.
|
|
49
|
-
"CREDIT COST: 1 for most
|
|
50
|
-
"(
|
|
51
|
-
"/
|
|
52
|
-
"
|
|
49
|
+
"CREDIT COST: 1 for most; /predictive/* 5 (transits/search 5-70 by span); /comparative/* 3; " +
|
|
50
|
+
"/calendar/* 10 (moon-phases, lunar-standstill 2; cross-quarter 5); /acg/* 10 (Pro); " +
|
|
51
|
+
"/electional/* 5 (Pro, span-priced searches; moment-analysis, station-tracker free); " +
|
|
52
|
+
"/human-design/*, /iching/* 2 (composite, transit-chart 3; penta 2/member); catalogs 0; " +
|
|
53
|
+
"4xx/5xx refunded. Query {format: 'llm'} = compact output.\n\n";
|
|
53
54
|
const READ_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
54
55
|
" GET /ephemeris/moon/phase — Current/queried moon phase\n" +
|
|
55
56
|
" GET /ephemeris/moon/void-of-course — Next void-of-course period\n" +
|
|
@@ -60,13 +61,13 @@ const READ_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
|
60
61
|
" GET /eclipse/solar/global — Next global solar eclipse (query: date=YYYY-MM-DD)\n" +
|
|
61
62
|
" GET /eclipse/solar/local — Local solar eclipse (query: lat, lon)\n" +
|
|
62
63
|
" GET /tidal/forcing — Gravitational tidal forcing index\n" +
|
|
63
|
-
" GET /calendar/astrology/moon-phases —
|
|
64
|
+
" GET /calendar/astrology/moon-phases — next New/Quarter/Full Moon after a date or jd\n" +
|
|
64
65
|
" GET /location/autocomplete — Geocode a place name (query: query=City Name)\n" +
|
|
65
66
|
" GET /chinese/zodiac — Chinese zodiac year element/animal\n" +
|
|
66
67
|
" GET /catalogs/bodies — List all supported celestial bodies\n";
|
|
67
68
|
const WRITE_COMMON_CALLS = "COMMON CALLS:\n" +
|
|
68
69
|
" POST /ephemeris/natal-chart — Full natal chart (body: {subject: {name: 'Name', birth_datetime: {iso: '1990-04-15T14:30:00-05:00'}, birth_location: {latitude: {decimal: 40.0}, longitude: {decimal: -70.0}, timezone: {}}}})\n" +
|
|
69
|
-
" POST /ephemeris/natal/batch — Up to
|
|
70
|
+
" POST /ephemeris/natal/batch — Up to 100 natal charts in one request (Startup tier)\n" +
|
|
70
71
|
" POST /ephemeris/relocation — Relocated chart (same natal, new location)\n" +
|
|
71
72
|
" POST /predictive/transits/search — Transit event search over a date range\n" +
|
|
72
73
|
" POST /predictive/returns/solar — Solar return chart\n" +
|
|
@@ -10,7 +10,8 @@ registerTool({
|
|
|
10
10
|
"date range, 'find me a good day next month' or 'pick an auspicious window'. Returns the best " +
|
|
11
11
|
"continuous timing windows in the range, scored hourly on essential dignity, aspect quality, sect " +
|
|
12
12
|
"and void-of-course Moon penalties, with optional filters (avoid_voc, lunar_phase).\n\n" +
|
|
13
|
-
"CREDIT COST: 5
|
|
13
|
+
"CREDIT COST: ≤30 days 5, ≤60 days 8, ≤120 days 12. Pro tier; max window Pro 30 days, Startup " +
|
|
14
|
+
"60, Scale 120.\n\n" +
|
|
14
15
|
"Do not use to score one specific moment or 'right now' — use electional_moment_analysis; for " +
|
|
15
16
|
"retrograde dates use electional_station_tracker.",
|
|
16
17
|
inputSchema: {
|
|
@@ -222,7 +223,7 @@ registerTool({
|
|
|
222
223
|
description: "Find all active aspects between planets at a specific moment. Returns aspect type, " +
|
|
223
224
|
"orb, quality score, and whether it's applying or separating. Great for checking " +
|
|
224
225
|
"the 'weather' of a given day.\n\n" +
|
|
225
|
-
"CREDIT COST: 5 credits per call.\n\n" +
|
|
226
|
+
"CREDIT COST: 5 credits per call. Pro tier.\n\n" +
|
|
226
227
|
"EXAMPLE: What aspects are active on March 21, 2026?\n" +
|
|
227
228
|
" date='2026-03-21T12:00:00Z'",
|
|
228
229
|
inputSchema: {
|
|
@@ -276,7 +277,7 @@ registerTool({
|
|
|
276
277
|
"Sun, Moon, Mercury, Venus, Mars and the lunar nodes — the same body set and formula the CCG hybrid " +
|
|
277
278
|
"transit map (/acg/ccg mode=ccg_hybrid) draws with; every other body stays a transit. Progressed " +
|
|
278
279
|
"crossings carry progressed: true and search_summary.progression names what was progressed.\n\n" +
|
|
279
|
-
"CREDIT COST: 5
|
|
280
|
+
"CREDIT COST: priced by start→end span — ≤1 month 5, ≤3 months 8, ≤6 months 10, ≤1 year 12. Pro tier.\n\n" +
|
|
280
281
|
"EXAMPLE: When does Mercury cross the Descendant over Warsaw on 2026-07-30?\n" +
|
|
281
282
|
" latitude=52.2297, longitude=21.0122, bodies='Mercury', angles='DC',\n" +
|
|
282
283
|
" start='2026-07-30T00:00:00Z', end='2026-07-31T00:00:00Z'",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTool, validateRequired, validateCoordinates } from "../index.js";
|
|
2
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
2
|
+
import { getActiveClient, rethrowBillingOrAuth } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { DATETIME_DESC, TIMEZONE_PROPERTY, assertZonedDatetime, toDateTimeInputBody } from "../datetime.js";
|
|
5
5
|
// `field`/`timezoneField` name the caller's own argument path (e.g.
|
|
@@ -134,10 +134,19 @@ registerTool({
|
|
|
134
134
|
}
|
|
135
135
|
// Fan-out: the backend only handles one planet per call; query all 10 in parallel.
|
|
136
136
|
const PLANET_IDS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
137
|
-
const
|
|
137
|
+
const settled = await Promise.allSettled(PLANET_IDS.map((pid) => client.post("/ephemeris/retrograde-status", {
|
|
138
138
|
date_time: toDateTimeInputBody("datetime", args.datetime, args.timezone),
|
|
139
139
|
planet_id: pid,
|
|
140
|
-
})
|
|
140
|
+
})));
|
|
141
|
+
// Out of credits / signed out / tier-gated: throw the credit-wall (or
|
|
142
|
+
// auth) error so its top-up link reaches the assistant. Swallowing it
|
|
143
|
+
// used to return `{planets: {}, success: true}` at zero credits.
|
|
144
|
+
rethrowBillingOrAuth(settled);
|
|
145
|
+
const results = settled.map((s) => (s.status === "fulfilled" ? s.value : null));
|
|
146
|
+
if (results.every((r) => r == null)) {
|
|
147
|
+
// Every planet failed for a non-billing reason — never report success with no data.
|
|
148
|
+
throw settled[0].reason;
|
|
149
|
+
}
|
|
141
150
|
// Merge into a keyed object: { planet_name: {...status} }
|
|
142
151
|
const merged = {};
|
|
143
152
|
for (const res of results) {
|
|
@@ -151,7 +160,13 @@ registerTool({
|
|
|
151
160
|
};
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
|
-
|
|
163
|
+
const failed = PLANET_IDS.filter((_, i) => results[i] == null);
|
|
164
|
+
return {
|
|
165
|
+
planets: merged,
|
|
166
|
+
datetime: args.datetime,
|
|
167
|
+
success: true,
|
|
168
|
+
...(failed.length ? { failed_planet_ids: failed } : {}),
|
|
169
|
+
};
|
|
155
170
|
},
|
|
156
171
|
});
|
|
157
172
|
// POST /ephemeris/midpoints
|
|
@@ -8,7 +8,7 @@ registerTool({
|
|
|
8
8
|
name: "human_design_composite",
|
|
9
9
|
description: "Calculate a Human Design composite chart for two people. Merges both bodygraphs " +
|
|
10
10
|
"to show shared channels, authority dynamics, and relationship type.\n\n" +
|
|
11
|
-
"CREDIT COST:
|
|
11
|
+
"CREDIT COST: 3 credits per call.\n\n" +
|
|
12
12
|
"EXAMPLE (local times + zones):\n" +
|
|
13
13
|
" person_a_datetime='1990-04-15T14:30:00', person_a_timezone='America/Chicago',\n" +
|
|
14
14
|
" person_b_datetime='1988-09-22T08:15:00', person_b_timezone='America/Los_Angeles'\n" +
|
|
@@ -74,7 +74,7 @@ registerTool({
|
|
|
74
74
|
name: "human_design_penta",
|
|
75
75
|
description: "Calculate a Human Design Penta (group) chart for 3-5 people. Shows functional " +
|
|
76
76
|
"attributes, leadership dynamics, channels, redundancies, and a group stability score.\n\n" +
|
|
77
|
-
"CREDIT COST:
|
|
77
|
+
"CREDIT COST: 2 credits per member (6-10 for 3-5 people).\n\n" +
|
|
78
78
|
"EXAMPLE (3 people — each datetime states its zone):\n" +
|
|
79
79
|
" group_name='Team Alpha',\n" +
|
|
80
80
|
" members=[\n" +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const NEXT_LUNAR_PHASE_MAX_COUNT = 12;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { registerTool, validateCoordinates } from "../index.js";
|
|
2
|
-
import { getActiveClient } from "../../backend/client.js";
|
|
2
|
+
import { getActiveClient, rethrowBillingOrAuth } from "../../backend/client.js";
|
|
3
3
|
import { OUTPUT_SCHEMA_JSON } from "../output-schemas.js";
|
|
4
4
|
import { TIMEZONE_PROPERTY } from "../datetime.js";
|
|
5
5
|
import { localToUtcIsoHistorical } from "../datetime-historical.js";
|
|
@@ -53,8 +53,15 @@ registerTool({
|
|
|
53
53
|
getActiveClient().request("GET", "/ephemeris/moon/phase", { params }),
|
|
54
54
|
getActiveClient().request("GET", "/ephemeris/moon/void-of-course", { params }),
|
|
55
55
|
]);
|
|
56
|
+
// Out of credits / signed out / tier-gated must reach the assistant as
|
|
57
|
+
// the thrown credit-wall (or auth) error with its top-up link — never
|
|
58
|
+
// as an `error` string tucked inside a success-shaped result.
|
|
59
|
+
rethrowBillingOrAuth([phase, voc]);
|
|
60
|
+
// The phase IS the answer; with it gone there is nothing to degrade to.
|
|
61
|
+
if (phase.status === "rejected")
|
|
62
|
+
throw phase.reason;
|
|
56
63
|
return {
|
|
57
|
-
phase: phase.
|
|
64
|
+
phase: phase.value,
|
|
58
65
|
void_of_course: voc.status === "fulfilled" ? voc.value : { error: voc.reason?.message },
|
|
59
66
|
};
|
|
60
67
|
},
|
|
@@ -65,7 +72,7 @@ registerTool({
|
|
|
65
72
|
"date is the next quarter moon' or 'list the next 3 full moons'. Returns the exact UTC datetime " +
|
|
66
73
|
"of each upcoming occurrence of the requested phase (phase = new_moon | full_moon | first_quarter " +
|
|
67
74
|
"| last_quarter), optionally after a given date and for count occurrences.\n\n" +
|
|
68
|
-
"CREDIT COST:
|
|
75
|
+
"CREDIT COST: 2 credits per occurrence; count max 12.\n\n" +
|
|
69
76
|
"Do not use for the phase or sign the Moon is in right now — use ephemeris_moon_phase; for " +
|
|
70
77
|
"eclipses use ephemeris_next_eclipse.",
|
|
71
78
|
inputSchema: {
|
|
@@ -86,7 +93,7 @@ registerTool({
|
|
|
86
93
|
},
|
|
87
94
|
count: {
|
|
88
95
|
type: "integer",
|
|
89
|
-
description: "Number of upcoming occurrences to return (1-12). Default 1.",
|
|
96
|
+
description: "Number of upcoming occurrences to return (1-12, 2 credits each). Default 1.",
|
|
90
97
|
},
|
|
91
98
|
},
|
|
92
99
|
required: ["phase"],
|
|
@@ -113,55 +120,24 @@ registerTool({
|
|
|
113
120
|
if (!wanted) {
|
|
114
121
|
throw new Error(`Unknown phase '${args.phase}'. Use one of: ${Object.keys(PHASE_IDS).join(", ")}.`);
|
|
115
122
|
}
|
|
116
|
-
const
|
|
123
|
+
const requested = Number.isFinite(Number(args.count)) ? Math.trunc(Number(args.count)) : 1;
|
|
124
|
+
const count = Math.max(1, Math.min(NEXT_LUNAR_PHASE_MAX_COUNT, requested));
|
|
117
125
|
const startDate = (args.after_date ?? new Date().toISOString()).substring(0, 10);
|
|
118
126
|
const startMs = Date.parse(startDate + "T00:00:00Z");
|
|
119
127
|
if (Number.isNaN(startMs)) {
|
|
120
128
|
throw new Error(`after_date must be an ISO date like '2026-06-01', got '${args.after_date}'.`);
|
|
121
129
|
}
|
|
122
|
-
const
|
|
123
|
-
// The calendar endpoint takes a single `date` (it searches forward from noon UTC
|
|
124
|
-
// that day) and returns the first occurrence of each principal phase within the
|
|
125
|
-
// following ~30 days. So one request yields at most one hit for the phase we want.
|
|
126
|
-
async function phaseTimesFrom(probeDay) {
|
|
127
|
-
const result = await getActiveClient().request("GET", "/calendar/astrology/moon-phases", { params: { date: probeDay } });
|
|
128
|
-
const events = Array.isArray(result?.data?.events) ? result.data.events : [];
|
|
129
|
-
// The endpoint prepends the Moon's *current* 8-phase bucket, stamped with the
|
|
130
|
-
// query instant (noon UTC of `date`) rather than an event time. Left in, it
|
|
131
|
-
// reads as a real phase occurring at exactly 12:00:00. Computed moments always
|
|
132
|
-
// carry sub-second precision, so an exact hit on noon is that placeholder.
|
|
133
|
-
const probeNoonMs = Date.parse(probeDay + "T12:00:00Z");
|
|
134
|
-
return events
|
|
135
|
-
.filter((e) => wanted.includes(String(e?.phase ?? e?.phase_name ?? "").toLowerCase()))
|
|
136
|
-
.map((e) => Date.parse(e?.time ?? e?.iso_time ?? ""))
|
|
137
|
-
.filter((ms) => !Number.isNaN(ms) && ms !== probeNoonMs)
|
|
138
|
-
.sort((a, b) => a - b);
|
|
139
|
-
}
|
|
140
|
-
// Probe from the day containing `afterMs - 12h`, so its noon-UTC search origin is
|
|
141
|
-
// always at or before `afterMs` and an occurrence earlier the same day is still
|
|
142
|
-
// visible. That leaves under a day of slack behind the cursor, which can hold at
|
|
143
|
-
// most one occurrence (they are ~29.5 days apart) — hence a single re-probe.
|
|
144
|
-
async function findNext(afterMs) {
|
|
145
|
-
let probeMs = afterMs - HALF_DAY;
|
|
146
|
-
for (let probe = 0; probe < 2; probe++) {
|
|
147
|
-
const hits = await phaseTimesFrom(new Date(probeMs).toISOString().substring(0, 10));
|
|
148
|
-
const next = hits.find((ms) => ms > afterMs);
|
|
149
|
-
if (next != null)
|
|
150
|
-
return next;
|
|
151
|
-
if (hits.length === 0)
|
|
152
|
-
return null;
|
|
153
|
-
probeMs = hits[hits.length - 1] + HALF_DAY;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
130
|
+
const client = getActiveClient();
|
|
157
131
|
const results = [];
|
|
158
|
-
|
|
132
|
+
// Include an occurrence at exactly 00:00 on after_date: the engine
|
|
133
|
+
// reports phases strictly after the probe instant.
|
|
134
|
+
let probeMs = startMs - 1000;
|
|
159
135
|
for (let i = 0; i < count; i++) {
|
|
160
|
-
const hitMs = await
|
|
136
|
+
const hitMs = await nextPhaseAfter(client, probeMs, wanted);
|
|
161
137
|
if (hitMs == null)
|
|
162
138
|
break;
|
|
163
139
|
results.push({ phase: args.phase, datetime: new Date(hitMs).toISOString() });
|
|
164
|
-
|
|
140
|
+
probeMs = hitMs + PROBE_STEP_PAST_HIT_MS;
|
|
165
141
|
}
|
|
166
142
|
// Every principal phase recurs every ~29.5 days, so an empty result is impossible.
|
|
167
143
|
// Fail loudly rather than handing back a plausible "there isn't one" non-answer.
|
|
@@ -179,3 +155,35 @@ registerTool({
|
|
|
179
155
|
};
|
|
180
156
|
},
|
|
181
157
|
});
|
|
158
|
+
// ── Next-lunar-phase lookup ─────────────────────────────────────────────────
|
|
159
|
+
//
|
|
160
|
+
// /calendar/astrology/moon-phases is the engine's exact principal-phase
|
|
161
|
+
// finder. It takes a single instant (`jd`, or a `date` = noon UTC), not a
|
|
162
|
+
// range, and returns the first New / First Quarter / Full / Third Quarter
|
|
163
|
+
// strictly after it within 30 days — so one call yields exactly one
|
|
164
|
+
// occurrence of each phase. The Go meter prices it at a flat 2 credits (one
|
|
165
|
+
// fixed 30-day span; usage_meter.go). There is no range form to widen, so
|
|
166
|
+
// `count` occurrences of one phase take `count` calls: 2 credits each.
|
|
167
|
+
//
|
|
168
|
+
// Probing with `jd` (the exact instant) rather than `date` (noon UTC) is what
|
|
169
|
+
// makes it one call per occurrence: the search origin sits exactly on the
|
|
170
|
+
// cursor, so no occurrence between midnight and noon can be skipped and no
|
|
171
|
+
// re-probe is needed.
|
|
172
|
+
export const NEXT_LUNAR_PHASE_MAX_COUNT = 12;
|
|
173
|
+
const UNIX_EPOCH_JD = 2440587.5;
|
|
174
|
+
const DAY_MS = 86_400_000;
|
|
175
|
+
/** Step past a found root before the next probe, so the solver's tolerance cannot return it again. */
|
|
176
|
+
const PROBE_STEP_PAST_HIT_MS = 60_000;
|
|
177
|
+
async function nextPhaseAfter(client, afterMs, wanted) {
|
|
178
|
+
const jd = afterMs / DAY_MS + UNIX_EPOCH_JD;
|
|
179
|
+
const result = await client.request("GET", "/calendar/astrology/moon-phases", {
|
|
180
|
+
params: { jd: jd.toFixed(6) },
|
|
181
|
+
});
|
|
182
|
+
const events = Array.isArray(result?.data?.events) ? result.data.events : [];
|
|
183
|
+
const hits = events
|
|
184
|
+
.filter((e) => wanted.includes(String(e?.phase ?? e?.phase_name ?? "").toLowerCase()))
|
|
185
|
+
.map((e) => Date.parse(e?.time ?? e?.iso_time ?? ""))
|
|
186
|
+
.filter((ms) => !Number.isNaN(ms) && ms > afterMs)
|
|
187
|
+
.sort((a, b) => a - b);
|
|
188
|
+
return hits.length ? hits[0] : null;
|
|
189
|
+
}
|