@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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.19.0] — 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The Human Design bodygraph is easier to read.** Gate numbers use a clean sans-serif and are
|
|
14
|
+
noticeably larger at chat width; the Design and Personality planet columns sit close to the body
|
|
15
|
+
under their own headings; open centres use the theme's colour instead of glaring white; channels
|
|
16
|
+
defined from both sides are drawn as two parallel stripes (Design red beside Personality); the
|
|
17
|
+
mandala's body is about 1.3× larger inside its rings.
|
|
18
|
+
- **Select a single activation.** Clicking a planet row (for example Personality Moon) highlights
|
|
19
|
+
only that activation's gate, stub and channel, never its Design twin. The rendered SVG tags every
|
|
20
|
+
element by side and activation, so your own CSS can do the same.
|
|
21
|
+
- **Every chart widget fits the chat better.** Widgets size themselves from the host; scrolling the
|
|
22
|
+
conversation past a chart scrolls instead of zooming (zoom with Ctrl/Cmd + scroll or a pinch); the
|
|
23
|
+
fullscreen button only appears where the host supports fullscreen; moon phase and transit timeline
|
|
24
|
+
support picture-in-picture; fullscreen uses one shared layout that stacks into a single column on
|
|
25
|
+
phones; all seven widgets share one colour and font set, and the host draws the card border.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Changing theme no longer drops a widget out of fullscreen.
|
|
29
|
+
- On moon phase and transit timeline, the info button no longer covers the fullscreen button.
|
|
30
|
+
- Bodygraph hover highlights work again.
|
|
31
|
+
- Widget resources are versioned, so hosts that cache them (such as ChatGPT) pick up new widget
|
|
32
|
+
versions after an update without reinstalling the app.
|
|
33
|
+
|
|
34
|
+
## [4.18.0] — 2026-09-26
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- **`explore_bi_wheel` progressed, solar return and lunar return modes work again.** They sent
|
|
38
|
+
requests the API rejected ("Validation failed" / "birth_datetime is required"); all six modes now
|
|
39
|
+
return a chart.
|
|
40
|
+
- **Solar arc directions are exact.** `explore_bi_wheel` mode `solar_arc` used a flat ~1°-per-year
|
|
41
|
+
estimate and left the houses at their birth positions. It now uses the Sun's real arc from the API:
|
|
42
|
+
every planet, angle and house cusp is directed by the same arc, with correct signs and houses.
|
|
43
|
+
`ephemeris_progressed_chart` with `method: "solar_arc"` directs the angles and house cusps too.
|
|
44
|
+
- **Every lunar node is named Mean or True.** The charts show "North Node (Mean)", "North Node
|
|
45
|
+
(True)", "South Node (Mean)" and "South Node (True)" as four separate points. The two South Nodes
|
|
46
|
+
used to merge into one, and the mean North Node was shown as a bare "North Node". The Mean/True
|
|
47
|
+
toggle now applies to the South Node as well.
|
|
48
|
+
- **Bi-wheel recalculation keeps the house system you choose** for the outer chart too (it was always
|
|
49
|
+
Placidus).
|
|
50
|
+
- **Black Moon Lilith appears on the chart wheel, named Mean, True or Interpolated.** Asking
|
|
51
|
+
`explore_natal_chart` for `lilith` or `lilith_true` returned the chart without it. The three Liliths
|
|
52
|
+
are now shown as "Lilith (Mean)", "Lilith (True)" and "Lilith (Interpolated)" (new slug
|
|
53
|
+
`lilith_interpolated`), never merged into one.
|
|
54
|
+
- **`explore_natal_chart` with `bodies: ["all"]` works.** It was rejected because it asked for Vertex and
|
|
55
|
+
Part of Fortune as extra bodies; "all" now covers every body the chart can add.
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
- **`include_visual` on `ephemeris_progressed_chart`.** The API never draws a progressed chart
|
|
59
|
+
image, so the option returned no picture. It was never charged. To see progressions over the natal
|
|
60
|
+
chart, use `explore_bi_wheel` with mode `progressed`.
|
|
61
|
+
|
|
10
62
|
## [4.17.0] — 2026-09-24
|
|
11
63
|
|
|
12
64
|
### Added
|
package/README.md
CHANGED
|
@@ -212,7 +212,7 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
212
212
|
|
|
213
213
|
- Missing/invalid credentials (`401`): tool call fails with a message that points users to sign up/sign in at `https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount`, then create/manage keys in `https://openephemeris.com/dashboard?tab=account`.
|
|
214
214
|
- Tier-gated endpoint (`403`): tool call returns an upgrade-required message with `https://openephemeris.com/pay` and dashboard billing/key management link.
|
|
215
|
-
-
|
|
215
|
+
- Out of credits (`402`): tool call returns a one-tap top-up link (Explorer's 150 free credits are one-time and do not reset; plan allowances renew each billing period) plus the dashboard usage link. Failed calls (any `4xx`/`5xx`) are refunded.
|
|
216
216
|
- Burst/rate limit (`429`): tool call returns retry guidance and links to dashboard usage monitoring.
|
|
217
217
|
|
|
218
218
|
## What You Can Ask
|
|
@@ -256,13 +256,13 @@ instead of the picture, so nothing breaks, you just don't get the wheel.
|
|
|
256
256
|
| Tool | What opens | What you can click | Credits |
|
|
257
257
|
|---|---|---|---|
|
|
258
258
|
| `explore_natal_chart` | Natal wheel — planets, houses, aspects, angles | Planets, houses, aspect lines; recalculate with new settings | 1 |
|
|
259
|
-
| `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 |
|
|
260
|
-
| `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables |
|
|
261
|
-
| `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels |
|
|
262
|
-
| `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type |
|
|
259
|
+
| `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 (6 for solar/lunar return) |
|
|
260
|
+
| `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables | 4 |
|
|
261
|
+
| `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels | 5 |
|
|
262
|
+
| `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type | 5 |
|
|
263
263
|
| `explore_vedic_chart` | South Indian Rashi grid — sidereal placements and Lagna | Each rashi, for its placements and nakshatras | 3 |
|
|
264
264
|
| `explore_bazi_chart` | Four Pillars (四柱命盘) — Year, Month, Day, Hour | Each pillar | 3 |
|
|
265
|
-
| `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 |
|
|
265
|
+
| `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 for up to 1 year (priced by span) |
|
|
266
266
|
| `explore_moon_phase` | Moon dial — illumination, phase, sign, void-of-course | Recalculate for another moment | 3 |
|
|
267
267
|
|
|
268
268
|
Ask for these the way you'd ask a person: *"show me my chart"*, *"put today's transits over my Human Design"*, *"what's the moon doing right now"*. The model picks the app.
|
|
@@ -300,10 +300,10 @@ Screenshots of each are on the way.
|
|
|
300
300
|
| Lunar return | `ephemeris_lunar_return` | Explorer |
|
|
301
301
|
| Planetary return | `ephemeris_planetary_return` | Explorer |
|
|
302
302
|
| Astrocartography lines | `acg_power_lines` | Developer |
|
|
303
|
-
| ACG hits at location | `acg_hits` |
|
|
303
|
+
| ACG hits at location | `acg_hits` | Developer |
|
|
304
304
|
| Venus Star Points | `venus_star_points` + 4 more | Explorer |
|
|
305
|
-
| Chart wheel image | `ephemeris_chart_wheel` |
|
|
306
|
-
| Bi-wheel image | `ephemeris_bi_wheel` |
|
|
305
|
+
| Chart wheel image | `ephemeris_chart_wheel` | Explorer |
|
|
306
|
+
| Bi-wheel image | `ephemeris_bi_wheel` | Explorer |
|
|
307
307
|
| Dignities / Midpoints / Fixed stars | `ephemeris_dignities`, `ephemeris_midpoints`, `ephemeris_fixed_stars` | Explorer |
|
|
308
308
|
|
|
309
309
|
## Tooling Model
|
|
@@ -335,7 +335,7 @@ Screenshots of each are on the way.
|
|
|
335
335
|
| `OPENEPHEMERIS_PROFILE` | No | `dev` by default |
|
|
336
336
|
| `OPENEPHEMERIS_TOOLS` | No | `core` (default) advertises a focused everyday tool set; `full` advertises every tool. See [Tool surface](#tool-surface) |
|
|
337
337
|
| `OPENEPHEMERIS_TELEMETRY` | No | Set to `0`/`false`/`off` to disable anonymous usage reporting. `DO_NOT_TRACK=1` also works. See [Telemetry](#telemetry) |
|
|
338
|
-
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth |
|
|
338
|
+
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth (stdio only; the hosted server refuses to start with one and always authenticates as the signed-in user) |
|
|
339
339
|
| `OPENEPHEMERIS_JWT` | No | Bearer token auth |
|
|
340
340
|
| `OPENEPHEMERIS_DEV_ALLOWLIST_PATH` | No | Override allowlist file path |
|
|
341
341
|
| `MCP_USER_ID` | No | Per-instance user identifier |
|
package/dist/backend/client.d.ts
CHANGED
|
@@ -6,7 +6,29 @@ export interface BackendConfig {
|
|
|
6
6
|
apiKey?: string;
|
|
7
7
|
/** Back-compat: previous name for JWT token (Bearer). */
|
|
8
8
|
authToken?: string;
|
|
9
|
+
/**
|
|
10
|
+
* When false, the client uses ONLY the credentials passed in this config and
|
|
11
|
+
* never falls back to the process environment (OPENEPHEMERIS_SERVICE_KEY,
|
|
12
|
+
* OPENEPHEMERIS_API_KEY, OPENEPHEMERIS_JWT and their legacy aliases).
|
|
13
|
+
*
|
|
14
|
+
* The hosted HTTP server builds one client per user session and MUST pass
|
|
15
|
+
* false: the interceptor sends X-Service-Key ahead of the user's own key, so
|
|
16
|
+
* an env credential inherited by a per-session client would make every
|
|
17
|
+
* user's calls unmetered (service key) or billed to one account (API key /
|
|
18
|
+
* JWT). Defaults to true so the stdio/local server keeps reading its config
|
|
19
|
+
* from the environment exactly as before.
|
|
20
|
+
*/
|
|
21
|
+
inheritEnvCredentials?: boolean;
|
|
9
22
|
}
|
|
23
|
+
/** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
|
|
24
|
+
export declare const SERVICE_KEY_ENV_VARS: readonly ["OPENEPHEMERIS_SERVICE_KEY", "ASTROMCP_SERVICE_KEY", "MERIDIAN_SERVICE_KEY"];
|
|
25
|
+
/** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
|
|
26
|
+
export declare const USER_CREDENTIAL_ENV_VARS: readonly ["OPENEPHEMERIS_API_KEY", "ASTROMCP_API_KEY", "MERIDIAN_API_KEY", "OPENEPHEMERIS_JWT", "ASTROMCP_JWT", "MERIDIAN_AUTH_TOKEN"];
|
|
27
|
+
/** Names of the credential env vars that are set (non-empty) in `env`. */
|
|
28
|
+
export declare function presentCredentialEnvVars(env?: NodeJS.ProcessEnv): {
|
|
29
|
+
serviceKeys: string[];
|
|
30
|
+
userCredentials: string[];
|
|
31
|
+
};
|
|
10
32
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
11
33
|
export interface BackendRequestOptions {
|
|
12
34
|
params?: Record<string, unknown>;
|
|
@@ -25,8 +47,33 @@ export declare const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboar
|
|
|
25
47
|
export declare const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
26
48
|
export declare const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
27
49
|
/** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
|
|
28
|
-
export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
|
|
50
|
+
export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
|
|
51
|
+
/**
|
|
52
|
+
* The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
|
|
53
|
+
* the caller's account so it opens Stripe checkout without a sign-in step
|
|
54
|
+
* (most people hitting the wall are in ChatGPT/Claude and not signed in on the
|
|
55
|
+
* site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
|
|
56
|
+
* clicks from raw API ones; otherwise fall back to the unsigned link.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveTopupUrl(apiTopupUrl: unknown): string;
|
|
29
59
|
export declare const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error body (typically a 422) is really the credit wall: the
|
|
62
|
+
* detail / type / field codes mention insufficient credits or an exceeded
|
|
63
|
+
* quota. Checked over the whole serialized body because the signal can sit in
|
|
64
|
+
* `detail`, `type`, or a nested `errors[].code`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function looksLikeCreditExhaustion(data: unknown, msg?: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* True for errors that mean "this account cannot be charged / is not allowed"
|
|
69
|
+
* — the credit wall (402), missing auth (401), or a tier gate (403). Tools
|
|
70
|
+
* that fan out or degrade gracefully must rethrow these instead of swallowing
|
|
71
|
+
* them: a placeholder result at zero credits hides the paywall (and its
|
|
72
|
+
* top-up link) from the assistant and reads as real data.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isBillingOrAuthError(err: unknown): err is BackendError;
|
|
75
|
+
/** Rethrows the first billing/auth BackendError among settled results, if any. */
|
|
76
|
+
export declare function rethrowBillingOrAuth(results: PromiseSettledResult<unknown>[]): void;
|
|
30
77
|
export declare class BackendError extends Error {
|
|
31
78
|
readonly status: number;
|
|
32
79
|
readonly code: string;
|
|
@@ -59,6 +106,15 @@ export declare class BackendClient {
|
|
|
59
106
|
* (JWT is the lowest-priority credential in the interceptor).
|
|
60
107
|
*/
|
|
61
108
|
setJwt(jwt: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* Drop every credential this client holds (service key, API key, JWT). The
|
|
111
|
+
* hosted HTTP server calls this on the module singleton at startup: the
|
|
112
|
+
* singleton is what getActiveClient() falls back to outside a session
|
|
113
|
+
* context, and it was built from the environment at import time.
|
|
114
|
+
*/
|
|
115
|
+
clearCredentials(): void;
|
|
116
|
+
/** Which configured credential the interceptor will send (diagnostics/tests). */
|
|
117
|
+
credentialKind(): "service_key" | "api_key" | "jwt" | null;
|
|
62
118
|
/**
|
|
63
119
|
* Start the device auth flow in the background if not already running.
|
|
64
120
|
* Called automatically when no credentials are found in the interceptor.
|
package/dist/backend/client.js
CHANGED
|
@@ -2,6 +2,29 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import axios, { AxiosError } from "axios";
|
|
3
3
|
import { CredentialManager } from "../auth/credentials.js";
|
|
4
4
|
import { DeviceAuthFlow } from "../auth/device-auth.js";
|
|
5
|
+
/** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
|
|
6
|
+
export const SERVICE_KEY_ENV_VARS = [
|
|
7
|
+
"OPENEPHEMERIS_SERVICE_KEY",
|
|
8
|
+
"ASTROMCP_SERVICE_KEY",
|
|
9
|
+
"MERIDIAN_SERVICE_KEY",
|
|
10
|
+
];
|
|
11
|
+
/** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
|
|
12
|
+
export const USER_CREDENTIAL_ENV_VARS = [
|
|
13
|
+
"OPENEPHEMERIS_API_KEY",
|
|
14
|
+
"ASTROMCP_API_KEY",
|
|
15
|
+
"MERIDIAN_API_KEY",
|
|
16
|
+
"OPENEPHEMERIS_JWT",
|
|
17
|
+
"ASTROMCP_JWT",
|
|
18
|
+
"MERIDIAN_AUTH_TOKEN",
|
|
19
|
+
];
|
|
20
|
+
/** Names of the credential env vars that are set (non-empty) in `env`. */
|
|
21
|
+
export function presentCredentialEnvVars(env = process.env) {
|
|
22
|
+
const isSet = (name) => typeof env[name] === "string" && env[name].trim() !== "";
|
|
23
|
+
return {
|
|
24
|
+
serviceKeys: SERVICE_KEY_ENV_VARS.filter(isSet),
|
|
25
|
+
userCredentials: USER_CREDENTIAL_ENV_VARS.filter(isSet),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
5
28
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
6
29
|
const RETRY_DELAYS_MS = [1_000, 2_000, 4_000]; // Three retries with backoff
|
|
7
30
|
// NOTE: 429 is intentionally NOT retryable. Every expensive compute endpoint is
|
|
@@ -23,8 +46,64 @@ export const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=ac
|
|
|
23
46
|
export const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
24
47
|
export const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
25
48
|
/** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
|
|
26
|
-
export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
|
|
49
|
+
export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
|
|
50
|
+
/**
|
|
51
|
+
* The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
|
|
52
|
+
* the caller's account so it opens Stripe checkout without a sign-in step
|
|
53
|
+
* (most people hitting the wall are in ChatGPT/Claude and not signed in on the
|
|
54
|
+
* site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
|
|
55
|
+
* clicks from raw API ones; otherwise fall back to the unsigned link.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTopupUrl(apiTopupUrl) {
|
|
58
|
+
if (typeof apiTopupUrl !== "string")
|
|
59
|
+
return TOPUP_URL;
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(apiTopupUrl);
|
|
62
|
+
if (url.protocol !== "https:" || url.hostname !== "openephemeris.com" || url.pathname !== "/topup") {
|
|
63
|
+
return TOPUP_URL;
|
|
64
|
+
}
|
|
65
|
+
url.searchParams.set("src", "mcp_402");
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return TOPUP_URL;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
27
72
|
export const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
73
|
+
const CREDIT_EXHAUSTION_RE = /credits?_insufficient|insufficient[ _-]?credits?|not enough credits|out of credits|credits?[ _-]exhausted|quota[ _-]exceeded|usage_quota|requires \d+ credits/i;
|
|
74
|
+
/**
|
|
75
|
+
* Whether an error body (typically a 422) is really the credit wall: the
|
|
76
|
+
* detail / type / field codes mention insufficient credits or an exceeded
|
|
77
|
+
* quota. Checked over the whole serialized body because the signal can sit in
|
|
78
|
+
* `detail`, `type`, or a nested `errors[].code`.
|
|
79
|
+
*/
|
|
80
|
+
export function looksLikeCreditExhaustion(data, msg) {
|
|
81
|
+
let text = msg ?? "";
|
|
82
|
+
try {
|
|
83
|
+
text += " " + (typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// unserializable body — fall back to the message alone
|
|
87
|
+
}
|
|
88
|
+
return CREDIT_EXHAUSTION_RE.test(text);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True for errors that mean "this account cannot be charged / is not allowed"
|
|
92
|
+
* — the credit wall (402), missing auth (401), or a tier gate (403). Tools
|
|
93
|
+
* that fan out or degrade gracefully must rethrow these instead of swallowing
|
|
94
|
+
* them: a placeholder result at zero credits hides the paywall (and its
|
|
95
|
+
* top-up link) from the assistant and reads as real data.
|
|
96
|
+
*/
|
|
97
|
+
export function isBillingOrAuthError(err) {
|
|
98
|
+
return err instanceof BackendError && (err.status === 402 || err.status === 401 || err.status === 403);
|
|
99
|
+
}
|
|
100
|
+
/** Rethrows the first billing/auth BackendError among settled results, if any. */
|
|
101
|
+
export function rethrowBillingOrAuth(results) {
|
|
102
|
+
for (const r of results) {
|
|
103
|
+
if (r.status === "rejected" && isBillingOrAuthError(r.reason))
|
|
104
|
+
throw r.reason;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
28
107
|
function sleep(ms) {
|
|
29
108
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
109
|
}
|
|
@@ -53,22 +132,24 @@ export class BackendClient {
|
|
|
53
132
|
_authFlowResult = null;
|
|
54
133
|
constructor(config) {
|
|
55
134
|
this.userId = config.userId || "anonymous";
|
|
135
|
+
// See BackendConfig.inheritEnvCredentials — per-session hosted clients opt out.
|
|
136
|
+
const env = config.inheritEnvCredentials === false ? {} : process.env;
|
|
56
137
|
this.jwt =
|
|
57
138
|
config.jwt ||
|
|
58
139
|
config.authToken ||
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
env.OPENEPHEMERIS_JWT ||
|
|
141
|
+
env.ASTROMCP_JWT ||
|
|
142
|
+
env.MERIDIAN_AUTH_TOKEN;
|
|
62
143
|
this.serviceKey =
|
|
63
144
|
config.serviceKey ||
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
env.OPENEPHEMERIS_SERVICE_KEY ||
|
|
146
|
+
env.ASTROMCP_SERVICE_KEY ||
|
|
147
|
+
env.MERIDIAN_SERVICE_KEY;
|
|
67
148
|
this.apiKey =
|
|
68
149
|
config.apiKey ||
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
150
|
+
env.OPENEPHEMERIS_API_KEY ||
|
|
151
|
+
env.ASTROMCP_API_KEY ||
|
|
152
|
+
env.MERIDIAN_API_KEY;
|
|
72
153
|
this.credentialManager = new CredentialManager();
|
|
73
154
|
this.client = axios.create({
|
|
74
155
|
baseURL: config.baseURL ||
|
|
@@ -138,6 +219,27 @@ export class BackendClient {
|
|
|
138
219
|
setJwt(jwt) {
|
|
139
220
|
this.jwt = jwt;
|
|
140
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Drop every credential this client holds (service key, API key, JWT). The
|
|
224
|
+
* hosted HTTP server calls this on the module singleton at startup: the
|
|
225
|
+
* singleton is what getActiveClient() falls back to outside a session
|
|
226
|
+
* context, and it was built from the environment at import time.
|
|
227
|
+
*/
|
|
228
|
+
clearCredentials() {
|
|
229
|
+
this.serviceKey = undefined;
|
|
230
|
+
this.apiKey = undefined;
|
|
231
|
+
this.jwt = undefined;
|
|
232
|
+
}
|
|
233
|
+
/** Which configured credential the interceptor will send (diagnostics/tests). */
|
|
234
|
+
credentialKind() {
|
|
235
|
+
if (this.serviceKey)
|
|
236
|
+
return "service_key";
|
|
237
|
+
if (this.apiKey)
|
|
238
|
+
return "api_key";
|
|
239
|
+
if (this.jwt)
|
|
240
|
+
return "jwt";
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
141
243
|
/**
|
|
142
244
|
* Start the device auth flow in the background if not already running.
|
|
143
245
|
* Called automatically when no credentials are found in the interceptor.
|
|
@@ -270,7 +372,12 @@ export class BackendClient {
|
|
|
270
372
|
`through Claude's connector settings, your session expired: in Claude's connector ` +
|
|
271
373
|
`settings, toggle OpenEphemeris off and on to reconnect — this only takes a moment.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
|
|
272
374
|
}
|
|
273
|
-
|
|
375
|
+
// A 422 whose body says the caller ran out of credits is the credit wall
|
|
376
|
+
// wearing a validation status (the Go visual add-on reservation returned
|
|
377
|
+
// 422 `credits_insufficient` before it moved to 402). Route it through
|
|
378
|
+
// the same 402 path so the assistant gets the one credit-wall message
|
|
379
|
+
// and top-up link, not "Validation error: ...".
|
|
380
|
+
if (status === 402 || (status === 422 && looksLikeCreditExhaustion(data, msg))) {
|
|
274
381
|
// PRICING SSOT: apps/web/config/billing-plans.ts — Explorer grant
|
|
275
382
|
// (EXPLORER.creditsNumeric = 150, one-time), Pro/`developer`
|
|
276
383
|
// (monthlyPrice = 29, credits = 75,000), and CREDIT_TOPUPS
|
|
@@ -289,18 +396,21 @@ export class BackendClient {
|
|
|
289
396
|
// One direct link, not a dashboard tab: /topup?pack=payg_5 signs the
|
|
290
397
|
// user in if needed and redirects straight to the prefilled Stripe
|
|
291
398
|
// Payment Link for the $5 → 150-credit pack.
|
|
399
|
+
const topupUrl = resolveTopupUrl(dataObj.upgrade?.topup_url);
|
|
292
400
|
let upsellMsg = "";
|
|
293
|
-
let actionUrl =
|
|
401
|
+
let actionUrl = topupUrl;
|
|
294
402
|
if (currentTier === 'explorer') {
|
|
295
403
|
upsellMsg =
|
|
296
|
-
`Let the user know warmly: Their 150 free credits are used up. ` +
|
|
297
|
-
`$5 gets 150 more — one tap: ${
|
|
404
|
+
`Let the user know warmly: Their 150 free credits are used up (they do not reset). ` +
|
|
405
|
+
`$5 gets 150 more — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
|
|
406
|
+
`(show this exact link to the user as a clickable link). ` +
|
|
298
407
|
`Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
|
|
299
408
|
}
|
|
300
409
|
else if (currentTier === 'payg') {
|
|
301
410
|
upsellMsg =
|
|
302
411
|
`Let the user know warmly: Their wallet balance is empty. ` +
|
|
303
|
-
`$5 gets 150 more credits — one tap: ${
|
|
412
|
+
`$5 gets 150 more credits — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
|
|
413
|
+
`(show this exact link to the user as a clickable link). ` +
|
|
304
414
|
`Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
|
|
305
415
|
}
|
|
306
416
|
else {
|
package/dist/index.js
CHANGED
|
@@ -10,14 +10,8 @@ import { buildUiMeta } from "./tools/ui-meta.js";
|
|
|
10
10
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
11
11
|
import { captureEvent, captureFirstToolCallOnce, distinctIdFor } from "./analytics.js";
|
|
12
12
|
import { listPrompts, getPromptContent } from "./prompts.js";
|
|
13
|
-
// ── MCP App
|
|
14
|
-
import {
|
|
15
|
-
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
16
|
-
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
17
|
-
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
18
|
-
import { TRANSIT_TIMELINE_RESOURCE_URI, TRANSIT_TIMELINE_MIME_TYPE, getTransitTimelineBundle, } from "./tools/apps/transit-timeline-app.js";
|
|
19
|
-
import { VEDIC_CHART_RESOURCE_URI, VEDIC_CHART_MIME_TYPE, getVedicChartBundle, } from "./tools/apps/vedic-chart-app.js";
|
|
20
|
-
import { BAZI_RESOURCE_URI, BAZI_MIME_TYPE, getBaziBundle, } from "./tools/apps/bazi-app.js";
|
|
13
|
+
// ── MCP App resources (list/read registry shared with server-sse.ts) ────────
|
|
14
|
+
import { listUiResources, readUiResource } from "./tools/apps/ui-resources.js";
|
|
21
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
16
|
function resolveServerVersion() {
|
|
23
17
|
try {
|
|
@@ -157,112 +151,16 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
157
151
|
],
|
|
158
152
|
};
|
|
159
153
|
});
|
|
160
|
-
// List available resources
|
|
161
154
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
uri: CHART_WHEEL_RESOURCE_URI,
|
|
166
|
-
name: "Chart Wheel Explorer",
|
|
167
|
-
description: "Interactive natal chart wheel with clickable planets, houses, and aspects.",
|
|
168
|
-
mimeType: CHART_WHEEL_MIME_TYPE,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (getBodygraphBundle()) {
|
|
172
|
-
resources.push({
|
|
173
|
-
uri: BODYGRAPH_RESOURCE_URI,
|
|
174
|
-
name: "Human Design Bodygraph Explorer",
|
|
175
|
-
description: "Interactive Human Design Bodygraph with clickable centers, gates, and channels.",
|
|
176
|
-
mimeType: BODYGRAPH_MIME_TYPE,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
if (getBiWheelBundle()) {
|
|
180
|
-
resources.push({
|
|
181
|
-
uri: BI_WHEEL_RESOURCE_URI,
|
|
182
|
-
name: "Bi-Wheel Explorer",
|
|
183
|
-
description: "Interactive synastry or transit bi-wheel with cross-aspect lines and clickable planets.",
|
|
184
|
-
mimeType: BI_WHEEL_MIME_TYPE,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
if (getMoonPhaseBundle()) {
|
|
188
|
-
resources.push({
|
|
189
|
-
uri: MOON_PHASE_RESOURCE_URI,
|
|
190
|
-
name: "Moon Phase Explorer",
|
|
191
|
-
description: "Interactive lunar phase dial showing illumination, sign, and void-of-course status.",
|
|
192
|
-
mimeType: MOON_PHASE_MIME_TYPE,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
if (getTransitTimelineBundle()) {
|
|
196
|
-
resources.push({
|
|
197
|
-
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
198
|
-
name: "Transit Timeline Explorer",
|
|
199
|
-
description: "Interactive vertical timeline of upcoming transit hits grouped by month, with clickable events.",
|
|
200
|
-
mimeType: TRANSIT_TIMELINE_MIME_TYPE,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
if (getVedicChartBundle()) {
|
|
204
|
-
resources.push({
|
|
205
|
-
uri: VEDIC_CHART_RESOURCE_URI,
|
|
206
|
-
name: "Vedic Chart Explorer",
|
|
207
|
-
description: "Interactive Vedic (Jyotish) South Indian Rashi grid with clickable sign placements.",
|
|
208
|
-
mimeType: VEDIC_CHART_MIME_TYPE,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
if (getBaziBundle()) {
|
|
212
|
-
resources.push({
|
|
213
|
-
uri: BAZI_RESOURCE_URI,
|
|
214
|
-
name: "BaZi Four Pillars Explorer",
|
|
215
|
-
description: "Interactive BaZi (Four Pillars of Destiny) chart with clickable Year/Month/Day/Hour pillars.",
|
|
216
|
-
mimeType: BAZI_MIME_TYPE,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
return { resources };
|
|
155
|
+
// One registry for both transports: versioned ui:// URIs + resource _meta
|
|
156
|
+
// (prefersBorder, CSP, widgetDescription). See tools/apps/ui-resources.ts.
|
|
157
|
+
return { resources: listUiResources() };
|
|
220
158
|
});
|
|
221
|
-
// Read resource
|
|
222
159
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
223
160
|
const uri = request.params.uri;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new Error("Chart Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
228
|
-
return { contents: [{ uri: CHART_WHEEL_RESOURCE_URI, mimeType: CHART_WHEEL_MIME_TYPE, text: bundle }] };
|
|
229
|
-
}
|
|
230
|
-
if (uri === BODYGRAPH_RESOURCE_URI) {
|
|
231
|
-
const bundle = getBodygraphBundle();
|
|
232
|
-
if (!bundle)
|
|
233
|
-
throw new Error("Bodygraph UI bundle not found. Run `npm run build:ui` to build it.");
|
|
234
|
-
return { contents: [{ uri: BODYGRAPH_RESOURCE_URI, mimeType: BODYGRAPH_MIME_TYPE, text: bundle }] };
|
|
235
|
-
}
|
|
236
|
-
if (uri === BI_WHEEL_RESOURCE_URI) {
|
|
237
|
-
const bundle = getBiWheelBundle();
|
|
238
|
-
if (!bundle)
|
|
239
|
-
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
240
|
-
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
241
|
-
}
|
|
242
|
-
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
243
|
-
const bundle = getMoonPhaseBundle();
|
|
244
|
-
if (!bundle)
|
|
245
|
-
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
246
|
-
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
247
|
-
}
|
|
248
|
-
if (uri === TRANSIT_TIMELINE_RESOURCE_URI) {
|
|
249
|
-
const bundle = getTransitTimelineBundle();
|
|
250
|
-
if (!bundle)
|
|
251
|
-
throw new Error("Transit Timeline UI bundle not found. Run `npm run build:ui` to build it.");
|
|
252
|
-
return { contents: [{ uri: TRANSIT_TIMELINE_RESOURCE_URI, mimeType: TRANSIT_TIMELINE_MIME_TYPE, text: bundle }] };
|
|
253
|
-
}
|
|
254
|
-
if (uri === VEDIC_CHART_RESOURCE_URI) {
|
|
255
|
-
const bundle = getVedicChartBundle();
|
|
256
|
-
if (!bundle)
|
|
257
|
-
throw new Error("Vedic Chart UI bundle not found. Run `npm run build:ui` to build it.");
|
|
258
|
-
return { contents: [{ uri: VEDIC_CHART_RESOURCE_URI, mimeType: VEDIC_CHART_MIME_TYPE, text: bundle }] };
|
|
259
|
-
}
|
|
260
|
-
if (uri === BAZI_RESOURCE_URI) {
|
|
261
|
-
const bundle = getBaziBundle();
|
|
262
|
-
if (!bundle)
|
|
263
|
-
throw new Error("BaZi UI bundle not found. Run `npm run build:ui` to build it.");
|
|
264
|
-
return { contents: [{ uri: BAZI_RESOURCE_URI, mimeType: BAZI_MIME_TYPE, text: bundle }] };
|
|
265
|
-
}
|
|
161
|
+
const result = readUiResource(uri);
|
|
162
|
+
if (result)
|
|
163
|
+
return result;
|
|
266
164
|
throw new Error(`Unknown resource: ${uri}`);
|
|
267
165
|
});
|
|
268
166
|
// Handle tool calls
|
package/dist/prompts.js
CHANGED
|
@@ -36,8 +36,8 @@ export const PROMPTS = [
|
|
|
36
36
|
"| `solar_return_year_ahead` | Annual Solar Return chart — themes for the coming birthday year |\n" +
|
|
37
37
|
"| `developer_api_integration` | Help a dev integrate the Open Ephemeris API into their app |\n\n" +
|
|
38
38
|
"## Before Calling Any Tool — Three Rules\n" +
|
|
39
|
-
"1. **Coordinates first**:
|
|
40
|
-
"
|
|
39
|
+
"1. **Coordinates first**: Resolve place names with `location_search` — never recall " +
|
|
40
|
+
"coordinates from memory. Positive = North/East; negative = South/West.\n" +
|
|
41
41
|
"2. **Datetime format**: Pass birth times as local ISO 8601 + separate `timezone` IANA name " +
|
|
42
42
|
"(e.g. `America/Chicago`), OR as a UTC-offset datetime `1990-04-15T14:30:00-05:00`. " +
|
|
43
43
|
"Never append `Z` to a local birth time — `Z` means UTC.\n" +
|
|
@@ -870,7 +870,7 @@ export const PROMPTS = [
|
|
|
870
870
|
"| Feature | MCP Tool | REST Endpoint | Credits |\n" +
|
|
871
871
|
"|---------|----------|---------------|---------|\n" +
|
|
872
872
|
"| Natal chart | `ephemeris_natal_chart` | `POST /ephemeris/natal-chart` | 1 |\n" +
|
|
873
|
-
"| Natal batch (up to 100) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
|
|
873
|
+
"| Natal batch (up to 100, Startup+) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
|
|
874
874
|
"| Vedic / Jyotish chart | `vedic_chart` | `POST /vedic/chart` | 1 |\n" +
|
|
875
875
|
"| Human Design | `human_design_chart` | `POST /human-design/chart` | 2 |\n" +
|
|
876
876
|
"| BaZi Four Pillars | `chinese_bazi` | `POST /chinese/bazi` | 1 |\n" +
|
|
@@ -879,38 +879,39 @@ export const PROMPTS = [
|
|
|
879
879
|
"| Midpoint composite | `ephemeris_composite_midpoint` | `POST /comparative/composite/midpoint` | 3 |\n" +
|
|
880
880
|
"| House overlay | `ephemeris_overlay` | `POST /comparative/overlay` | 3 |\n" +
|
|
881
881
|
"| Natal transits (now) | `ephemeris_natal_transits` | `POST /comparative/natal-transits` | 3 |\n" +
|
|
882
|
-
"| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` |
|
|
882
|
+
"| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` | 5–70 by span (≤1y 5 … ≤40y 70); tool +1 for the natal chart |\n" +
|
|
883
883
|
"| Relocation chart | `ephemeris_relocation` | `POST /ephemeris/relocation` | 1 |\n" +
|
|
884
884
|
"| Solar return | `ephemeris_solar_return` | `POST /predictive/returns/solar` | 5 |\n" +
|
|
885
885
|
"| Lunar return | `ephemeris_lunar_return` | `POST /predictive/returns/lunar` | 5 |\n" +
|
|
886
886
|
"| Progressions | `ephemeris_progressed_chart` | `POST /ephemeris/progressed` | 1 |\n" +
|
|
887
|
-
"| ACG power lines | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
|
|
888
|
-
"| ACG city hits | `acg_hits` | `POST /acg/hits` |
|
|
887
|
+
"| ACG power lines (Pro+) | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
|
|
888
|
+
"| ACG city hits (Pro+) | `acg_hits` | `POST /acg/hits` | 10 |\n" +
|
|
889
889
|
"| Eclipse finder | `ephemeris_next_eclipse` | `GET /eclipse/next-visible` | 1 |\n" +
|
|
890
|
-
"| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` |
|
|
891
|
-
"| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` |
|
|
892
|
-
"| Electional windows | `ephemeris_electional` | `GET /electional/find-window` | 5 |\n" +
|
|
893
|
-
"| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` |
|
|
890
|
+
"| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` + `/void-of-course` | 2 |\n" +
|
|
891
|
+
"| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` | 2 per occurrence |\n" +
|
|
892
|
+
"| Electional windows (Pro+) | `ephemeris_electional` | `GET /electional/find-window` | 5 / 8 / 12 for ≤30 / 60 / 120 days |\n" +
|
|
893
|
+
"| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` | 5 |\n" +
|
|
894
894
|
"| Dignities | `ephemeris_dignities` | `POST /ephemeris/dignities` | 1 |\n" +
|
|
895
895
|
"| Hermetic lots | `ephemeris_hermetic_lots` | `POST /ephemeris/hermetic-lots` | 1 |\n" +
|
|
896
896
|
"| Midpoints | `ephemeris_midpoints` | `POST /ephemeris/midpoints` | 1 |\n\n" +
|
|
897
897
|
"## Step 3 — Critical Implementation Patterns\n\n" +
|
|
898
898
|
"### ⚠️ Datetime Handling — Most Common Source of Bugs\n" +
|
|
899
899
|
"```\n" +
|
|
900
|
-
"# CORRECT —
|
|
900
|
+
"# CORRECT — local wall-clock time + IANA zone (server applies historical DST)\n" +
|
|
901
901
|
"POST /ephemeris/natal-chart\n" +
|
|
902
|
-
"{ \"
|
|
903
|
-
"
|
|
904
|
-
"{ \"
|
|
905
|
-
"# CORRECT — UTC required for HD and Vedic\n" +
|
|
902
|
+
"{ \"subject\": { \"name\": \"A\", \"birth_datetime\": { \"iso\": \"1990-04-15T14:30:00\" },\n" +
|
|
903
|
+
" \"birth_location\": { \"latitude\": { \"decimal\": 41.88 }, \"longitude\": { \"decimal\": -87.63 },\n" +
|
|
904
|
+
" \"timezone\": { \"iana_name\": \"America/Chicago\" } } } }\n\n" +
|
|
906
905
|
"POST /human-design/chart\n" +
|
|
907
|
-
"{ \"
|
|
908
|
-
"
|
|
909
|
-
"
|
|
906
|
+
"{ \"birth_datetime_local\": \"1990-04-15T14:30:00\", \"timezone\": { \"iana_name\": \"America/Chicago\" },\n" +
|
|
907
|
+
" \"latitude\": 41.88, \"longitude\": -87.63 }\n\n" +
|
|
908
|
+
"# ALSO CORRECT — an explicit instant (offset or Z): trusted as-is\n" +
|
|
909
|
+
"\"1990-04-15T14:30:00-05:00\" or \"1990-04-15T19:30:00Z\"\n\n" +
|
|
910
|
+
"# WRONG — Z on a local time reads as UTC 2:30pm, not Central 2:30pm (no error raised)\n" +
|
|
911
|
+
"\"1990-04-15T14:30:00Z\"\n" +
|
|
910
912
|
"```\n\n" +
|
|
911
|
-
"**Rule of thumb
|
|
912
|
-
"
|
|
913
|
-
"- Human Design, Vedic: always UTC with Z\n\n" +
|
|
913
|
+
"**Rule of thumb**: on every endpoint, prefer local time + IANA zone. A zone-less clock time " +
|
|
914
|
+
"with no zone is a hard 400; a bare date resolves to 12:00 UTC.\n\n" +
|
|
914
915
|
"### Coordinates\n" +
|
|
915
916
|
"- Decimal degrees, at least 4 decimal places\n" +
|
|
916
917
|
"- Positive = North/East; Negative = South/West\n" +
|
|
@@ -938,9 +939,10 @@ export const PROMPTS = [
|
|
|
938
939
|
"### Credit Optimization\n" +
|
|
939
940
|
"- **Batch natal charts**: use `POST /ephemeris/natal/batch` for multi-user onboarding (1 credit/subject vs N calls)\n" +
|
|
940
941
|
"- **Current transits**: prefer `ephemeris_natal_transits` (3 credits) over " +
|
|
941
|
-
"`ephemeris_transits` (6 credits) when you only need right-now, not a date range\n" +
|
|
942
|
-
"- **
|
|
943
|
-
"
|
|
942
|
+
"`ephemeris_transits` (6+ credits, priced by span) when you only need right-now, not a date range\n" +
|
|
943
|
+
"- **Range searches**: priced by the span you ask for, before compute; a span over the plan cap " +
|
|
944
|
+
"(transit search: Explorer/PAYG 1y, Pro 5y, Startup 10y, Scale 40y) is a 400 `search_span_limit` — split it\n" +
|
|
945
|
+
"- **Failed calls are free**: every 4xx/5xx response is refunded\n" +
|
|
944
946
|
"- **Format matters**: `format: 'llm'` reduces token usage in your LLM calls significantly\n\n" +
|
|
945
947
|
"## Step 4 — Generate a Code Scaffold\n" +
|
|
946
948
|
"Ask: 'What language would you like the scaffold in? And what is the first feature you want to implement?'\n\n" +
|