ether-to-astro 1.0.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 (138) hide show
  1. package/.env.example +13 -0
  2. package/.github/pull_request_template.md +16 -0
  3. package/.github/workflows/release.yml +35 -0
  4. package/.github/workflows/test.yml +32 -0
  5. package/AGENTS.md +99 -0
  6. package/LICENSE +18 -0
  7. package/NOTICE.md +45 -0
  8. package/README.md +301 -0
  9. package/SETUP.md +70 -0
  10. package/TESTING_SUMMARY.md +238 -0
  11. package/TEST_SUITE_STATUS.md +218 -0
  12. package/biome.json +48 -0
  13. package/dist/astro-service.d.ts +98 -0
  14. package/dist/astro-service.js +496 -0
  15. package/dist/chart-types.d.ts +52 -0
  16. package/dist/chart-types.js +51 -0
  17. package/dist/charts.d.ts +125 -0
  18. package/dist/charts.js +324 -0
  19. package/dist/cli.d.ts +7 -0
  20. package/dist/cli.js +472 -0
  21. package/dist/constants.d.ts +81 -0
  22. package/dist/constants.js +76 -0
  23. package/dist/eclipses.d.ts +85 -0
  24. package/dist/eclipses.js +184 -0
  25. package/dist/ephemeris.d.ts +120 -0
  26. package/dist/ephemeris.js +379 -0
  27. package/dist/formatter.d.ts +2 -0
  28. package/dist/formatter.js +22 -0
  29. package/dist/houses.d.ts +82 -0
  30. package/dist/houses.js +169 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.js +150 -0
  33. package/dist/loader.d.ts +2 -0
  34. package/dist/loader.js +31 -0
  35. package/dist/logger.d.ts +25 -0
  36. package/dist/logger.js +73 -0
  37. package/dist/profile-store.d.ts +48 -0
  38. package/dist/profile-store.js +156 -0
  39. package/dist/riseset.d.ts +82 -0
  40. package/dist/riseset.js +185 -0
  41. package/dist/storage.d.ts +10 -0
  42. package/dist/storage.js +40 -0
  43. package/dist/time-utils.d.ts +68 -0
  44. package/dist/time-utils.js +136 -0
  45. package/dist/tool-registry.d.ts +35 -0
  46. package/dist/tool-registry.js +307 -0
  47. package/dist/tool-result.d.ts +175 -0
  48. package/dist/tool-result.js +188 -0
  49. package/dist/transits.d.ts +108 -0
  50. package/dist/transits.js +263 -0
  51. package/dist/types.d.ts +450 -0
  52. package/dist/types.js +161 -0
  53. package/example-usage.md +131 -0
  54. package/natal-chart.json +187 -0
  55. package/package.json +61 -0
  56. package/scripts/download-ephemeris.js +115 -0
  57. package/setup.sh +21 -0
  58. package/src/astro-service.ts +710 -0
  59. package/src/chart-types.ts +125 -0
  60. package/src/charts.ts +399 -0
  61. package/src/cli.ts +694 -0
  62. package/src/constants.ts +89 -0
  63. package/src/eclipses.ts +226 -0
  64. package/src/ephemeris.ts +437 -0
  65. package/src/formatter.ts +25 -0
  66. package/src/houses.ts +202 -0
  67. package/src/index.ts +170 -0
  68. package/src/loader.ts +36 -0
  69. package/src/logger.ts +104 -0
  70. package/src/profile-store.ts +285 -0
  71. package/src/riseset.ts +229 -0
  72. package/src/time-utils.ts +167 -0
  73. package/src/tool-registry.ts +357 -0
  74. package/src/tool-result.ts +283 -0
  75. package/src/transits.ts +352 -0
  76. package/src/types.ts +547 -0
  77. package/tests/README.md +173 -0
  78. package/tests/TESTING_STRATEGY.md +178 -0
  79. package/tests/fixtures/bowen-yang-chart.ts +69 -0
  80. package/tests/fixtures/calculate-expected.ts +81 -0
  81. package/tests/fixtures/expected-results.ts +117 -0
  82. package/tests/fixtures/generate-expected-simple.ts +94 -0
  83. package/tests/helpers/date-fixtures.ts +15 -0
  84. package/tests/helpers/ephem.ts +11 -0
  85. package/tests/helpers/temp.ts +9 -0
  86. package/tests/setup.ts +11 -0
  87. package/tests/unit/astro-service.test.ts +323 -0
  88. package/tests/unit/chart-types.test.ts +18 -0
  89. package/tests/unit/charts-errors.test.ts +42 -0
  90. package/tests/unit/charts.test.ts +157 -0
  91. package/tests/unit/cli-commands.test.ts +82 -0
  92. package/tests/unit/cli-profiles.test.ts +128 -0
  93. package/tests/unit/cli.test.ts +191 -0
  94. package/tests/unit/constants.test.ts +26 -0
  95. package/tests/unit/correctness-critical.test.ts +408 -0
  96. package/tests/unit/eclipses.test.ts +108 -0
  97. package/tests/unit/ephemeris.test.ts +213 -0
  98. package/tests/unit/error-handling.test.ts +116 -0
  99. package/tests/unit/formatter.test.ts +29 -0
  100. package/tests/unit/houses-errors.test.ts +27 -0
  101. package/tests/unit/houses-validation.test.ts +164 -0
  102. package/tests/unit/houses.test.ts +205 -0
  103. package/tests/unit/profile-store.test.ts +163 -0
  104. package/tests/unit/real-user-charts.test.ts +148 -0
  105. package/tests/unit/riseset.test.ts +106 -0
  106. package/tests/unit/solver-edges.test.ts +197 -0
  107. package/tests/unit/time-utils-temporal.test.ts +303 -0
  108. package/tests/unit/time-utils.test.ts +173 -0
  109. package/tests/unit/tool-registry.test.ts +222 -0
  110. package/tests/unit/tool-result.test.ts +45 -0
  111. package/tests/unit/transit-correctness.test.ts +78 -0
  112. package/tests/unit/transits.test.ts +238 -0
  113. package/tests/validation/README.md +32 -0
  114. package/tests/validation/adapters/astrolog.ts +306 -0
  115. package/tests/validation/adapters/internal.ts +184 -0
  116. package/tests/validation/compare/eclipses.ts +47 -0
  117. package/tests/validation/compare/houses.ts +76 -0
  118. package/tests/validation/compare/positions.ts +104 -0
  119. package/tests/validation/compare/riseSet.ts +48 -0
  120. package/tests/validation/compare/roots.ts +90 -0
  121. package/tests/validation/compare/transits.ts +69 -0
  122. package/tests/validation/fixtures/astrolog-parity/core.ts +194 -0
  123. package/tests/validation/fixtures/eclipses/core.ts +14 -0
  124. package/tests/validation/fixtures/houses/core.ts +47 -0
  125. package/tests/validation/fixtures/positions/core.ts +159 -0
  126. package/tests/validation/fixtures/rise-set/core.ts +20 -0
  127. package/tests/validation/fixtures/roots/core.ts +47 -0
  128. package/tests/validation/fixtures/transits/core.ts +61 -0
  129. package/tests/validation/fixtures/transits/dst.ts +21 -0
  130. package/tests/validation/oracle.spec.ts +129 -0
  131. package/tests/validation/utils/denseRootOracle.ts +269 -0
  132. package/tests/validation/utils/fixtureTypes.ts +146 -0
  133. package/tests/validation/utils/report.ts +60 -0
  134. package/tests/validation/utils/tolerances.ts +23 -0
  135. package/tests/validation/validation.spec.ts +836 -0
  136. package/tools/color-picker.html +388 -0
  137. package/tsconfig.json +17 -0
  138. package/vitest.config.ts +31 -0
@@ -0,0 +1,496 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { Temporal } from '@js-temporal/polyfill';
3
+ import { ChartRenderer } from './charts.js';
4
+ import { getDefaultTheme } from './constants.js';
5
+ import { EclipseCalculator } from './eclipses.js';
6
+ import { EphemerisCalculator } from './ephemeris.js';
7
+ import { formatDateOnly, formatInTimezone } from './formatter.js';
8
+ import { HouseCalculator } from './houses.js';
9
+ import { RiseSetCalculator } from './riseset.js';
10
+ import { addLocalDays, localToUTC, utcToLocal } from './time-utils.js';
11
+ import { deduplicateTransits, TransitCalculator } from './transits.js';
12
+ import { ASTEROIDS, NODES, OUTER_PLANETS, PERSONAL_PLANETS, PLANETS, ZODIAC_SIGNS, } from './types.js';
13
+ export function parseDateOnlyInput(dateStr) {
14
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
15
+ if (!match) {
16
+ throw new Error(`Invalid date format: expected YYYY-MM-DD, got "${dateStr}"`);
17
+ }
18
+ const year = Number(match[1]);
19
+ const month = Number(match[2]);
20
+ const day = Number(match[3]);
21
+ if (month < 1 || month > 12) {
22
+ throw new Error(`Invalid month: ${month} (must be 1-12)`);
23
+ }
24
+ if (day < 1 || day > 31) {
25
+ throw new Error(`Invalid day: ${day} (must be 1-31)`);
26
+ }
27
+ try {
28
+ Temporal.PlainDate.from({ year, month, day });
29
+ }
30
+ catch {
31
+ throw new Error(`Invalid calendar date: ${dateStr}`);
32
+ }
33
+ return { year, month, day, hour: 12, minute: 0 };
34
+ }
35
+ export class AstroService {
36
+ ephem;
37
+ transitCalc;
38
+ houseCalc;
39
+ riseSetCalc;
40
+ eclipseCalc;
41
+ chartRenderer;
42
+ now;
43
+ writeFileFn;
44
+ constructor(deps = {}) {
45
+ this.ephem = deps.ephem ?? new EphemerisCalculator();
46
+ this.houseCalc = deps.houseCalc ?? new HouseCalculator(this.ephem);
47
+ this.transitCalc = deps.transitCalc ?? new TransitCalculator(this.ephem);
48
+ this.riseSetCalc = deps.riseSetCalc ?? new RiseSetCalculator(this.ephem);
49
+ this.eclipseCalc = deps.eclipseCalc ?? new EclipseCalculator(this.ephem);
50
+ this.chartRenderer = deps.chartRenderer ?? new ChartRenderer(this.ephem, this.houseCalc);
51
+ this.now = deps.now ?? (() => new Date());
52
+ this.writeFileFn = deps.writeFile ?? writeFile;
53
+ }
54
+ async init() {
55
+ await this.ephem.init();
56
+ }
57
+ isInitialized() {
58
+ return !!this.ephem.eph;
59
+ }
60
+ setNatalChart(input) {
61
+ const requestedHouseSystem = input.house_system ?? null;
62
+ const chart = {
63
+ name: input.name,
64
+ birthDate: {
65
+ year: input.year,
66
+ month: input.month,
67
+ day: input.day,
68
+ hour: input.hour,
69
+ minute: input.minute,
70
+ },
71
+ location: {
72
+ latitude: input.latitude,
73
+ longitude: input.longitude,
74
+ timezone: input.timezone,
75
+ },
76
+ };
77
+ const birthTimeDisambiguation = input.birth_time_disambiguation ?? 'reject';
78
+ const utcDate = localToUTC(chart.birthDate, chart.location.timezone, birthTimeDisambiguation);
79
+ const utcComponents = utcToLocal(utcDate, 'UTC');
80
+ const jd = this.ephem.dateToJulianDay(utcDate);
81
+ const planetIds = Object.values(PLANETS);
82
+ const positions = this.ephem.getAllPlanets(jd, planetIds);
83
+ const isPolar = Math.abs(chart.location.latitude) > 66;
84
+ let houseSystem = requestedHouseSystem || 'P';
85
+ if (isPolar && houseSystem === 'P') {
86
+ houseSystem = 'W';
87
+ }
88
+ const houses = this.houseCalc.calculateHouses(jd, chart.location.latitude, chart.location.longitude, houseSystem);
89
+ const storedChart = {
90
+ ...chart,
91
+ planets: positions,
92
+ julianDay: jd,
93
+ houseSystem: houses.system,
94
+ utcDateTime: utcComponents,
95
+ };
96
+ const sun = positions.find((p) => p.planet === 'Sun');
97
+ const moon = positions.find((p) => p.planet === 'Moon');
98
+ if (!sun || !moon) {
99
+ throw new Error('Ephemeris failed to compute Sun/Moon positions for natal chart.');
100
+ }
101
+ const formatDegree = (lon) => {
102
+ const sign = ZODIAC_SIGNS[Math.floor(lon / 30)];
103
+ const degree = lon % 30;
104
+ return `${degree.toFixed(0)}° ${sign}`;
105
+ };
106
+ const localTimeStr = `${chart.birthDate.month}/${chart.birthDate.day}/${chart.birthDate.year} ${chart.birthDate.hour}:${String(chart.birthDate.minute).padStart(2, '0')}`;
107
+ const utcTimeStr = `${utcComponents.month}/${utcComponents.day}/${utcComponents.year} ${utcComponents.hour}:${String(utcComponents.minute).padStart(2, '0')} UTC`;
108
+ const systemNames = {
109
+ P: 'Placidus',
110
+ W: 'Whole Sign',
111
+ K: 'Koch',
112
+ E: 'Equal',
113
+ };
114
+ const latDir = chart.location.latitude >= 0 ? 'N' : 'S';
115
+ const lonDir = chart.location.longitude >= 0 ? 'E' : 'W';
116
+ const latAbs = Math.abs(chart.location.latitude);
117
+ const lonAbs = Math.abs(chart.location.longitude);
118
+ const feedback = [
119
+ `Natal chart saved for ${chart.name}`,
120
+ '',
121
+ 'Birth Details:',
122
+ `- Local Time: ${localTimeStr} (${chart.location.timezone})`,
123
+ `- UTC Time: ${utcTimeStr}`,
124
+ `- Location: ${latAbs.toFixed(2)}°${latDir}, ${lonAbs.toFixed(2)}°${lonDir}`,
125
+ '',
126
+ 'Chart Angles:',
127
+ `- Sun: ${formatDegree(sun.longitude)}`,
128
+ `- Moon: ${formatDegree(moon.longitude)}`,
129
+ `- Ascendant: ${formatDegree(houses.ascendant)}`,
130
+ `- MC: ${formatDegree(houses.mc)}`,
131
+ '',
132
+ `House System: ${systemNames[houses.system] || houses.system}`,
133
+ ];
134
+ if (isPolar && houses.system !== houseSystem) {
135
+ feedback.push('', `Note: Polar latitude detected (${chart.location.latitude.toFixed(1)}°). Requested ${systemNames[houseSystem]}, using ${systemNames[houses.system]} instead.`);
136
+ }
137
+ else if (isPolar) {
138
+ feedback.push('', `Note: Polar latitude detected (${chart.location.latitude.toFixed(1)}°). Using ${systemNames[houses.system]} house system.`);
139
+ }
140
+ const structuredData = {
141
+ name: chart.name,
142
+ birthTime: {
143
+ local: localTimeStr,
144
+ utc: utcTimeStr,
145
+ timezone: chart.location.timezone,
146
+ },
147
+ location: {
148
+ latitude: chart.location.latitude,
149
+ longitude: chart.location.longitude,
150
+ },
151
+ julianDay: jd,
152
+ requestedHouseSystem,
153
+ resolvedHouseSystem: houses.system,
154
+ angles: {
155
+ sun: formatDegree(sun.longitude),
156
+ moon: formatDegree(moon.longitude),
157
+ ascendant: formatDegree(houses.ascendant),
158
+ mc: formatDegree(houses.mc),
159
+ },
160
+ isPolar,
161
+ };
162
+ return {
163
+ chart: storedChart,
164
+ data: structuredData,
165
+ text: feedback.join('\n'),
166
+ };
167
+ }
168
+ getTransits(natalChart, input = {}) {
169
+ const dateStr = input.date;
170
+ const categories = input.categories ?? ['all'];
171
+ const includeMundane = input.include_mundane ?? false;
172
+ const daysAhead = input.days_ahead ?? 0;
173
+ const maxOrb = input.max_orb ?? 8;
174
+ const exactOnly = input.exact_only ?? false;
175
+ const applyingOnly = input.applying_only ?? false;
176
+ if (daysAhead < 0) {
177
+ throw new Error('days_ahead must be >= 0');
178
+ }
179
+ if (maxOrb < 0) {
180
+ throw new Error('max_orb must be >= 0');
181
+ }
182
+ let transitingPlanetIds = [];
183
+ if (categories.includes('all')) {
184
+ transitingPlanetIds = Object.values(PLANETS);
185
+ }
186
+ else {
187
+ if (categories.includes('moon'))
188
+ transitingPlanetIds.push(PLANETS.MOON);
189
+ if (categories.includes('personal')) {
190
+ transitingPlanetIds.push(...PERSONAL_PLANETS.filter((p) => !transitingPlanetIds.includes(p)));
191
+ }
192
+ if (categories.includes('outer')) {
193
+ transitingPlanetIds.push(...OUTER_PLANETS.filter((p) => !transitingPlanetIds.includes(p)));
194
+ }
195
+ }
196
+ const timezone = natalChart.location.timezone;
197
+ let targetDate;
198
+ if (dateStr) {
199
+ const parsed = parseDateOnlyInput(dateStr);
200
+ targetDate = localToUTC(parsed, timezone);
201
+ }
202
+ else {
203
+ const now = this.now();
204
+ const localNow = utcToLocal(now, timezone);
205
+ const localNoon = { ...localNow, hour: 12, minute: 0, second: 0 };
206
+ targetDate = localToUTC(localNoon, timezone);
207
+ }
208
+ const allTransits = [];
209
+ const startLocal = utcToLocal(targetDate, timezone);
210
+ for (let day = 0; day <= daysAhead; day++) {
211
+ const dayUTC = addLocalDays(startLocal, timezone, day);
212
+ const jd = this.ephem.dateToJulianDay(dayUTC);
213
+ const transitingPlanets = this.ephem.getAllPlanets(jd, transitingPlanetIds);
214
+ const transits = this.transitCalc.findTransits(transitingPlanets, natalChart.planets || [], jd);
215
+ allTransits.push(...transits);
216
+ }
217
+ let filteredTransits = deduplicateTransits(allTransits);
218
+ filteredTransits = filteredTransits.filter((t) => t.orb <= maxOrb);
219
+ if (exactOnly)
220
+ filteredTransits = filteredTransits.filter((t) => t.exactTime !== undefined);
221
+ if (applyingOnly)
222
+ filteredTransits = filteredTransits.filter((t) => t.isApplying);
223
+ filteredTransits.sort((a, b) => a.orb - b.orb);
224
+ const localDate = utcToLocal(targetDate, timezone);
225
+ const dateLabel = `${localDate.year}-${String(localDate.month).padStart(2, '0')}-${String(localDate.day).padStart(2, '0')}`;
226
+ const structuredData = {
227
+ date: dateLabel,
228
+ timezone,
229
+ transits: filteredTransits.map((t) => ({
230
+ transitingPlanet: t.transitingPlanet,
231
+ aspect: t.aspect,
232
+ natalPlanet: t.natalPlanet,
233
+ orb: Number.parseFloat(t.orb.toFixed(2)),
234
+ isApplying: t.isApplying,
235
+ exactTimeStatus: t.exactTimeStatus,
236
+ exactTime: t.exactTime?.toISOString(),
237
+ transitLongitude: t.transitLongitude,
238
+ natalLongitude: t.natalLongitude,
239
+ })),
240
+ };
241
+ let responseData = structuredData;
242
+ let mundaneText = '';
243
+ if (includeMundane) {
244
+ const currentJD = this.ephem.dateToJulianDay(targetDate);
245
+ const currentPositions = this.ephem.getAllPlanets(currentJD, transitingPlanetIds);
246
+ const mundaneData = {
247
+ date: dateLabel,
248
+ timezone,
249
+ positions: currentPositions,
250
+ };
251
+ responseData = { transits: structuredData, mundane: mundaneData };
252
+ mundaneText = `\n\nCurrent Planetary Positions:\n\n${currentPositions
253
+ .map((p) => `${p.planet}: ${p.degree.toFixed(1)}° ${p.sign} (${p.isRetrograde ? 'Rx' : 'Direct'})`)
254
+ .join('\n')}`;
255
+ }
256
+ const humanLines = filteredTransits
257
+ .map((t) => {
258
+ const exactStr = t.exactTime ? ` - Exact: ${formatInTimezone(t.exactTime, timezone)}` : '';
259
+ const applyStr = t.isApplying ? '(applying)' : '(separating)';
260
+ return `${t.transitingPlanet} ${t.aspect} ${t.natalPlanet}: ${t.orb.toFixed(2)}° orb ${applyStr}${exactStr}`;
261
+ })
262
+ .join('\n');
263
+ const rangeStr = daysAhead > 0 ? ` (next ${daysAhead + 1} days)` : '';
264
+ const transitHeader = filteredTransits.length > 0
265
+ ? `Transits${rangeStr}:\n\n${humanLines}`
266
+ : 'No transits found matching the specified criteria.';
267
+ return {
268
+ data: responseData,
269
+ text: transitHeader + mundaneText,
270
+ };
271
+ }
272
+ getHouses(natalChart, input = {}) {
273
+ const system = input.system || natalChart.houseSystem || 'P';
274
+ if (!natalChart.julianDay) {
275
+ throw new Error('Natal chart is missing julianDay. Re-run set_natal_chart to fix.');
276
+ }
277
+ const houses = this.houseCalc.calculateHouses(natalChart.julianDay, natalChart.location.latitude, natalChart.location.longitude, system);
278
+ const humanLines = houses.cusps
279
+ .slice(1)
280
+ .map((deg, i) => {
281
+ const sign = ZODIAC_SIGNS[Math.floor(deg / 30)];
282
+ return `House ${i + 1}: ${(deg % 30).toFixed(2)}° ${sign}`;
283
+ })
284
+ .join('\n');
285
+ const humanText = `Houses (${houses.system}):\nAsc: ${houses.ascendant.toFixed(2)}° | MC: ${houses.mc.toFixed(2)}°\n\n${humanLines}`;
286
+ return {
287
+ data: houses,
288
+ text: humanText,
289
+ };
290
+ }
291
+ getRetrogradePlanets(timezone = 'UTC') {
292
+ const now = this.now();
293
+ const jd = this.ephem.dateToJulianDay(now);
294
+ const allPlanetIds = Object.values(PLANETS);
295
+ const positions = this.ephem.getAllPlanets(jd, allPlanetIds);
296
+ const retrograde = positions.filter((p) => p.isRetrograde);
297
+ const localNow = utcToLocal(now, timezone);
298
+ const dateLabel = `${localNow.year}-${String(localNow.month).padStart(2, '0')}-${String(localNow.day).padStart(2, '0')}`;
299
+ const structuredData = {
300
+ date: dateLabel,
301
+ timezone,
302
+ planets: retrograde,
303
+ };
304
+ const humanText = retrograde.length === 0
305
+ ? 'No planets are currently retrograde.'
306
+ : `Retrograde Planets:\n\n${retrograde.map((p) => `${p.planet}: ${p.degree.toFixed(2)}° ${p.sign}`).join('\n')}`;
307
+ return { data: structuredData, text: humanText };
308
+ }
309
+ async getRiseSetTimes(natalChart) {
310
+ const timezone = natalChart.location.timezone;
311
+ const now = this.now();
312
+ const localNow = utcToLocal(now, timezone);
313
+ const localMidnight = {
314
+ year: localNow.year,
315
+ month: localNow.month,
316
+ day: localNow.day,
317
+ hour: 0,
318
+ minute: 0,
319
+ second: 0,
320
+ };
321
+ const midnightUTC = localToUTC(localMidnight, timezone);
322
+ const dateLabel = `${localNow.year}-${String(localNow.month).padStart(2, '0')}-${String(localNow.day).padStart(2, '0')}`;
323
+ const results = await this.riseSetCalc.getAllRiseSet(midnightUTC, natalChart.location.latitude, natalChart.location.longitude);
324
+ const structuredData = {
325
+ date: dateLabel,
326
+ timezone,
327
+ times: results.map((r) => ({
328
+ planet: r.planet,
329
+ rise: r.rise?.toISOString() ?? null,
330
+ set: r.set?.toISOString() ?? null,
331
+ })),
332
+ };
333
+ const humanText = `Rise/Set Times:\n\n${results
334
+ .map((r) => {
335
+ const rise = r.rise ? formatInTimezone(r.rise, timezone) : 'none';
336
+ const set = r.set ? formatInTimezone(r.set, timezone) : 'none';
337
+ return `${r.planet}: Rise ${rise}, Set ${set}`;
338
+ })
339
+ .join('\n')}`;
340
+ return {
341
+ data: structuredData,
342
+ text: humanText,
343
+ };
344
+ }
345
+ getAsteroidPositions(timezone = 'UTC') {
346
+ const now = this.now();
347
+ const jd = this.ephem.dateToJulianDay(now);
348
+ const asteroidIds = [...ASTEROIDS, ...NODES];
349
+ const positions = this.ephem.getAllPlanets(jd, asteroidIds);
350
+ const localNow = utcToLocal(now, timezone);
351
+ const dateLabel = `${localNow.year}-${String(localNow.month).padStart(2, '0')}-${String(localNow.day).padStart(2, '0')}`;
352
+ const structuredData = {
353
+ date: dateLabel,
354
+ timezone,
355
+ positions,
356
+ };
357
+ const humanText = `Asteroid & Node Positions:\n\n${positions
358
+ .map((p) => {
359
+ const rx = p.isRetrograde ? ' Rx' : '';
360
+ return `${p.planet}: ${p.degree.toFixed(2)}° ${p.sign}${rx}`;
361
+ })
362
+ .join('\n')}`;
363
+ return {
364
+ data: structuredData,
365
+ text: humanText,
366
+ };
367
+ }
368
+ getNextEclipses(timezone = 'UTC') {
369
+ const now = this.now();
370
+ const jd = this.ephem.dateToJulianDay(now);
371
+ const solarEclipse = this.eclipseCalc.findNextSolarEclipse(jd);
372
+ const lunarEclipse = this.eclipseCalc.findNextLunarEclipse(jd);
373
+ const eclipses = [];
374
+ const humanLines = [];
375
+ if (solarEclipse) {
376
+ eclipses.push({
377
+ type: solarEclipse.type,
378
+ eclipseType: solarEclipse.eclipseType,
379
+ maxTime: solarEclipse.maxTime.toISOString(),
380
+ });
381
+ humanLines.push(`Next Solar Eclipse: ${formatInTimezone(solarEclipse.maxTime, timezone)} (${solarEclipse.eclipseType})`);
382
+ }
383
+ if (lunarEclipse) {
384
+ eclipses.push({
385
+ type: lunarEclipse.type,
386
+ eclipseType: lunarEclipse.eclipseType,
387
+ maxTime: lunarEclipse.maxTime.toISOString(),
388
+ });
389
+ humanLines.push(`Next Lunar Eclipse: ${formatInTimezone(lunarEclipse.maxTime, timezone)} (${lunarEclipse.eclipseType})`);
390
+ }
391
+ const structuredData = { timezone, eclipses };
392
+ const humanText = eclipses.length === 0
393
+ ? 'No eclipses found in the near future.'
394
+ : `Upcoming Eclipses:\n\n${humanLines.join('\n')}`;
395
+ return { data: structuredData, text: humanText };
396
+ }
397
+ getServerStatus(natalChart) {
398
+ const statusData = {
399
+ serverVersion: '1.0.0',
400
+ hasNatalChart: natalChart !== null,
401
+ natalChartName: natalChart?.name ?? null,
402
+ natalChartTimezone: natalChart?.location.timezone ?? null,
403
+ ephemerisInitialized: this.isInitialized(),
404
+ stateModel: 'stateful-per-process',
405
+ };
406
+ const humanText = natalChart
407
+ ? `Server ready. Natal chart loaded: ${natalChart.name} (${natalChart.location.timezone})`
408
+ : 'Server ready. No natal chart loaded — call set_natal_chart first.';
409
+ return { data: statusData, text: humanText };
410
+ }
411
+ async generateNatalChart(natalChart, input = {}) {
412
+ const theme = input.theme || getDefaultTheme(natalChart.location.timezone);
413
+ const format = input.format || 'svg';
414
+ const outputPath = input.output_path;
415
+ const chart = await this.chartRenderer.generateNatalChart(natalChart, theme, format);
416
+ if (outputPath) {
417
+ if (format === 'svg') {
418
+ await this.writeFileFn(outputPath, chart, 'utf-8');
419
+ }
420
+ else {
421
+ await this.writeFileFn(outputPath, chart);
422
+ }
423
+ return {
424
+ format,
425
+ outputPath,
426
+ text: `Natal Chart for ${natalChart.name} saved to: ${outputPath}`,
427
+ };
428
+ }
429
+ if (format === 'svg') {
430
+ return {
431
+ format,
432
+ text: `Natal Chart for ${natalChart.name}:`,
433
+ svg: chart,
434
+ };
435
+ }
436
+ const base64 = chart.toString('base64');
437
+ const mimeType = format === 'png' ? 'image/png' : 'image/webp';
438
+ return {
439
+ format,
440
+ text: `Natal Chart for ${natalChart.name} (${theme} theme, ${format.toUpperCase()} format):`,
441
+ image: {
442
+ data: base64,
443
+ mimeType,
444
+ },
445
+ };
446
+ }
447
+ async generateTransitChart(natalChart, input = {}) {
448
+ const dateStr = input.date;
449
+ const theme = input.theme ?? getDefaultTheme(natalChart.location.timezone);
450
+ const format = input.format ?? 'svg';
451
+ let targetDate;
452
+ if (dateStr) {
453
+ const parsed = parseDateOnlyInput(dateStr);
454
+ targetDate = localToUTC(parsed, natalChart.location.timezone);
455
+ }
456
+ else {
457
+ const now = this.now();
458
+ const localNow = utcToLocal(now, natalChart.location.timezone);
459
+ const localNoon = { ...localNow, hour: 12, minute: 0, second: 0 };
460
+ targetDate = localToUTC(localNoon, natalChart.location.timezone);
461
+ }
462
+ const outputPath = input.output_path;
463
+ const chart = await this.chartRenderer.generateTransitChart(natalChart, targetDate, theme, format);
464
+ const dateLabel = formatDateOnly(targetDate, natalChart.location.timezone);
465
+ if (outputPath) {
466
+ if (format === 'svg') {
467
+ await this.writeFileFn(outputPath, chart, 'utf-8');
468
+ }
469
+ else {
470
+ await this.writeFileFn(outputPath, chart);
471
+ }
472
+ return {
473
+ format,
474
+ outputPath,
475
+ text: `Transit Chart for ${natalChart.name} (${dateLabel}) saved to ${outputPath}`,
476
+ };
477
+ }
478
+ if (format === 'svg') {
479
+ return {
480
+ format,
481
+ text: `Transit Chart for ${natalChart.name} (${dateLabel})`,
482
+ svg: chart,
483
+ };
484
+ }
485
+ const base64 = chart.toString('base64');
486
+ const mimeType = format === 'png' ? 'image/png' : 'image/webp';
487
+ return {
488
+ format,
489
+ text: `Transit Chart for ${natalChart.name} (${dateLabel}, ${theme} theme, ${format.toUpperCase()} format):`,
490
+ image: {
491
+ data: base64,
492
+ mimeType,
493
+ },
494
+ };
495
+ }
496
+ }
@@ -0,0 +1,52 @@
1
+ export interface AstroChartConstructor {
2
+ new (elementId: string, width: number, height: number, settings: Partial<AstroChartSettings>): AstroChartInstance;
3
+ default?: AstroChartConstructor;
4
+ }
5
+ export interface AstroChartInstance {
6
+ radix(data: AstroChartData): RadixChart;
7
+ }
8
+ export interface RadixChart {
9
+ aspects(): void;
10
+ transit(data: AstroChartData): void;
11
+ }
12
+ export interface AstroChartData {
13
+ planets: AstroChartPlanets;
14
+ cusps: number[];
15
+ }
16
+ export interface AstroChartPlanets {
17
+ [planetName: string]: number[];
18
+ }
19
+ export interface AstroChartSettings {
20
+ SYMBOL_SCALE?: number;
21
+ STROKE_ONLY?: boolean;
22
+ COLOR_BACKGROUND?: string;
23
+ CIRCLE_COLOR?: string;
24
+ LINE_COLOR?: string;
25
+ POINTS_COLOR?: string;
26
+ SIGNS_COLOR?: string;
27
+ CUSPS_FONT_COLOR?: string;
28
+ SYMBOL_AXIS_FONT_COLOR?: string;
29
+ COLOR_ARIES?: string;
30
+ COLOR_TAURUS?: string;
31
+ COLOR_GEMINI?: string;
32
+ COLOR_CANCER?: string;
33
+ COLOR_LEO?: string;
34
+ COLOR_VIRGO?: string;
35
+ COLOR_LIBRA?: string;
36
+ COLOR_SCORPIO?: string;
37
+ COLOR_SAGITTARIUS?: string;
38
+ COLOR_CAPRICORN?: string;
39
+ COLOR_AQUARIUS?: string;
40
+ COLOR_PISCES?: string;
41
+ COLOR_SIGNS?: string[];
42
+ ASPECTS?: {
43
+ [key: string]: {
44
+ degree: number;
45
+ orbit: number;
46
+ color: string;
47
+ };
48
+ };
49
+ }
50
+ export type ChartTheme = 'light' | 'dark';
51
+ export type ChartFormat = 'svg' | 'png' | 'webp';
52
+ export declare function getThemeSettings(theme: ChartTheme, transparent?: boolean): Partial<AstroChartSettings>;
@@ -0,0 +1,51 @@
1
+ // Type definitions for AstroChart library data structures
2
+ import { DARK_ASPECT_COLORS, DARK_THEME_COLORS, LIGHT_ASPECT_COLORS, LIGHT_THEME_COLORS, } from './constants.js';
3
+ export function getThemeSettings(theme, transparent = false) {
4
+ if (theme === 'dark') {
5
+ return {
6
+ COLOR_BACKGROUND: transparent ? 'transparent' : '#282c34',
7
+ CIRCLE_COLOR: '#4b5263',
8
+ LINE_COLOR: '#4b5263',
9
+ POINTS_COLOR: '#abb2bf',
10
+ SIGNS_COLOR: '#d7dae0',
11
+ CUSPS_FONT_COLOR: '#abb2bf',
12
+ SYMBOL_AXIS_FONT_COLOR: '#abb2bf',
13
+ ASPECTS: DARK_ASPECT_COLORS,
14
+ COLOR_ARIES: DARK_THEME_COLORS[0],
15
+ COLOR_TAURUS: DARK_THEME_COLORS[1],
16
+ COLOR_GEMINI: DARK_THEME_COLORS[2],
17
+ COLOR_CANCER: DARK_THEME_COLORS[3],
18
+ COLOR_LEO: DARK_THEME_COLORS[4],
19
+ COLOR_VIRGO: DARK_THEME_COLORS[5],
20
+ COLOR_LIBRA: DARK_THEME_COLORS[6],
21
+ COLOR_SCORPIO: DARK_THEME_COLORS[7],
22
+ COLOR_SAGITTARIUS: DARK_THEME_COLORS[8],
23
+ COLOR_CAPRICORN: DARK_THEME_COLORS[9],
24
+ COLOR_AQUARIUS: DARK_THEME_COLORS[10],
25
+ COLOR_PISCES: DARK_THEME_COLORS[11],
26
+ };
27
+ }
28
+ // Light theme (defaults)
29
+ return {
30
+ COLOR_BACKGROUND: transparent ? 'transparent' : '#ffffff',
31
+ CIRCLE_COLOR: '#333333',
32
+ LINE_COLOR: '#333333',
33
+ POINTS_COLOR: '#000000',
34
+ SIGNS_COLOR: '#000000',
35
+ CUSPS_FONT_COLOR: '#000000',
36
+ SYMBOL_AXIS_FONT_COLOR: '#333333',
37
+ ASPECTS: LIGHT_ASPECT_COLORS,
38
+ COLOR_ARIES: LIGHT_THEME_COLORS[0],
39
+ COLOR_TAURUS: LIGHT_THEME_COLORS[1],
40
+ COLOR_GEMINI: LIGHT_THEME_COLORS[2],
41
+ COLOR_CANCER: LIGHT_THEME_COLORS[3],
42
+ COLOR_LEO: LIGHT_THEME_COLORS[4],
43
+ COLOR_VIRGO: LIGHT_THEME_COLORS[5],
44
+ COLOR_LIBRA: LIGHT_THEME_COLORS[6],
45
+ COLOR_SCORPIO: LIGHT_THEME_COLORS[7],
46
+ COLOR_SAGITTARIUS: LIGHT_THEME_COLORS[8],
47
+ COLOR_CAPRICORN: LIGHT_THEME_COLORS[9],
48
+ COLOR_AQUARIUS: LIGHT_THEME_COLORS[10],
49
+ COLOR_PISCES: LIGHT_THEME_COLORS[11],
50
+ };
51
+ }