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
@@ -4,6 +4,7 @@ import { ElectionalService } from './astro-service/electional-service.js';
4
4
  import { NatalService } from './astro-service/natal-service.js';
5
5
  import { RisingSignService } from './astro-service/rising-sign-service.js';
6
6
  import { resolveReportingTimezone } from './astro-service/shared.js';
7
+ import { SignBoundaryService } from './astro-service/sign-boundary-service.js';
7
8
  import { SkyService } from './astro-service/sky-service.js';
8
9
  import { TransitService } from './astro-service/transit-service.js';
9
10
  import { ChartRenderer } from './charts.js';
@@ -12,7 +13,9 @@ import { EphemerisCalculator } from './ephemeris.js';
12
13
  import { formatInTimezone } from './formatter.js';
13
14
  import { HouseCalculator } from './houses.js';
14
15
  import { RiseSetCalculator } from './riseset.js';
16
+ import { isValidTimezone } from './time-utils.js';
15
17
  import { TransitCalculator } from './transits.js';
18
+ const VALID_RUNTIME_HOUSE_STYLES = new Set(['P', 'W', 'K', 'E']);
16
19
  export { parseDateOnlyInput } from './astro-service/date-input.js';
17
20
  /**
18
21
  * Shared service facade used by both the MCP server and the CLI.
@@ -29,9 +32,11 @@ export class AstroService {
29
32
  eclipseCalc;
30
33
  chartRenderer;
31
34
  mcpStartupDefaults;
35
+ runtimePreferences = {};
32
36
  transitService;
33
37
  electionalService;
34
38
  risingSignService;
39
+ signBoundaryService;
35
40
  natalService;
36
41
  skyService;
37
42
  chartOutputService;
@@ -63,6 +68,10 @@ export class AstroService {
63
68
  ephem: this.ephem,
64
69
  houseCalc: this.houseCalc,
65
70
  });
71
+ this.signBoundaryService = new SignBoundaryService({
72
+ ephem: this.ephem,
73
+ now: this.now,
74
+ });
66
75
  this.natalService = new NatalService({
67
76
  ephem: this.ephem,
68
77
  houseCalc: this.houseCalc,
@@ -73,7 +82,6 @@ export class AstroService {
73
82
  ephem: this.ephem,
74
83
  riseSetCalc: this.riseSetCalc,
75
84
  eclipseCalc: this.eclipseCalc,
76
- mcpStartupDefaults: this.mcpStartupDefaults,
77
85
  now: this.now,
78
86
  formatTimestamp: this.formatTimestamp.bind(this),
79
87
  });
@@ -99,7 +107,18 @@ export class AstroService {
99
107
  * timezone, and finally UTC.
100
108
  */
101
109
  resolveReportingTimezone(explicitTimezone, natalTimezone) {
102
- return resolveReportingTimezone(this.mcpStartupDefaults, explicitTimezone, natalTimezone);
110
+ return (explicitTimezone ??
111
+ this.runtimePreferences.preferredTimezone ??
112
+ resolveReportingTimezone(this.mcpStartupDefaults, undefined, natalTimezone));
113
+ }
114
+ applyRuntimeHouseStyle(natalChart) {
115
+ if (this.runtimePreferences.preferredHouseStyle === undefined) {
116
+ return natalChart;
117
+ }
118
+ return {
119
+ ...natalChart,
120
+ requestedHouseSystem: this.runtimePreferences.preferredHouseStyle,
121
+ };
103
122
  }
104
123
  /**
105
124
  * Initialize the underlying ephemeris engine.
@@ -131,7 +150,10 @@ export class AstroService {
131
150
  * may use a different reporting timezone for labels when startup defaults are set.
132
151
  */
133
152
  getTransits(natalChart, input = {}) {
134
- return this.transitService.getTransits(natalChart, input);
153
+ const effectiveInput = input.timezone === undefined && this.runtimePreferences.preferredTimezone !== undefined
154
+ ? { ...input, timezone: this.runtimePreferences.preferredTimezone }
155
+ : input;
156
+ return this.transitService.getTransits(this.applyRuntimeHouseStyle(natalChart), effectiveInput);
135
157
  }
136
158
  /**
137
159
  * Produce deterministic electional context for a single local instant.
@@ -151,7 +173,7 @@ export class AstroService {
151
173
  * chart preference, then startup defaults.
152
174
  */
153
175
  getHouses(natalChart, input = {}) {
154
- return this.natalService.getHouses(natalChart, input);
176
+ return this.natalService.getHouses(this.applyRuntimeHouseStyle(natalChart), input);
155
177
  }
156
178
  /**
157
179
  * Find rising-sign windows across a calendar day at a specific location.
@@ -163,11 +185,21 @@ export class AstroService {
163
185
  getRisingSignWindows(input) {
164
186
  return this.risingSignService.getRisingSignWindows(input);
165
187
  }
188
+ /**
189
+ * Return exact sign-boundary events across a local calendar window.
190
+ */
191
+ getSignBoundaryEvents(input = {}) {
192
+ const timezone = this.resolveReportingTimezone(input.timezone);
193
+ return this.signBoundaryService.getSignBoundaryEvents({
194
+ ...input,
195
+ timezone,
196
+ });
197
+ }
166
198
  /**
167
199
  * Return the currently retrograde planets for the requested reporting timezone.
168
200
  */
169
201
  getRetrogradePlanets(timezone) {
170
- return this.skyService.getRetrogradePlanets(timezone);
202
+ return this.skyService.getRetrogradePlanets(this.resolveReportingTimezone(timezone));
171
203
  }
172
204
  /**
173
205
  * Return the next rise and set events after the local day anchor for the chart location.
@@ -176,26 +208,64 @@ export class AstroService {
176
208
  * The lookup anchor remains local midnight in the natal chart timezone even
177
209
  * when reporting text uses a preferred reporting timezone.
178
210
  */
179
- async getRiseSetTimes(natalChart) {
180
- return this.skyService.getRiseSetTimes(natalChart);
211
+ async getRiseSetTimes(natalChart, timezone) {
212
+ return this.skyService.getRiseSetTimes(natalChart, this.resolveReportingTimezone(timezone, natalChart.location.timezone));
181
213
  }
182
214
  /**
183
215
  * Return current asteroid and node positions for the requested reporting timezone.
184
216
  */
185
217
  getAsteroidPositions(timezone) {
186
- return this.skyService.getAsteroidPositions(timezone);
218
+ return this.skyService.getAsteroidPositions(this.resolveReportingTimezone(timezone));
187
219
  }
188
220
  /**
189
221
  * Look up the next solar and lunar eclipses after the current instant.
190
222
  */
191
223
  getNextEclipses(timezone) {
192
- return this.skyService.getNextEclipses(timezone);
224
+ return this.skyService.getNextEclipses(this.resolveReportingTimezone(timezone));
193
225
  }
194
226
  /**
195
227
  * Summarize process-local server state and configured startup defaults.
196
228
  */
197
229
  getServerStatus(natalChart) {
198
- return this.natalService.getServerStatus(natalChart);
230
+ return this.natalService.getServerStatus(natalChart, this.runtimePreferences);
231
+ }
232
+ /**
233
+ * Update process-local MCP runtime preferences.
234
+ */
235
+ setPreferences(input) {
236
+ if (input.preferred_timezone !== undefined) {
237
+ if (input.preferred_timezone === null) {
238
+ delete this.runtimePreferences.preferredTimezone;
239
+ }
240
+ else {
241
+ if (!isValidTimezone(input.preferred_timezone)) {
242
+ throw new Error(`Invalid timezone: ${input.preferred_timezone}`);
243
+ }
244
+ this.runtimePreferences.preferredTimezone = input.preferred_timezone;
245
+ }
246
+ }
247
+ if (input.preferred_house_style !== undefined) {
248
+ if (input.preferred_house_style === null) {
249
+ delete this.runtimePreferences.preferredHouseStyle;
250
+ }
251
+ else {
252
+ if (!VALID_RUNTIME_HOUSE_STYLES.has(input.preferred_house_style)) {
253
+ throw new Error(`Invalid preferred house style: ${input.preferred_house_style} (must be one of P, W, K, E)`);
254
+ }
255
+ this.runtimePreferences.preferredHouseStyle = input.preferred_house_style;
256
+ }
257
+ }
258
+ const preferredTimezone = this.runtimePreferences.preferredTimezone ?? null;
259
+ const preferredHouseStyle = this.runtimePreferences.preferredHouseStyle ?? null;
260
+ return {
261
+ data: {
262
+ runtimePreferences: {
263
+ preferredTimezone,
264
+ preferredHouseStyle,
265
+ },
266
+ },
267
+ text: `Runtime preferences updated. Reporting timezone: ${preferredTimezone ?? 'default'}. House style: ${preferredHouseStyle ?? 'default'}.`,
268
+ };
199
269
  }
200
270
  /**
201
271
  * Generate a natal chart image or SVG for the current chart.
package/dist/cli.js CHANGED
@@ -308,6 +308,29 @@ export async function runCli(argv, io = {
308
308
  const result = await spec.execute({ service, natalChart: null }, { timezone });
309
309
  emitExecution(io, result, options.pretty ?? false);
310
310
  });
311
+ program
312
+ .command('get-sign-boundary-events')
313
+ .description(mustTool('get_sign_boundary_events').description)
314
+ .option('--date <yyyy-mm-dd>', toolSchemaProperty('get_sign_boundary_events', 'date').description ?? 'Start local date')
315
+ .option('--days-ahead <number>', toolSchemaProperty('get_sign_boundary_events', 'days_ahead').description ?? 'Days ahead')
316
+ .option('--timezone <tz>', toolSchemaProperty('get_sign_boundary_events', 'timezone').description ?? 'Timezone')
317
+ .option('--bodies <list>', 'Comma-separated list of bodies to scan (Sun,Moon,Mercury,Venus,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto)')
318
+ .option('--pretty', 'Human-readable output instead of JSON')
319
+ .action(async (options) => {
320
+ const spec = mustTool('get_sign_boundary_events');
321
+ const timezone = await resolveCommandTimezone(options);
322
+ const bodies = options.bodies
323
+ ?.split(',')
324
+ .map((value) => value.trim())
325
+ .filter(Boolean);
326
+ const result = await spec.execute({ service, natalChart: null }, {
327
+ date: options.date,
328
+ timezone,
329
+ days_ahead: options.daysAhead == null ? undefined : toNumber(options.daysAhead, 'days-ahead'),
330
+ bodies,
331
+ });
332
+ emitExecution(io, result, options.pretty ?? false);
333
+ });
311
334
  program
312
335
  .command('get-electional-context')
313
336
  .description(mustTool('get_electional_context').description)
@@ -11,3 +11,4 @@ export interface EntrypointResolution {
11
11
  mcpStartupDefaults: Readonly<McpStartupDefaults>;
12
12
  }
13
13
  export declare function resolveEntrypoint(argv: string[], invokedPath?: string): EntrypointResolution;
14
+ export declare function isDirectExecution(importMetaUrl: string, invokedPath?: string): boolean;
@@ -1,4 +1,6 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { isValidTimezone } from './time-utils.js';
3
5
  const VALID_HOUSE_STYLES = new Set(['P', 'W', 'K', 'E']);
4
6
  function readOptionValue(argv, index, flag) {
@@ -76,3 +78,14 @@ export function resolveEntrypoint(argv, invokedPath = process.argv[1] ?? '') {
76
78
  mcpStartupDefaults: Object.freeze({ ...defaults }),
77
79
  };
78
80
  }
81
+ export function isDirectExecution(importMetaUrl, invokedPath = process.argv[1] ?? '') {
82
+ if (!invokedPath) {
83
+ return false;
84
+ }
85
+ try {
86
+ return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(invokedPath);
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
package/dist/loader.js CHANGED
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  // Set up browser globals for astrochart library BEFORE any imports
5
5
  import { JSDOM } from 'jsdom';
6
- import { resolveEntrypoint } from './entrypoint.js';
6
+ import { isDirectExecution, resolveEntrypoint } from './entrypoint.js';
7
7
  function initializeRuntime() {
8
8
  const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
9
9
  globalThis.window = dom.window;
@@ -61,7 +61,7 @@ export async function runEntrypoint(argv = process.argv.slice(2), invokedPath =
61
61
  const code = await runCli(resolved.cliArgv);
62
62
  process.exit(code);
63
63
  }
64
- if (import.meta.url === `file://${process.argv[1]}`) {
64
+ if (isDirectExecution(import.meta.url)) {
65
65
  runEntrypoint().catch((error) => {
66
66
  console.error('[ERROR] Failed to start program:', error);
67
67
  process.exit(1);
package/dist/mcp-alias.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { isDirectExecution } from './entrypoint.js';
2
3
  import { runEntrypoint } from './loader.js';
3
- if (import.meta.url === `file://${process.argv[1]}`) {
4
+ if (isDirectExecution(import.meta.url)) {
4
5
  runEntrypoint(['--mcp', ...process.argv.slice(2)], process.argv[1]).catch((error) => {
5
6
  console.error('[ERROR] Failed to start program:', error);
6
7
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import type { AstroService } from './astro-service.js';
2
- import type { NatalChart } from './types.js';
2
+ import { type NatalChart } from './types.js';
3
3
  type ToolContent = {
4
4
  type: 'text';
5
5
  text: string;
@@ -1,4 +1,30 @@
1
+ import { SIGN_BOUNDARY_BODIES, } from './types.js';
1
2
  export const MCP_TOOL_SPECS = [
3
+ {
4
+ name: 'set_preferences',
5
+ description: 'Update process-local MCP runtime preferences. Use this to change session reporting defaults such as timezone and house style without restarting the server.',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ preferred_timezone: {
10
+ anyOf: [{ type: 'string' }, { type: 'null' }],
11
+ description: 'Optional reporting timezone override for this MCP session. Use null to clear the override.',
12
+ },
13
+ preferred_house_style: {
14
+ anyOf: [{ type: 'string', enum: ['P', 'W', 'K', 'E'] }, { type: 'null' }],
15
+ description: 'Optional preferred house style override for this MCP session. Use null to clear the override.',
16
+ },
17
+ },
18
+ },
19
+ requiresNatalChart: false,
20
+ execute: (ctx, args) => {
21
+ const result = ctx.service.setPreferences({
22
+ preferred_timezone: args.preferred_timezone,
23
+ preferred_house_style: args.preferred_house_style,
24
+ });
25
+ return { kind: 'state', data: result.data, text: result.text };
26
+ },
27
+ },
2
28
  {
3
29
  name: 'set_natal_chart',
4
30
  description: 'Store natal chart data for transit calculations. Birth time should be LOCAL time at the birth location (not UTC). The server converts to UTC using the timezone parameter. Optional birth_time_disambiguation handles DST overlap/gap edge cases (default: reject).',
@@ -98,6 +124,46 @@ export const MCP_TOOL_SPECS = [
98
124
  return { kind: 'state', data: result.data, text: result.text };
99
125
  },
100
126
  },
127
+ {
128
+ name: 'get_sign_boundary_events',
129
+ description: 'Return exact sign-boundary events for one or more planets across a local date window. Each event includes both from_sign and to_sign so ingress and egress are represented as one crossing.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ date: {
134
+ type: 'string',
135
+ description: 'Start local date (YYYY-MM-DD). Defaults to today in the resolved timezone.',
136
+ },
137
+ timezone: {
138
+ type: 'string',
139
+ description: 'Optional timezone used for local-day interpretation and reporting. Falls back to the current MCP session timezone.',
140
+ },
141
+ days_ahead: {
142
+ type: 'number',
143
+ description: 'Number of days to look ahead from the start date. Defaults to 0 for a single-day window.',
144
+ default: 0,
145
+ },
146
+ bodies: {
147
+ type: 'array',
148
+ items: {
149
+ type: 'string',
150
+ enum: [...SIGN_BOUNDARY_BODIES],
151
+ },
152
+ description: 'Optional list of supported bodies to scan. Defaults to all supported sign-boundary bodies.',
153
+ },
154
+ },
155
+ },
156
+ requiresNatalChart: false,
157
+ execute: (ctx, args) => {
158
+ const result = ctx.service.getSignBoundaryEvents({
159
+ date: args.date,
160
+ timezone: args.timezone,
161
+ days_ahead: args.days_ahead,
162
+ bodies: args.bodies,
163
+ });
164
+ return { kind: 'state', data: result.data, text: result.text };
165
+ },
166
+ },
101
167
  {
102
168
  name: 'get_electional_context',
103
169
  description: 'Get stateless electional context for a specific local date, time, and location. Returns deterministic timing facts such as ascendant, sect/day-night classification, Moon phase, applying aspects, and optional ascendant-ruler basics. This tool does not require a natal chart and is separate from get_transits.',
@@ -168,6 +234,10 @@ export const MCP_TOOL_SPECS = [
168
234
  type: 'string',
169
235
  description: 'Date for transits (ISO format YYYY-MM-DD). Defaults to today.',
170
236
  },
237
+ timezone: {
238
+ type: 'string',
239
+ description: 'Optional reporting timezone override. Calculation day interpretation still uses the natal chart timezone.',
240
+ },
171
241
  categories: {
172
242
  type: 'array',
173
243
  items: { type: 'string', enum: ['moon', 'personal', 'outer', 'all'] },
@@ -175,7 +245,7 @@ export const MCP_TOOL_SPECS = [
175
245
  },
176
246
  include_mundane: {
177
247
  type: 'boolean',
178
- description: 'Include deterministic mundane baseline data for the requested window. Output includes planetary positions, transit-to-transit mundane aspects, and non-narrative weather grouping metadata; forecast windows also include per-day mundane.days entries. Defaults to false.',
248
+ description: 'Include deterministic mundane baseline data for the requested window. Output includes planetary positions using the same sign-boundary normalization as serialized transits, transit-to-transit mundane aspects, and non-narrative weather grouping metadata; forecast windows also include per-day mundane.days entries. Defaults to false.',
179
249
  },
180
250
  days_ahead: {
181
251
  type: 'number',
@@ -206,6 +276,7 @@ export const MCP_TOOL_SPECS = [
206
276
  execute: (ctx, args) => {
207
277
  const result = ctx.service.getTransits(ctx.natalChart, {
208
278
  date: args.date,
279
+ timezone: args.timezone,
209
280
  categories: args.categories,
210
281
  include_mundane: args.include_mundane,
211
282
  days_ahead: args.days_ahead,
@@ -242,7 +313,15 @@ export const MCP_TOOL_SPECS = [
242
313
  {
243
314
  name: 'get_retrograde_planets',
244
315
  description: 'Show which planets are currently retrograde',
245
- inputSchema: { type: 'object', properties: {} },
316
+ inputSchema: {
317
+ type: 'object',
318
+ properties: {
319
+ timezone: {
320
+ type: 'string',
321
+ description: 'Optional reporting timezone override',
322
+ },
323
+ },
324
+ },
246
325
  requiresNatalChart: false,
247
326
  execute: (ctx, args) => {
248
327
  const timezone = ctx.service.resolveReportingTimezone(args.timezone, ctx.natalChart?.location?.timezone);
@@ -253,17 +332,33 @@ export const MCP_TOOL_SPECS = [
253
332
  {
254
333
  name: 'get_rise_set_times',
255
334
  description: 'Get sunrise, sunset, moonrise, moonset times for today',
256
- inputSchema: { type: 'object', properties: {} },
335
+ inputSchema: {
336
+ type: 'object',
337
+ properties: {
338
+ timezone: {
339
+ type: 'string',
340
+ description: 'Optional reporting timezone override. Rise/set anchoring still uses the natal chart timezone.',
341
+ },
342
+ },
343
+ },
257
344
  requiresNatalChart: true,
258
- execute: async (ctx) => {
259
- const result = await ctx.service.getRiseSetTimes(ctx.natalChart);
345
+ execute: async (ctx, args) => {
346
+ const result = await ctx.service.getRiseSetTimes(ctx.natalChart, args.timezone);
260
347
  return { kind: 'state', data: result.data, text: result.text };
261
348
  },
262
349
  },
263
350
  {
264
351
  name: 'get_asteroid_positions',
265
352
  description: 'Get positions of major asteroids (Chiron, Ceres, Pallas, Juno, Vesta) and Nodes',
266
- inputSchema: { type: 'object', properties: {} },
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ timezone: {
357
+ type: 'string',
358
+ description: 'Optional reporting timezone override',
359
+ },
360
+ },
361
+ },
267
362
  requiresNatalChart: false,
268
363
  execute: (ctx, args) => {
269
364
  const timezone = ctx.service.resolveReportingTimezone(args.timezone, ctx.natalChart?.location?.timezone);
@@ -274,7 +369,15 @@ export const MCP_TOOL_SPECS = [
274
369
  {
275
370
  name: 'get_next_eclipses',
276
371
  description: 'Find the next solar and lunar eclipses',
277
- inputSchema: { type: 'object', properties: {} },
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ timezone: {
376
+ type: 'string',
377
+ description: 'Optional reporting timezone override',
378
+ },
379
+ },
380
+ },
278
381
  requiresNatalChart: false,
279
382
  execute: (ctx, args) => {
280
383
  const timezone = ctx.service.resolveReportingTimezone(args.timezone, ctx.natalChart?.location?.timezone);
@@ -284,7 +387,7 @@ export const MCP_TOOL_SPECS = [
284
387
  },
285
388
  {
286
389
  name: 'get_server_status',
287
- description: 'Inspect the current server state: whether a natal chart is loaded, its name and timezone, and the server version. Call this before making assumptions about loaded context.',
390
+ description: 'Inspect the current server state: whether a natal chart is loaded, its name and timezone, the effective reporting timezone and house-style context, and the server version. Call this before making assumptions about loaded context.',
288
391
  inputSchema: { type: 'object', properties: {} },
289
392
  requiresNatalChart: false,
290
393
  execute: (ctx) => {
@@ -124,7 +124,8 @@ export function mapToolErrorMessageToCode(errorMessage) {
124
124
  errorMessage.includes('missing julianDay') ||
125
125
  errorMessage.includes('Invalid mode') ||
126
126
  errorMessage.includes('Invalid latitude') ||
127
- errorMessage.includes('Invalid longitude')) {
127
+ errorMessage.includes('Invalid longitude') ||
128
+ errorMessage.includes('Invalid preferred house style')) {
128
129
  return 'INVALID_INPUT';
129
130
  }
130
131
  if (errorMessage.includes('Invalid timezone') || errorMessage.includes('timezone')) {
package/dist/types.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export type HouseSystem = 'P' | 'K' | 'W' | 'E' | 'O' | 'R' | 'C' | 'A' | 'V' | 'X' | 'H' | 'T' | 'B';
2
2
  export type SolarEclipseType = 'partial' | 'annular' | 'total' | 'annular-total';
3
3
  export type LunarEclipseType = 'penumbral' | 'partial' | 'total';
4
- export type PlanetName = 'Sun' | 'Moon' | 'Mercury' | 'Venus' | 'Mars' | 'Jupiter' | 'Saturn' | 'Uranus' | 'Neptune' | 'Pluto' | 'Chiron' | 'North Node (Mean)' | 'North Node (True)' | 'Ceres' | 'Pallas' | 'Juno' | 'Vesta';
4
+ export declare const CLASSICAL_PLANET_NAMES: readonly ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn"];
5
+ export declare const MAJOR_PLANET_NAMES: readonly ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"];
6
+ export declare const SUPPLEMENTAL_PLANET_NAMES: readonly ["Chiron", "North Node (Mean)", "North Node (True)", "Ceres", "Pallas", "Juno", "Vesta"];
7
+ export declare const ALL_PLANET_NAMES: readonly ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", "Chiron", "North Node (Mean)", "North Node (True)", "Ceres", "Pallas", "Juno", "Vesta"];
8
+ export type PlanetName = (typeof ALL_PLANET_NAMES)[number];
9
+ export declare const SIGN_BOUNDARY_BODIES: readonly ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"];
10
+ export type SignBoundaryBody = (typeof SIGN_BOUNDARY_BODIES)[number];
5
11
  /**
6
12
  * Represents a complete natal birth chart with all necessary data for calculations
7
13
  *
@@ -124,6 +130,22 @@ export interface PlanetPosition {
124
130
  */
125
131
  isRetrograde: boolean;
126
132
  }
133
+ export interface SignBoundaryEvent {
134
+ body: SignBoundaryBody;
135
+ from_sign: string;
136
+ to_sign: string;
137
+ exact_time: string;
138
+ longitude: number;
139
+ direction: 'direct' | 'retrograde';
140
+ }
141
+ export interface SignBoundaryEventResponse {
142
+ date: string;
143
+ timezone: string;
144
+ calculation_timezone: string;
145
+ reporting_timezone: string;
146
+ days_ahead: number;
147
+ events: SignBoundaryEvent[];
148
+ }
127
149
  /**
128
150
  * Base interface for all transit data
129
151
  *
@@ -370,6 +392,7 @@ export type PlanetId = (typeof PLANETS)[keyof typeof PLANETS];
370
392
  export declare const PLANET_NAMES: {
371
393
  [key: number]: PlanetName;
372
394
  };
395
+ export declare const PLANET_IDS_BY_NAME: Record<PlanetName, PlanetId>;
373
396
  /**
374
397
  * Personal planets (inner planets)
375
398
  *
@@ -400,7 +423,7 @@ export declare const OUTER_PLANETS: (8 | 6 | 5 | 7 | 9)[];
400
423
  * - Juno: Partnership, commitment
401
424
  * - Vesta: Devotion, service
402
425
  */
403
- export declare const ASTEROIDS: (18 | 15 | 17 | 19 | 20)[];
426
+ export declare const ASTEROIDS: (18 | 17 | 15 | 19 | 20)[];
404
427
  /**
405
428
  * Lunar nodes
406
429
  *
package/dist/types.js CHANGED
@@ -1,3 +1,29 @@
1
+ export const CLASSICAL_PLANET_NAMES = [
2
+ 'Sun',
3
+ 'Moon',
4
+ 'Mercury',
5
+ 'Venus',
6
+ 'Mars',
7
+ 'Jupiter',
8
+ 'Saturn',
9
+ ];
10
+ export const MAJOR_PLANET_NAMES = [
11
+ ...CLASSICAL_PLANET_NAMES,
12
+ 'Uranus',
13
+ 'Neptune',
14
+ 'Pluto',
15
+ ];
16
+ export const SUPPLEMENTAL_PLANET_NAMES = [
17
+ 'Chiron',
18
+ 'North Node (Mean)',
19
+ 'North Node (True)',
20
+ 'Ceres',
21
+ 'Pallas',
22
+ 'Juno',
23
+ 'Vesta',
24
+ ];
25
+ export const ALL_PLANET_NAMES = [...MAJOR_PLANET_NAMES, ...SUPPLEMENTAL_PLANET_NAMES];
26
+ export const SIGN_BOUNDARY_BODIES = MAJOR_PLANET_NAMES;
1
27
  /**
2
28
  * Aspect definitions with angles and default orbs
3
29
  *
@@ -81,6 +107,25 @@ export const PLANET_NAMES = {
81
107
  19: 'Juno',
82
108
  20: 'Vesta',
83
109
  };
110
+ export const PLANET_IDS_BY_NAME = {
111
+ Sun: PLANETS.SUN,
112
+ Moon: PLANETS.MOON,
113
+ Mercury: PLANETS.MERCURY,
114
+ Venus: PLANETS.VENUS,
115
+ Mars: PLANETS.MARS,
116
+ Jupiter: PLANETS.JUPITER,
117
+ Saturn: PLANETS.SATURN,
118
+ Uranus: PLANETS.URANUS,
119
+ Neptune: PLANETS.NEPTUNE,
120
+ Pluto: PLANETS.PLUTO,
121
+ Chiron: PLANETS.CHIRON,
122
+ 'North Node (Mean)': PLANETS.MEAN_NODE,
123
+ 'North Node (True)': PLANETS.TRUE_NODE,
124
+ Ceres: PLANETS.CERES,
125
+ Pallas: PLANETS.PALLAS,
126
+ Juno: PLANETS.JUNO,
127
+ Vesta: PLANETS.VESTA,
128
+ };
84
129
  /**
85
130
  * Personal planets (inner planets)
86
131
  *
@@ -0,0 +1,36 @@
1
+ # ether-to-astro v1.4.0
2
+
3
+ Suggested release type: `minor`
4
+
5
+ This release builds on `1.3.1` by adding a reusable sign-boundary event primitive for deterministic ingress and egress queries across MCP, CLI, and the shared AstroService layer. It also hardens cusp-edge behavior so exact-boundary lookups avoid false events at tangential stations and at the exclusive end of a requested local-date window.
6
+
7
+ ## 1.4.0 (2026-03-29)
8
+
9
+ ### New feature
10
+
11
+ - add `get_sign_boundary_events` as a stateless MCP tool for exact sign-boundary crossings across a local date window
12
+ - add `get-sign-boundary-events` to the CLI so sign-boundary scans are available outside MCP sessions
13
+ - add shared `AstroService` support and structured response types for reusable sign-boundary event lookups
14
+
15
+ ### Bug fix
16
+
17
+ - skip cusp-touch roots that do not actually move the body into the adjacent sign
18
+ - exclude sign-boundary roots that land exactly on the exclusive end of the requested window
19
+ - keep error mapping and response-shape coverage aligned with the new sign-boundary surface
20
+
21
+ ### Documentation Changes
22
+
23
+ - document the new sign-boundary tool in the README tool and CLI command lists
24
+
25
+ ### ⚙️ Chore
26
+
27
+ - centralize supported sign-boundary bodies and planet-name constants for reuse across service logic and tests
28
+ - extend unit and property coverage for the new sign-boundary workflow and related shared typing
29
+
30
+ ## Upgrade notes
31
+
32
+ - no breaking API changes are included in this release
33
+
34
+ ## Included PRs
35
+
36
+ - #65 add sign boundary events tool
@@ -10,8 +10,9 @@ Conventions:
10
10
 
11
11
  Current draft:
12
12
 
13
- - [v1.2.0 draft](/Users/salted/Code/astro-mcp/docs/releases/1.2.0-draft.md)
13
+ - none currently
14
14
 
15
15
  Released notes:
16
16
 
17
- - [v1.3.0 notes](/Users/salted/Code/astro-mcp/docs/releases/1.3.0.md)
17
+ - [v1.4.0 notes](./1.4.0.md)
18
+ - [v1.3.0 notes](./1.3.0.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ether-to-astro",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Local-first astrology toolkit with a unified e2a binary for CLI and MCP workflows, plus an e2a-mcp compatibility alias.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",