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.
Files changed (53) hide show
  1. package/README.md +5 -1
  2. package/dist/astro-service/natal-service.d.ts +6 -2
  3. package/dist/astro-service/natal-service.js +43 -4
  4. package/dist/astro-service/service-types.d.ts +18 -1
  5. package/dist/astro-service/shared.d.ts +8 -1
  6. package/dist/astro-service/shared.js +15 -1
  7. package/dist/astro-service/sign-boundary-service.d.ts +36 -0
  8. package/dist/astro-service/sign-boundary-service.js +156 -0
  9. package/dist/astro-service/sky-service.d.ts +1 -4
  10. package/dist/astro-service/sky-service.js +18 -14
  11. package/dist/astro-service/transit-service.js +7 -5
  12. package/dist/astro-service.d.ts +14 -3
  13. package/dist/astro-service.js +80 -10
  14. package/dist/cli.js +23 -0
  15. package/dist/entrypoint.d.ts +1 -0
  16. package/dist/entrypoint.js +13 -0
  17. package/dist/loader.js +2 -2
  18. package/dist/mcp-alias.js +2 -1
  19. package/dist/tool-registry.d.ts +1 -1
  20. package/dist/tool-registry.js +111 -8
  21. package/dist/tool-result.js +2 -1
  22. package/dist/types.d.ts +25 -2
  23. package/dist/types.js +45 -0
  24. package/docs/releases/1.4.0.md +36 -0
  25. package/docs/releases/README.md +3 -2
  26. package/package.json +1 -1
  27. package/skills/.curated/daily-brief/SKILL.md +10 -6
  28. package/skills/.curated/weekly-overview/SKILL.md +9 -6
  29. package/src/astro-service/natal-service.ts +57 -4
  30. package/src/astro-service/service-types.ts +20 -1
  31. package/src/astro-service/shared.ts +23 -1
  32. package/src/astro-service/sign-boundary-service.ts +222 -0
  33. package/src/astro-service/sky-service.ts +21 -16
  34. package/src/astro-service/transit-service.ts +7 -4
  35. package/src/astro-service.ts +108 -11
  36. package/src/cli.ts +46 -0
  37. package/src/entrypoint.ts +17 -0
  38. package/src/loader.ts +2 -2
  39. package/src/mcp-alias.ts +2 -1
  40. package/src/tool-registry.ts +129 -9
  41. package/src/tool-result.ts +2 -1
  42. package/src/types.ts +72 -18
  43. package/tests/property/transits.property.test.ts +3 -13
  44. package/tests/unit/astro-service/natal-service.test.ts +16 -2
  45. package/tests/unit/astro-service/sign-boundary-service.test.ts +188 -0
  46. package/tests/unit/astro-service/sky-service.test.ts +8 -6
  47. package/tests/unit/astro-service/transit-service.test.ts +41 -0
  48. package/tests/unit/astro-service.test.ts +161 -8
  49. package/tests/unit/cli-commands.test.ts +1 -0
  50. package/tests/unit/entrypoint.test.ts +101 -2
  51. package/tests/unit/error-mapping.test.ts +7 -0
  52. package/tests/unit/tool-registry.test.ts +43 -1
  53. 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 humanText = natalChart
175
- ? `Server ready. Natal chart loaded: ${natalChart.name} (${natalChart.location.timezone})`
176
- : 'Server ready. No natal chart loaded — call set_natal_chart first.';
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 = resolveReportingTimezone(this.mcpStartupDefaults, timezone);
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 timezone = natalChart.location.timezone;
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, timezone);
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, timezone);
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, timezone),
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 = resolveReportingTimezone(this.mcpStartupDefaults, timezone);
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 = resolveReportingTimezone(this.mcpStartupDefaults, timezone);
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 = { timezone: resolvedTimezone, eclipses };
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, PLANET_NAMES, PLANETS, } from '../types.js';
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, undefined, natalChart.location.timezone);
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(PLANET_NAMES).map(([planetId, planetName]) => [planetName, Number(planetId)]));
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.getAllPlanets(currentJD, transitingPlanetIds);
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,
@@ -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
  *