ether-to-astro 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/astro-service/natal-service.d.ts +6 -2
- package/dist/astro-service/natal-service.js +43 -4
- package/dist/astro-service/service-types.d.ts +18 -1
- package/dist/astro-service/shared.d.ts +8 -1
- package/dist/astro-service/shared.js +15 -1
- package/dist/astro-service/sign-boundary-service.d.ts +36 -0
- package/dist/astro-service/sign-boundary-service.js +156 -0
- package/dist/astro-service/sky-service.d.ts +1 -4
- package/dist/astro-service/sky-service.js +18 -14
- package/dist/astro-service/transit-service.js +7 -5
- package/dist/astro-service.d.ts +14 -3
- package/dist/astro-service.js +80 -10
- package/dist/cli.js +23 -0
- package/dist/entrypoint.d.ts +1 -0
- package/dist/entrypoint.js +13 -0
- package/dist/loader.js +2 -2
- package/dist/mcp-alias.js +2 -1
- package/dist/tool-registry.d.ts +1 -1
- package/dist/tool-registry.js +111 -8
- package/dist/tool-result.js +2 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.js +45 -0
- package/docs/releases/1.4.0.md +36 -0
- package/docs/releases/README.md +3 -2
- package/package.json +1 -1
- package/skills/.curated/daily-brief/SKILL.md +10 -6
- package/skills/.curated/weekly-overview/SKILL.md +9 -6
- package/src/astro-service/natal-service.ts +57 -4
- package/src/astro-service/service-types.ts +20 -1
- package/src/astro-service/shared.ts +23 -1
- package/src/astro-service/sign-boundary-service.ts +222 -0
- package/src/astro-service/sky-service.ts +21 -16
- package/src/astro-service/transit-service.ts +7 -4
- package/src/astro-service.ts +108 -11
- package/src/cli.ts +46 -0
- package/src/entrypoint.ts +17 -0
- package/src/loader.ts +2 -2
- package/src/mcp-alias.ts +2 -1
- package/src/tool-registry.ts +129 -9
- package/src/tool-result.ts +2 -1
- package/src/types.ts +72 -18
- package/tests/property/transits.property.test.ts +3 -13
- package/tests/unit/astro-service/natal-service.test.ts +16 -2
- package/tests/unit/astro-service/sign-boundary-service.test.ts +188 -0
- package/tests/unit/astro-service/sky-service.test.ts +8 -6
- package/tests/unit/astro-service/transit-service.test.ts +41 -0
- package/tests/unit/astro-service.test.ts +161 -8
- package/tests/unit/cli-commands.test.ts +1 -0
- package/tests/unit/entrypoint.test.ts +101 -2
- package/tests/unit/error-mapping.test.ts +7 -0
- package/tests/unit/tool-registry.test.ts +43 -1
- package/tests/validation/adapters/astrolog.ts +2 -14
package/README.md
CHANGED
|
@@ -295,6 +295,8 @@ Ask your AI agent:
|
|
|
295
295
|
## MCP Tools Available
|
|
296
296
|
|
|
297
297
|
### Setup
|
|
298
|
+
- `get_server_status` - Inspect loaded chart state and effective MCP session settings
|
|
299
|
+
- `set_preferences` - Update process-local MCP runtime preferences such as reporting timezone and preferred house style
|
|
298
300
|
- `set_natal_chart` - Store birth chart data
|
|
299
301
|
|
|
300
302
|
### Transits
|
|
@@ -304,11 +306,12 @@ Ask your AI agent:
|
|
|
304
306
|
- `forecast`: day-grouped transit output across the selected date window
|
|
305
307
|
- if `mode` is omitted, legacy behavior is preserved: `days_ahead=0` resolves to `snapshot`, and `days_ahead>0` resolves to `best_hit`
|
|
306
308
|
- each transit now includes additive placement metadata for both sides: sign, degree, and house
|
|
307
|
-
- with `include_mundane=true`, output includes deterministic mundane positions plus `mundane.aspects` and non-narrative `mundane.weather` grouping metadata
|
|
309
|
+
- with `include_mundane=true`, output includes deterministic mundane positions normalized with the same sign-boundary policy as serialized transits, plus `mundane.aspects` and non-narrative `mundane.weather` grouping metadata
|
|
308
310
|
- when `include_mundane=true` and `mode=forecast`, output includes `mundane.days[]` with per-day grouped mundane aspects/weather
|
|
309
311
|
|
|
310
312
|
### Electional
|
|
311
313
|
- `get_electional_context` - Stateless electional context for a local date, time, and location. Returns deterministic ascendant, sect/day-night classification, Moon phase, applying aspects, and optional ASC-ruler basics without requiring a natal chart.
|
|
314
|
+
- `get_sign_boundary_events` - Stateless exact sign-boundary crossings for supported planets across a local date window, with both `from_sign` and `to_sign` so ingress and egress are represented as one event.
|
|
312
315
|
|
|
313
316
|
### Advanced Tools
|
|
314
317
|
- `get_houses` - House cusps, Ascendant, Midheaven (Placidus, Koch, Whole Sign, Equal)
|
|
@@ -324,6 +327,7 @@ Ask your AI agent:
|
|
|
324
327
|
## CLI Commands Available
|
|
325
328
|
|
|
326
329
|
- `set-natal-chart`
|
|
330
|
+
- `get-sign-boundary-events`
|
|
327
331
|
- `get-transits`
|
|
328
332
|
- `get-houses`
|
|
329
333
|
- `get-retrograde-planets`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { McpStartupDefaults } from '../entrypoint.js';
|
|
2
2
|
import type { EphemerisCalculator } from '../ephemeris.js';
|
|
3
3
|
import type { HouseCalculator } from '../houses.js';
|
|
4
|
-
import { type NatalChart } from '../types.js';
|
|
4
|
+
import { type HouseSystem, type NatalChart } from '../types.js';
|
|
5
5
|
import type { GetHousesInput, ServiceResult, SetNatalChartInput } from './service-types.js';
|
|
6
6
|
interface NatalServiceDependencies {
|
|
7
7
|
ephem: EphemerisCalculator;
|
|
@@ -9,6 +9,10 @@ interface NatalServiceDependencies {
|
|
|
9
9
|
mcpStartupDefaults: Readonly<McpStartupDefaults>;
|
|
10
10
|
isInitialized: () => boolean;
|
|
11
11
|
}
|
|
12
|
+
interface RuntimePreferenceSnapshot {
|
|
13
|
+
preferredTimezone?: string;
|
|
14
|
+
preferredHouseStyle?: HouseSystem;
|
|
15
|
+
}
|
|
12
16
|
/**
|
|
13
17
|
* Internal natal/chart-state workflow used by `AstroService`.
|
|
14
18
|
*
|
|
@@ -36,6 +40,6 @@ export declare class NatalService {
|
|
|
36
40
|
/**
|
|
37
41
|
* Summarize process-local server state and configured startup defaults.
|
|
38
42
|
*/
|
|
39
|
-
getServerStatus(natalChart: NatalChart | null): ServiceResult<Record<string, unknown>>;
|
|
43
|
+
getServerStatus(natalChart: NatalChart | null, runtimePreferences?: RuntimePreferenceSnapshot): ServiceResult<Record<string, unknown>>;
|
|
40
44
|
}
|
|
41
45
|
export {};
|
|
@@ -157,23 +157,62 @@ export class NatalService {
|
|
|
157
157
|
/**
|
|
158
158
|
* Summarize process-local server state and configured startup defaults.
|
|
159
159
|
*/
|
|
160
|
-
getServerStatus(natalChart) {
|
|
160
|
+
getServerStatus(natalChart, runtimePreferences = {}) {
|
|
161
|
+
const reportingTimezone = runtimePreferences.preferredTimezone ??
|
|
162
|
+
this.mcpStartupDefaults.preferredTimezone ??
|
|
163
|
+
natalChart?.location.timezone ??
|
|
164
|
+
'UTC';
|
|
165
|
+
const reportingTimezoneSource = runtimePreferences.preferredTimezone !== undefined
|
|
166
|
+
? 'runtime'
|
|
167
|
+
: this.mcpStartupDefaults.preferredTimezone !== undefined
|
|
168
|
+
? 'startup'
|
|
169
|
+
: natalChart?.location.timezone
|
|
170
|
+
? 'natal'
|
|
171
|
+
: 'fallback';
|
|
172
|
+
const preferredHouseStyle = runtimePreferences.preferredHouseStyle ??
|
|
173
|
+
natalChart?.requestedHouseSystem ??
|
|
174
|
+
this.mcpStartupDefaults.preferredHouseStyle ??
|
|
175
|
+
natalChart?.houseSystem ??
|
|
176
|
+
'P';
|
|
177
|
+
const preferredHouseStyleSource = runtimePreferences.preferredHouseStyle !== undefined
|
|
178
|
+
? 'runtime'
|
|
179
|
+
: natalChart?.requestedHouseSystem !== undefined
|
|
180
|
+
? 'chart_requested'
|
|
181
|
+
: this.mcpStartupDefaults.preferredHouseStyle !== undefined
|
|
182
|
+
? 'startup'
|
|
183
|
+
: natalChart?.houseSystem !== undefined
|
|
184
|
+
? 'chart_resolved'
|
|
185
|
+
: 'fallback';
|
|
161
186
|
const statusData = {
|
|
162
187
|
serverVersion: '1.0.0',
|
|
163
188
|
hasNatalChart: natalChart !== null,
|
|
164
189
|
natalChartName: natalChart?.name ?? null,
|
|
165
190
|
natalChartTimezone: natalChart?.location.timezone ?? null,
|
|
191
|
+
natalChartRequestedHouseSystem: natalChart?.requestedHouseSystem ?? null,
|
|
192
|
+
natalChartResolvedHouseSystem: natalChart?.houseSystem ?? null,
|
|
166
193
|
startupDefaults: {
|
|
167
194
|
preferredTimezone: this.mcpStartupDefaults.preferredTimezone ?? null,
|
|
168
195
|
preferredHouseStyle: this.mcpStartupDefaults.preferredHouseStyle ?? null,
|
|
169
196
|
weekdayLabels: this.mcpStartupDefaults.weekdayLabels ?? null,
|
|
170
197
|
},
|
|
198
|
+
runtimePreferences: {
|
|
199
|
+
preferredTimezone: runtimePreferences.preferredTimezone ?? null,
|
|
200
|
+
preferredHouseStyle: runtimePreferences.preferredHouseStyle ?? null,
|
|
201
|
+
},
|
|
202
|
+
effectiveSettings: {
|
|
203
|
+
reportingTimezone,
|
|
204
|
+
reportingTimezoneSource,
|
|
205
|
+
preferredHouseStyle,
|
|
206
|
+
preferredHouseStyleSource,
|
|
207
|
+
},
|
|
171
208
|
ephemerisInitialized: this.isInitialized(),
|
|
172
209
|
stateModel: 'stateful-per-process',
|
|
173
210
|
};
|
|
174
|
-
const
|
|
175
|
-
? `
|
|
176
|
-
: '
|
|
211
|
+
const chartText = natalChart
|
|
212
|
+
? `Natal chart loaded: ${natalChart.name} (${natalChart.location.timezone})`
|
|
213
|
+
: 'No natal chart loaded';
|
|
214
|
+
const humanText = `Server ready. Reporting timezone: ${reportingTimezone} (${reportingTimezoneSource}). ` +
|
|
215
|
+
`House style: ${preferredHouseStyle} (${preferredHouseStyleSource}). ${chartText}.`;
|
|
177
216
|
return { data: statusData, text: humanText };
|
|
178
217
|
}
|
|
179
218
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Disambiguation } from '../time-utils.js';
|
|
2
|
-
import type { ElectionalHouseSystem, HouseSystem } from '../types.js';
|
|
2
|
+
import type { ElectionalHouseSystem, HouseSystem, SignBoundaryBody } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Public input type for building and caching the shared natal chart payload.
|
|
5
5
|
*/
|
|
@@ -21,6 +21,7 @@ export interface SetNatalChartInput {
|
|
|
21
21
|
*/
|
|
22
22
|
export interface GetTransitsInput {
|
|
23
23
|
date?: string;
|
|
24
|
+
timezone?: string;
|
|
24
25
|
categories?: string[];
|
|
25
26
|
include_mundane?: boolean;
|
|
26
27
|
days_ahead?: number;
|
|
@@ -59,6 +60,15 @@ export interface GetRisingSignWindowsInput {
|
|
|
59
60
|
timezone: string;
|
|
60
61
|
mode?: 'approximate' | 'exact';
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Public input type for stateless sign-boundary event lookup.
|
|
65
|
+
*/
|
|
66
|
+
export interface GetSignBoundaryEventsInput {
|
|
67
|
+
date?: string;
|
|
68
|
+
timezone?: string;
|
|
69
|
+
days_ahead?: number;
|
|
70
|
+
bodies?: SignBoundaryBody[];
|
|
71
|
+
}
|
|
62
72
|
/**
|
|
63
73
|
* Public output-wrapper shared by service methods that return data plus text.
|
|
64
74
|
*/
|
|
@@ -66,6 +76,13 @@ export interface ServiceResult<T> {
|
|
|
66
76
|
data: T;
|
|
67
77
|
text: string;
|
|
68
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Public input type for updating process-local MCP runtime preferences.
|
|
81
|
+
*/
|
|
82
|
+
export interface SetPreferencesInput {
|
|
83
|
+
preferred_timezone?: string | null;
|
|
84
|
+
preferred_house_style?: HouseSystem | null;
|
|
85
|
+
}
|
|
69
86
|
/**
|
|
70
87
|
* Public input type for chart rendering methods.
|
|
71
88
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { McpStartupDefaults } from '../entrypoint.js';
|
|
2
|
-
import { type HouseData, type HouseSystem, type NatalChart } from '../types.js';
|
|
2
|
+
import { type HouseData, type HouseSystem, type NatalChart, type PlanetPosition } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Normalize any longitude into the standard 0-360 range.
|
|
5
5
|
*
|
|
@@ -21,6 +21,13 @@ export declare function getSignAndDegree(longitude: number): {
|
|
|
21
21
|
sign: string;
|
|
22
22
|
degree: number;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a serialized planet placement to the shared sign-boundary policy.
|
|
26
|
+
*
|
|
27
|
+
* @param position - Planet position to normalize for response output
|
|
28
|
+
* @returns Copy of the position with sign/degree derived from shared boundary handling
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizePlanetPlacement(position: PlanetPosition): PlanetPosition;
|
|
24
31
|
/**
|
|
25
32
|
* Map a longitude to its house number for a resolved house table.
|
|
26
33
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ZODIAC_SIGNS } from '../types.js';
|
|
1
|
+
import { ZODIAC_SIGNS, } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Normalize any longitude into the standard 0-360 range.
|
|
4
4
|
*
|
|
@@ -31,6 +31,20 @@ export function getSignAndDegree(longitude) {
|
|
|
31
31
|
degree: shouldCarryToNextSign ? 0 : roundedDegree,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a serialized planet placement to the shared sign-boundary policy.
|
|
36
|
+
*
|
|
37
|
+
* @param position - Planet position to normalize for response output
|
|
38
|
+
* @returns Copy of the position with sign/degree derived from shared boundary handling
|
|
39
|
+
*/
|
|
40
|
+
export function normalizePlanetPlacement(position) {
|
|
41
|
+
const placement = getSignAndDegree(position.longitude);
|
|
42
|
+
return {
|
|
43
|
+
...position,
|
|
44
|
+
sign: placement.sign,
|
|
45
|
+
degree: placement.degree,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
34
48
|
/**
|
|
35
49
|
* Map a longitude to its house number for a resolved house table.
|
|
36
50
|
*
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EphemerisCalculator } from '../ephemeris.js';
|
|
2
|
+
import type { GetSignBoundaryEventsInput, ServiceResult } from './service-types.js';
|
|
3
|
+
interface SignBoundaryServiceDependencies {
|
|
4
|
+
ephem: EphemerisCalculator;
|
|
5
|
+
now: () => Date;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Internal stateless sign-boundary event scanner used by `AstroService`.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* This service owns local-day window resolution plus exact root lookup for
|
|
12
|
+
* planets crossing zodiac sign boundaries. It returns reusable structured
|
|
13
|
+
* events rather than question-shaped ingress/egress prose.
|
|
14
|
+
*/
|
|
15
|
+
export declare class SignBoundaryService {
|
|
16
|
+
private readonly ephem;
|
|
17
|
+
private readonly now;
|
|
18
|
+
constructor(deps: SignBoundaryServiceDependencies);
|
|
19
|
+
/**
|
|
20
|
+
* Return sign-boundary crossing events across a local calendar window.
|
|
21
|
+
*/
|
|
22
|
+
getSignBoundaryEvents(input: GetSignBoundaryEventsInput & {
|
|
23
|
+
timezone: string;
|
|
24
|
+
}): ServiceResult<Record<string, unknown>>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the local-midnight window start for the requested date.
|
|
27
|
+
*/
|
|
28
|
+
private resolveWindowStart;
|
|
29
|
+
/**
|
|
30
|
+
* Format a local date for stable response metadata.
|
|
31
|
+
*/
|
|
32
|
+
private formatDateLabel;
|
|
33
|
+
private classifyCrossing;
|
|
34
|
+
private signedBoundaryOffset;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { addLocalDays, localToUTC, utcToLocal } from '../time-utils.js';
|
|
2
|
+
import { PLANET_IDS_BY_NAME, SIGN_BOUNDARY_BODIES, ZODIAC_SIGNS, } from '../types.js';
|
|
3
|
+
import { parseDateOnlyInput } from './date-input.js';
|
|
4
|
+
import { normalizeLongitude } from './shared.js';
|
|
5
|
+
const ROOT_CLASSIFICATION_SAMPLE_DAYS = 1 / 24;
|
|
6
|
+
/**
|
|
7
|
+
* Internal stateless sign-boundary event scanner used by `AstroService`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* This service owns local-day window resolution plus exact root lookup for
|
|
11
|
+
* planets crossing zodiac sign boundaries. It returns reusable structured
|
|
12
|
+
* events rather than question-shaped ingress/egress prose.
|
|
13
|
+
*/
|
|
14
|
+
export class SignBoundaryService {
|
|
15
|
+
ephem;
|
|
16
|
+
now;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.ephem = deps.ephem;
|
|
19
|
+
this.now = deps.now;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Return sign-boundary crossing events across a local calendar window.
|
|
23
|
+
*/
|
|
24
|
+
getSignBoundaryEvents(input) {
|
|
25
|
+
const daysAhead = input.days_ahead ?? 0;
|
|
26
|
+
if (!Number.isFinite(daysAhead) || daysAhead < 0) {
|
|
27
|
+
throw new Error('days_ahead must be a finite number >= 0');
|
|
28
|
+
}
|
|
29
|
+
const requestedBodies = input.bodies ?? SIGN_BOUNDARY_BODIES;
|
|
30
|
+
for (const body of requestedBodies) {
|
|
31
|
+
if (!SIGN_BOUNDARY_BODIES.includes(body)) {
|
|
32
|
+
throw new Error(`Invalid body: ${body} (must be one of ${SIGN_BOUNDARY_BODIES.join(', ')})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const windowStart = this.resolveWindowStart(input.date, input.timezone);
|
|
36
|
+
const windowEnd = addLocalDays(utcToLocal(windowStart, input.timezone), input.timezone, daysAhead + 1);
|
|
37
|
+
const startJD = this.ephem.dateToJulianDay(windowStart);
|
|
38
|
+
const endJD = this.ephem.dateToJulianDay(windowEnd);
|
|
39
|
+
const events = [];
|
|
40
|
+
const seenKeys = new Set();
|
|
41
|
+
for (const body of requestedBodies) {
|
|
42
|
+
const planetId = PLANET_IDS_BY_NAME[body];
|
|
43
|
+
for (let signIndex = 0; signIndex < ZODIAC_SIGNS.length; signIndex++) {
|
|
44
|
+
const boundaryLongitude = signIndex * 30;
|
|
45
|
+
const roots = this.ephem.findExactTransitTimes(planetId, boundaryLongitude, startJD, endJD);
|
|
46
|
+
for (const root of roots) {
|
|
47
|
+
if (root >= endJD) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const crossing = this.classifyCrossing(planetId, root, boundaryLongitude);
|
|
51
|
+
if (!crossing) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const eventDate = this.ephem.julianDayToDate(root);
|
|
55
|
+
const position = this.ephem.getPlanetPosition(planetId, root);
|
|
56
|
+
const normalizedLongitude = normalizeLongitude(position.longitude);
|
|
57
|
+
const event = {
|
|
58
|
+
body,
|
|
59
|
+
from_sign: crossing.fromSign,
|
|
60
|
+
to_sign: crossing.toSign,
|
|
61
|
+
exact_time: eventDate.toISOString(),
|
|
62
|
+
longitude: Number.parseFloat(normalizedLongitude.toFixed(6)),
|
|
63
|
+
direction: crossing.direction,
|
|
64
|
+
};
|
|
65
|
+
const dedupeKey = `${event.body}:${event.exact_time}:${event.to_sign}:${event.from_sign}`;
|
|
66
|
+
if (!seenKeys.has(dedupeKey)) {
|
|
67
|
+
seenKeys.add(dedupeKey);
|
|
68
|
+
events.push(event);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
events.sort((left, right) => {
|
|
74
|
+
const timeOrder = left.exact_time.localeCompare(right.exact_time);
|
|
75
|
+
if (timeOrder !== 0) {
|
|
76
|
+
return timeOrder;
|
|
77
|
+
}
|
|
78
|
+
return left.body.localeCompare(right.body);
|
|
79
|
+
});
|
|
80
|
+
const startLocal = utcToLocal(windowStart, input.timezone);
|
|
81
|
+
const response = {
|
|
82
|
+
date: this.formatDateLabel(startLocal),
|
|
83
|
+
timezone: input.timezone,
|
|
84
|
+
calculation_timezone: input.timezone,
|
|
85
|
+
reporting_timezone: input.timezone,
|
|
86
|
+
days_ahead: daysAhead,
|
|
87
|
+
events,
|
|
88
|
+
};
|
|
89
|
+
const rangeLabel = daysAhead > 0 ? ` (next ${daysAhead + 1} days)` : '';
|
|
90
|
+
const humanText = events.length === 0
|
|
91
|
+
? `No sign-boundary events found${rangeLabel}.`
|
|
92
|
+
: `Sign-boundary events${rangeLabel}:\n\n${events
|
|
93
|
+
.map((event) => `${event.body}: ${event.from_sign} -> ${event.to_sign} at ${event.exact_time} (${event.direction})`)
|
|
94
|
+
.join('\n')}`;
|
|
95
|
+
return {
|
|
96
|
+
data: response,
|
|
97
|
+
text: humanText,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the local-midnight window start for the requested date.
|
|
102
|
+
*/
|
|
103
|
+
resolveWindowStart(date, timezone) {
|
|
104
|
+
if (date) {
|
|
105
|
+
const parsed = parseDateOnlyInput(date);
|
|
106
|
+
return localToUTC({ ...parsed, hour: 0, minute: 0, second: 0 }, timezone);
|
|
107
|
+
}
|
|
108
|
+
const localNow = utcToLocal(this.now(), timezone);
|
|
109
|
+
return localToUTC({
|
|
110
|
+
year: localNow.year,
|
|
111
|
+
month: localNow.month,
|
|
112
|
+
day: localNow.day,
|
|
113
|
+
hour: 0,
|
|
114
|
+
minute: 0,
|
|
115
|
+
second: 0,
|
|
116
|
+
}, timezone);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Format a local date for stable response metadata.
|
|
120
|
+
*/
|
|
121
|
+
formatDateLabel(localDate) {
|
|
122
|
+
return `${localDate.year}-${String(localDate.month).padStart(2, '0')}-${String(localDate.day).padStart(2, '0')}`;
|
|
123
|
+
}
|
|
124
|
+
classifyCrossing(planetId, root, boundaryLongitude) {
|
|
125
|
+
const before = this.signedBoundaryOffset(planetId, root - ROOT_CLASSIFICATION_SAMPLE_DAYS, boundaryLongitude);
|
|
126
|
+
const after = this.signedBoundaryOffset(planetId, root + ROOT_CLASSIFICATION_SAMPLE_DAYS, boundaryLongitude);
|
|
127
|
+
if (before === 0 || after === 0 || before === after) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const toSignIndex = Math.floor(boundaryLongitude / 30) % ZODIAC_SIGNS.length;
|
|
131
|
+
const fromSignIndex = (toSignIndex - 1 + ZODIAC_SIGNS.length) % ZODIAC_SIGNS.length;
|
|
132
|
+
return before < after
|
|
133
|
+
? {
|
|
134
|
+
fromSign: ZODIAC_SIGNS[fromSignIndex],
|
|
135
|
+
toSign: ZODIAC_SIGNS[toSignIndex],
|
|
136
|
+
direction: 'direct',
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
fromSign: ZODIAC_SIGNS[toSignIndex],
|
|
140
|
+
toSign: ZODIAC_SIGNS[fromSignIndex],
|
|
141
|
+
direction: 'retrograde',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
signedBoundaryOffset(planetId, jd, boundaryLongitude) {
|
|
145
|
+
const longitude = this.ephem.getPlanetPosition(planetId, jd).longitude;
|
|
146
|
+
let diff = normalizeLongitude(longitude) - normalizeLongitude(boundaryLongitude);
|
|
147
|
+
if (diff > 180)
|
|
148
|
+
diff -= 360;
|
|
149
|
+
if (diff < -180)
|
|
150
|
+
diff += 360;
|
|
151
|
+
if (Math.abs(diff) < 1e-6) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
return diff > 0 ? 1 : -1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EclipseCalculator } from '../eclipses.js';
|
|
2
|
-
import type { McpStartupDefaults } from '../entrypoint.js';
|
|
3
2
|
import type { EphemerisCalculator } from '../ephemeris.js';
|
|
4
3
|
import type { RiseSetCalculator } from '../riseset.js';
|
|
5
4
|
import { type NatalChart } from '../types.js';
|
|
@@ -8,7 +7,6 @@ interface SkyServiceDependencies {
|
|
|
8
7
|
ephem: EphemerisCalculator;
|
|
9
8
|
riseSetCalc: RiseSetCalculator;
|
|
10
9
|
eclipseCalc: EclipseCalculator;
|
|
11
|
-
mcpStartupDefaults: Readonly<McpStartupDefaults>;
|
|
12
10
|
now: () => Date;
|
|
13
11
|
formatTimestamp: (date: Date, timezone: string) => string;
|
|
14
12
|
}
|
|
@@ -23,7 +21,6 @@ export declare class SkyService {
|
|
|
23
21
|
private readonly ephem;
|
|
24
22
|
private readonly riseSetCalc;
|
|
25
23
|
private readonly eclipseCalc;
|
|
26
|
-
private readonly mcpStartupDefaults;
|
|
27
24
|
private readonly now;
|
|
28
25
|
private readonly formatTimestamp;
|
|
29
26
|
constructor(deps: SkyServiceDependencies);
|
|
@@ -34,7 +31,7 @@ export declare class SkyService {
|
|
|
34
31
|
/**
|
|
35
32
|
* Return the next rise and set events after the local day anchor for the chart location.
|
|
36
33
|
*/
|
|
37
|
-
getRiseSetTimes(natalChart: NatalChart): Promise<ServiceResult<Record<string, unknown>>>;
|
|
34
|
+
getRiseSetTimes(natalChart: NatalChart, reportingTimezone: string): Promise<ServiceResult<Record<string, unknown>>>;
|
|
38
35
|
/**
|
|
39
36
|
* Return current asteroid and node positions for the requested reporting timezone.
|
|
40
37
|
*/
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { localToUTC, utcToLocal } from '../time-utils.js';
|
|
2
2
|
import { ASTEROIDS, NODES, PLANETS } from '../types.js';
|
|
3
|
-
import { resolveReportingTimezone } from './shared.js';
|
|
4
3
|
/**
|
|
5
4
|
* Internal current-sky and runtime lookup workflow used by `AstroService`.
|
|
6
5
|
*
|
|
@@ -12,14 +11,12 @@ export class SkyService {
|
|
|
12
11
|
ephem;
|
|
13
12
|
riseSetCalc;
|
|
14
13
|
eclipseCalc;
|
|
15
|
-
mcpStartupDefaults;
|
|
16
14
|
now;
|
|
17
15
|
formatTimestamp;
|
|
18
16
|
constructor(deps) {
|
|
19
17
|
this.ephem = deps.ephem;
|
|
20
18
|
this.riseSetCalc = deps.riseSetCalc;
|
|
21
19
|
this.eclipseCalc = deps.eclipseCalc;
|
|
22
|
-
this.mcpStartupDefaults = deps.mcpStartupDefaults;
|
|
23
20
|
this.now = deps.now;
|
|
24
21
|
this.formatTimestamp = deps.formatTimestamp;
|
|
25
22
|
}
|
|
@@ -27,7 +24,7 @@ export class SkyService {
|
|
|
27
24
|
* Return the currently retrograde planets for the requested reporting timezone.
|
|
28
25
|
*/
|
|
29
26
|
getRetrogradePlanets(timezone) {
|
|
30
|
-
const resolvedTimezone =
|
|
27
|
+
const resolvedTimezone = timezone ?? 'UTC';
|
|
31
28
|
const now = this.now();
|
|
32
29
|
const jd = this.ephem.dateToJulianDay(now);
|
|
33
30
|
const positions = this.ephem.getAllPlanets(jd, Object.values(PLANETS));
|
|
@@ -35,6 +32,7 @@ export class SkyService {
|
|
|
35
32
|
const structuredData = {
|
|
36
33
|
date: this.getDateLabel(now, resolvedTimezone),
|
|
37
34
|
timezone: resolvedTimezone,
|
|
35
|
+
reporting_timezone: resolvedTimezone,
|
|
38
36
|
planets: retrograde,
|
|
39
37
|
};
|
|
40
38
|
const humanText = retrograde.length === 0
|
|
@@ -45,11 +43,10 @@ export class SkyService {
|
|
|
45
43
|
/**
|
|
46
44
|
* Return the next rise and set events after the local day anchor for the chart location.
|
|
47
45
|
*/
|
|
48
|
-
async getRiseSetTimes(natalChart) {
|
|
49
|
-
const
|
|
50
|
-
const reportingTimezone = this.mcpStartupDefaults.preferredTimezone || timezone;
|
|
46
|
+
async getRiseSetTimes(natalChart, reportingTimezone) {
|
|
47
|
+
const calculationTimezone = natalChart.location.timezone;
|
|
51
48
|
const now = this.now();
|
|
52
|
-
const localNow = utcToLocal(now,
|
|
49
|
+
const localNow = utcToLocal(now, calculationTimezone);
|
|
53
50
|
const localMidnight = {
|
|
54
51
|
year: localNow.year,
|
|
55
52
|
month: localNow.month,
|
|
@@ -58,11 +55,13 @@ export class SkyService {
|
|
|
58
55
|
minute: 0,
|
|
59
56
|
second: 0,
|
|
60
57
|
};
|
|
61
|
-
const midnightUTC = localToUTC(localMidnight,
|
|
58
|
+
const midnightUTC = localToUTC(localMidnight, calculationTimezone);
|
|
62
59
|
const results = await this.riseSetCalc.getAllRiseSet(midnightUTC, natalChart.location.latitude, natalChart.location.longitude);
|
|
63
60
|
const structuredData = {
|
|
64
|
-
date: this.getDateLabel(now,
|
|
65
|
-
timezone,
|
|
61
|
+
date: this.getDateLabel(now, calculationTimezone),
|
|
62
|
+
timezone: calculationTimezone,
|
|
63
|
+
calculation_timezone: calculationTimezone,
|
|
64
|
+
reporting_timezone: reportingTimezone,
|
|
66
65
|
times: results.map((result) => ({
|
|
67
66
|
planet: result.planet,
|
|
68
67
|
rise: result.rise?.toISOString() ?? null,
|
|
@@ -85,13 +84,14 @@ export class SkyService {
|
|
|
85
84
|
* Return current asteroid and node positions for the requested reporting timezone.
|
|
86
85
|
*/
|
|
87
86
|
getAsteroidPositions(timezone) {
|
|
88
|
-
const resolvedTimezone =
|
|
87
|
+
const resolvedTimezone = timezone ?? 'UTC';
|
|
89
88
|
const now = this.now();
|
|
90
89
|
const jd = this.ephem.dateToJulianDay(now);
|
|
91
90
|
const positions = this.ephem.getAllPlanets(jd, [...ASTEROIDS, ...NODES]);
|
|
92
91
|
const structuredData = {
|
|
93
92
|
date: this.getDateLabel(now, resolvedTimezone),
|
|
94
93
|
timezone: resolvedTimezone,
|
|
94
|
+
reporting_timezone: resolvedTimezone,
|
|
95
95
|
positions,
|
|
96
96
|
};
|
|
97
97
|
const humanText = `Asteroid & Node Positions:\n\n${positions
|
|
@@ -109,7 +109,7 @@ export class SkyService {
|
|
|
109
109
|
* Look up the next solar and lunar eclipses after the current instant.
|
|
110
110
|
*/
|
|
111
111
|
getNextEclipses(timezone) {
|
|
112
|
-
const resolvedTimezone =
|
|
112
|
+
const resolvedTimezone = timezone ?? 'UTC';
|
|
113
113
|
const jd = this.ephem.dateToJulianDay(this.now());
|
|
114
114
|
const solarEclipse = this.eclipseCalc.findNextSolarEclipse(jd);
|
|
115
115
|
const lunarEclipse = this.eclipseCalc.findNextLunarEclipse(jd);
|
|
@@ -131,7 +131,11 @@ export class SkyService {
|
|
|
131
131
|
});
|
|
132
132
|
humanLines.push(`Next Lunar Eclipse: ${this.formatTimestamp(lunarEclipse.maxTime, resolvedTimezone)} (${lunarEclipse.eclipseType})`);
|
|
133
133
|
}
|
|
134
|
-
const structuredData = {
|
|
134
|
+
const structuredData = {
|
|
135
|
+
timezone: resolvedTimezone,
|
|
136
|
+
reporting_timezone: resolvedTimezone,
|
|
137
|
+
eclipses,
|
|
138
|
+
};
|
|
135
139
|
const humanText = eclipses.length === 0
|
|
136
140
|
? 'No eclipses found in the near future.'
|
|
137
141
|
: `Upcoming Eclipses:\n\n${humanLines.join('\n')}`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { addLocalDays, localToUTC, utcToLocal } from '../time-utils.js';
|
|
2
2
|
import { deduplicateTransits } from '../transits.js';
|
|
3
|
-
import { ASPECTS, OUTER_PLANETS, PERSONAL_PLANETS,
|
|
3
|
+
import { ASPECTS, OUTER_PLANETS, PERSONAL_PLANETS, PLANET_IDS_BY_NAME, PLANETS, } from '../types.js';
|
|
4
4
|
import { parseDateOnlyInput } from './date-input.js';
|
|
5
|
-
import { getHouseNumber, getSignAndDegree, resolveHouseSystem, resolveTimezones, } from './shared.js';
|
|
5
|
+
import { getHouseNumber, getSignAndDegree, normalizePlanetPlacement, resolveHouseSystem, resolveTimezones, } from './shared.js';
|
|
6
6
|
/**
|
|
7
7
|
* Internal transit workflow service used by `AstroService`.
|
|
8
8
|
*
|
|
@@ -56,7 +56,7 @@ export class TransitService {
|
|
|
56
56
|
const mode = requestedMode ?? (daysAhead === 0 ? 'snapshot' : 'best_hit');
|
|
57
57
|
const modeSource = requestedMode === undefined ? 'legacy_default' : 'explicit';
|
|
58
58
|
const transitingPlanetIds = this.resolveTransitingPlanetIds(categories);
|
|
59
|
-
const { calculationTimezone, reportingTimezone } = resolveTimezones(this.mcpStartupDefaults,
|
|
59
|
+
const { calculationTimezone, reportingTimezone } = resolveTimezones(this.mcpStartupDefaults, input.timezone, natalChart.location.timezone);
|
|
60
60
|
const targetDate = this.resolveTargetDate(dateStr, calculationTimezone);
|
|
61
61
|
const allTransits = [];
|
|
62
62
|
const transitsByDay = new Map();
|
|
@@ -88,7 +88,7 @@ export class TransitService {
|
|
|
88
88
|
const chartHouseSystem = resolveHouseSystem(natalChart, this.mcpStartupDefaults);
|
|
89
89
|
const natalHouses = this.houseCalc.calculateHouses(natalChart.julianDay, natalChart.location.latitude, natalChart.location.longitude, chartHouseSystem);
|
|
90
90
|
const transitHouseCache = new Map();
|
|
91
|
-
const planetIdsByName = new Map(Object.entries(
|
|
91
|
+
const planetIdsByName = new Map(Object.entries(PLANET_IDS_BY_NAME).map(([planetName, planetId]) => [planetName, planetId]));
|
|
92
92
|
const getTransitHouses = (julianDay) => {
|
|
93
93
|
const cached = transitHouseCache.get(julianDay);
|
|
94
94
|
if (cached) {
|
|
@@ -334,7 +334,9 @@ export class TransitService {
|
|
|
334
334
|
const localDay = utcToLocal(dayUTC, timezone);
|
|
335
335
|
const dateLabel = this.formatDateLabel(localDay);
|
|
336
336
|
const currentJD = this.ephem.dateToJulianDay(dayUTC);
|
|
337
|
-
const positions = this.ephem
|
|
337
|
+
const positions = this.ephem
|
|
338
|
+
.getAllPlanets(currentJD, transitingPlanetIds)
|
|
339
|
+
.map(normalizePlanetPlacement);
|
|
338
340
|
const aspects = this.getMundaneAspects(dateLabel, positions);
|
|
339
341
|
return {
|
|
340
342
|
date: dateLabel,
|
package/dist/astro-service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GenerateChartInput, GenerateTransitChartInput, GetElectionalContextInput, GetHousesInput, GetRisingSignWindowsInput, GetTransitsInput, ServiceResult, SetNatalChartInput } from './astro-service/service-types.js';
|
|
1
|
+
import type { GenerateChartInput, GenerateTransitChartInput, GetElectionalContextInput, GetHousesInput, GetRisingSignWindowsInput, GetSignBoundaryEventsInput, GetTransitsInput, ServiceResult, SetNatalChartInput, SetPreferencesInput } from './astro-service/service-types.js';
|
|
2
2
|
import { ChartRenderer } from './charts.js';
|
|
3
3
|
import { EclipseCalculator } from './eclipses.js';
|
|
4
4
|
import type { McpStartupDefaults } from './entrypoint.js';
|
|
@@ -29,7 +29,7 @@ interface ChartServiceResult {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export { parseDateOnlyInput } from './astro-service/date-input.js';
|
|
32
|
-
export type { GenerateChartInput, GenerateTransitChartInput, GetElectionalContextInput, GetHousesInput, GetRisingSignWindowsInput, GetTransitsInput, ServiceResult, SetNatalChartInput, } from './astro-service/service-types.js';
|
|
32
|
+
export type { GenerateChartInput, GenerateTransitChartInput, GetElectionalContextInput, GetHousesInput, GetRisingSignWindowsInput, GetSignBoundaryEventsInput, GetTransitsInput, ServiceResult, SetNatalChartInput, SetPreferencesInput, } from './astro-service/service-types.js';
|
|
33
33
|
/**
|
|
34
34
|
* Shared service facade used by both the MCP server and the CLI.
|
|
35
35
|
*
|
|
@@ -45,9 +45,11 @@ export declare class AstroService {
|
|
|
45
45
|
readonly eclipseCalc: EclipseCalculator;
|
|
46
46
|
readonly chartRenderer: ChartRenderer;
|
|
47
47
|
readonly mcpStartupDefaults: Readonly<McpStartupDefaults>;
|
|
48
|
+
private readonly runtimePreferences;
|
|
48
49
|
private readonly transitService;
|
|
49
50
|
private readonly electionalService;
|
|
50
51
|
private readonly risingSignService;
|
|
52
|
+
private readonly signBoundaryService;
|
|
51
53
|
private readonly natalService;
|
|
52
54
|
private readonly skyService;
|
|
53
55
|
private readonly chartOutputService;
|
|
@@ -66,6 +68,7 @@ export declare class AstroService {
|
|
|
66
68
|
* timezone, and finally UTC.
|
|
67
69
|
*/
|
|
68
70
|
resolveReportingTimezone(explicitTimezone?: string, natalTimezone?: string): string;
|
|
71
|
+
private applyRuntimeHouseStyle;
|
|
69
72
|
/**
|
|
70
73
|
* Initialize the underlying ephemeris engine.
|
|
71
74
|
*/
|
|
@@ -116,6 +119,10 @@ export declare class AstroService {
|
|
|
116
119
|
* keeps the cheaper bucketed scan behavior.
|
|
117
120
|
*/
|
|
118
121
|
getRisingSignWindows(input: GetRisingSignWindowsInput): ServiceResult<Record<string, unknown>>;
|
|
122
|
+
/**
|
|
123
|
+
* Return exact sign-boundary events across a local calendar window.
|
|
124
|
+
*/
|
|
125
|
+
getSignBoundaryEvents(input?: GetSignBoundaryEventsInput): ServiceResult<Record<string, unknown>>;
|
|
119
126
|
/**
|
|
120
127
|
* Return the currently retrograde planets for the requested reporting timezone.
|
|
121
128
|
*/
|
|
@@ -127,7 +134,7 @@ export declare class AstroService {
|
|
|
127
134
|
* The lookup anchor remains local midnight in the natal chart timezone even
|
|
128
135
|
* when reporting text uses a preferred reporting timezone.
|
|
129
136
|
*/
|
|
130
|
-
getRiseSetTimes(natalChart: NatalChart): Promise<ServiceResult<Record<string, unknown>>>;
|
|
137
|
+
getRiseSetTimes(natalChart: NatalChart, timezone?: string): Promise<ServiceResult<Record<string, unknown>>>;
|
|
131
138
|
/**
|
|
132
139
|
* Return current asteroid and node positions for the requested reporting timezone.
|
|
133
140
|
*/
|
|
@@ -140,6 +147,10 @@ export declare class AstroService {
|
|
|
140
147
|
* Summarize process-local server state and configured startup defaults.
|
|
141
148
|
*/
|
|
142
149
|
getServerStatus(natalChart: NatalChart | null): ServiceResult<Record<string, unknown>>;
|
|
150
|
+
/**
|
|
151
|
+
* Update process-local MCP runtime preferences.
|
|
152
|
+
*/
|
|
153
|
+
setPreferences(input: SetPreferencesInput): ServiceResult<Record<string, unknown>>;
|
|
143
154
|
/**
|
|
144
155
|
* Generate a natal chart image or SVG for the current chart.
|
|
145
156
|
*
|