ether-to-astro 1.0.2 → 1.2.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 (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug-report.yml +87 -0
  2. package/.github/ISSUE_TEMPLATE/capability-request.yml +117 -0
  3. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. package/.github/ISSUE_TEMPLATE/paper-cut.yml +59 -0
  5. package/.github/pull_request_template.md +1 -0
  6. package/.github/workflows/release.yml +2 -2
  7. package/.github/workflows/test.yml +2 -2
  8. package/AGENTS.md +46 -1
  9. package/DEVELOPER.md +78 -0
  10. package/README.md +128 -75
  11. package/SETUP.md +100 -41
  12. package/dist/astro-service.d.ts +51 -2
  13. package/dist/astro-service.js +660 -56
  14. package/dist/cli.js +31 -0
  15. package/dist/entrypoint.d.ts +13 -0
  16. package/dist/entrypoint.js +78 -0
  17. package/dist/ephemeris.d.ts +15 -0
  18. package/dist/ephemeris.js +33 -0
  19. package/dist/formatter.d.ts +5 -1
  20. package/dist/formatter.js +4 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +63 -114
  23. package/dist/loader.d.ts +1 -1
  24. package/dist/loader.js +61 -23
  25. package/dist/mcp-alias.d.ts +2 -0
  26. package/dist/mcp-alias.js +8 -0
  27. package/dist/time-utils.d.ts +8 -0
  28. package/dist/time-utils.js +16 -0
  29. package/dist/tool-registry.js +111 -5
  30. package/dist/tool-result.d.ts +8 -0
  31. package/dist/tool-result.js +39 -0
  32. package/dist/types.d.ts +79 -1
  33. package/docs/product/adrs/0001-mcp-vs-skill-boundary.md +96 -0
  34. package/docs/product/architecture-boundaries.md +223 -0
  35. package/docs/product/product-tenets.md +174 -0
  36. package/docs/releases/1.2.0-draft.md +48 -0
  37. package/package.json +7 -7
  38. package/skills/.curated/daily-brief/SKILL.md +75 -0
  39. package/skills/.curated/electional-overlay/SKILL.md +67 -0
  40. package/skills/.curated/weekly-overview/SKILL.md +73 -0
  41. package/skills/.system/write-skill/SKILL.md +90 -0
  42. package/src/astro-service.ts +861 -60
  43. package/src/cli.ts +84 -0
  44. package/src/entrypoint.ts +118 -0
  45. package/src/ephemeris.ts +44 -0
  46. package/src/formatter.ts +13 -1
  47. package/src/index.ts +77 -121
  48. package/src/loader.ts +69 -25
  49. package/src/mcp-alias.ts +10 -0
  50. package/src/time-utils.ts +18 -0
  51. package/src/tool-registry.ts +129 -9
  52. package/src/tool-result.ts +44 -0
  53. package/src/types.ts +91 -1
  54. package/tests/unit/astro-service.test.ts +751 -5
  55. package/tests/unit/cli-commands.test.ts +13 -0
  56. package/tests/unit/entrypoint.test.ts +67 -0
  57. package/tests/unit/error-mapping.test.ts +20 -0
  58. package/tests/unit/formatter.test.ts +6 -0
  59. package/tests/unit/tool-registry.test.ts +114 -2
  60. package/setup.sh +0 -21
@@ -1,6 +1,6 @@
1
1
  import type { AstroService } from './astro-service.js';
2
2
  import type { Disambiguation } from './time-utils.js';
3
- import type { HouseSystem, NatalChart } from './types.js';
3
+ import type { ElectionalHouseSystem, HouseSystem, NatalChart } from './types.js';
4
4
 
5
5
  type ToolContent =
6
6
  | { type: 'text'; text: string }
@@ -91,10 +91,117 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
91
91
  return { kind: 'state', data: result.data, text: result.text, natalChart: result.chart };
92
92
  },
93
93
  },
94
+ {
95
+ name: 'get_rising_sign_windows',
96
+ description:
97
+ 'Get local-time windows for which zodiac signs are rising on a given date and location. Returns deterministic intervals only (no ranking or interpretation).',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ date: {
102
+ type: 'string',
103
+ description: 'Target local date (YYYY-MM-DD)',
104
+ },
105
+ latitude: { type: 'number', description: 'Latitude in decimal degrees (-90 to 90)' },
106
+ longitude: {
107
+ type: 'number',
108
+ description: 'Longitude in decimal degrees (-180 to 180)',
109
+ },
110
+ timezone: {
111
+ type: 'string',
112
+ description: 'IANA timezone (e.g., America/New_York)',
113
+ },
114
+ mode: {
115
+ type: 'string',
116
+ enum: ['approximate', 'exact'],
117
+ description:
118
+ 'Boundary mode. approximate uses coarser stepping; exact refines sign-change boundaries.',
119
+ default: 'approximate',
120
+ },
121
+ },
122
+ required: ['date', 'latitude', 'longitude', 'timezone'],
123
+ },
124
+ requiresNatalChart: false,
125
+ execute: (ctx, args) => {
126
+ const result = ctx.service.getRisingSignWindows({
127
+ date: args.date as string,
128
+ latitude: args.latitude as number,
129
+ longitude: args.longitude as number,
130
+ timezone: args.timezone as string,
131
+ mode: args.mode as 'approximate' | 'exact' | undefined,
132
+ });
133
+ return { kind: 'state', data: result.data, text: result.text };
134
+ },
135
+ },
136
+ {
137
+ name: 'get_electional_context',
138
+ description:
139
+ '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.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ date: {
144
+ type: 'string',
145
+ description: 'Target local date (YYYY-MM-DD)',
146
+ },
147
+ time: {
148
+ type: 'string',
149
+ description:
150
+ 'Target local time (HH:mm or HH:mm:ss). DST-ambiguous or nonexistent local times are rejected.',
151
+ },
152
+ timezone: {
153
+ type: 'string',
154
+ description: 'IANA timezone (e.g., America/New_York)',
155
+ },
156
+ latitude: { type: 'number', description: 'Latitude in decimal degrees (-90 to 90)' },
157
+ longitude: {
158
+ type: 'number',
159
+ description: 'Longitude in decimal degrees (-180 to 180)',
160
+ },
161
+ house_system: {
162
+ type: 'string',
163
+ enum: ['P', 'K', 'W', 'R'],
164
+ description:
165
+ 'House system used for ascendant extraction: P=Placidus (default), K=Koch, W=Whole Sign, R=Regiomontanus.',
166
+ },
167
+ include_ruler_basics: {
168
+ type: 'boolean',
169
+ description:
170
+ 'Include ascendant-ruler position, speed, and retrograde flag. Defaults to false.',
171
+ },
172
+ include_planetary_applications: {
173
+ type: 'boolean',
174
+ description: 'Include applying major aspects between current planets. Defaults to true.',
175
+ },
176
+ orb_degrees: {
177
+ type: 'number',
178
+ description:
179
+ 'Orb for electional aspect detection in degrees. Defaults to 3 and must be between 0.1 and 10.',
180
+ default: 3,
181
+ },
182
+ },
183
+ required: ['date', 'time', 'timezone', 'latitude', 'longitude'],
184
+ },
185
+ requiresNatalChart: false,
186
+ execute: (ctx, args) => {
187
+ const result = ctx.service.getElectionalContext({
188
+ date: args.date as string,
189
+ time: args.time as string,
190
+ timezone: args.timezone as string,
191
+ latitude: args.latitude as number,
192
+ longitude: args.longitude as number,
193
+ house_system: args.house_system as ElectionalHouseSystem | undefined,
194
+ include_ruler_basics: args.include_ruler_basics as boolean | undefined,
195
+ include_planetary_applications: args.include_planetary_applications as boolean | undefined,
196
+ orb_degrees: args.orb_degrees as number | undefined,
197
+ });
198
+ return { kind: 'state', data: result.data, text: result.text };
199
+ },
200
+ },
94
201
  {
95
202
  name: 'get_transits',
96
203
  description:
97
- 'Get transits (aspects between current/future planets and natal chart). Returns aspects within orb, with exact timing when close. Date defaults to today at local noon in the natal chart timezone.',
204
+ 'Get transits (aspects between current/future planets and natal chart). Each transit includes additive placement metadata for both sides (sign, degree, house) so clients can render activation context without reconstructing house logic. Supports mode=snapshot (single-day), mode=best_hit (multi-day compressed preview), and mode=forecast (day-grouped output). If mode is omitted, legacy behavior is preserved: days_ahead=0 resolves to snapshot and days_ahead>0 resolves to best_hit.',
98
205
  inputSchema: {
99
206
  type: 'object',
100
207
  properties: {
@@ -116,9 +223,15 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
116
223
  days_ahead: {
117
224
  type: 'number',
118
225
  description:
119
- 'Number of days to look ahead for upcoming transits. 0 = today only. Defaults to 0.',
226
+ 'Number of days to look ahead. In snapshot mode only the start day is used. If mode is omitted, legacy behavior is preserved: 0 resolves to snapshot and values > 0 resolve to best_hit.',
120
227
  default: 0,
121
228
  },
229
+ mode: {
230
+ type: 'string',
231
+ enum: ['snapshot', 'best_hit', 'forecast'],
232
+ description:
233
+ 'Transit output mode: snapshot=single-day, best_hit=compressed preview across range, forecast=day-grouped output. If omitted, legacy behavior is preserved.',
234
+ },
122
235
  max_orb: {
123
236
  type: 'number',
124
237
  description: 'Maximum orb in degrees to include. Defaults to 8.',
@@ -142,6 +255,7 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
142
255
  categories: args.categories as string[] | undefined,
143
256
  include_mundane: args.include_mundane as boolean | undefined,
144
257
  days_ahead: args.days_ahead as number | undefined,
258
+ mode: args.mode as 'snapshot' | 'best_hit' | 'forecast' | undefined,
145
259
  max_orb: args.max_orb as number | undefined,
146
260
  exact_only: args.exact_only as boolean | undefined,
147
261
  applying_only: args.applying_only as boolean | undefined,
@@ -178,8 +292,10 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
178
292
  inputSchema: { type: 'object', properties: {} },
179
293
  requiresNatalChart: false,
180
294
  execute: (ctx, args) => {
181
- const timezone =
182
- (args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
295
+ const timezone = ctx.service.resolveReportingTimezone(
296
+ args.timezone as string | undefined,
297
+ ctx.natalChart?.location?.timezone
298
+ );
183
299
  const result = ctx.service.getRetrogradePlanets(timezone);
184
300
  return { kind: 'state', data: result.data, text: result.text };
185
301
  },
@@ -200,8 +316,10 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
200
316
  inputSchema: { type: 'object', properties: {} },
201
317
  requiresNatalChart: false,
202
318
  execute: (ctx, args) => {
203
- const timezone =
204
- (args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
319
+ const timezone = ctx.service.resolveReportingTimezone(
320
+ args.timezone as string | undefined,
321
+ ctx.natalChart?.location?.timezone
322
+ );
205
323
  const result = ctx.service.getAsteroidPositions(timezone);
206
324
  return { kind: 'state', data: result.data, text: result.text };
207
325
  },
@@ -212,8 +330,10 @@ export const MCP_TOOL_SPECS: ToolSpec[] = [
212
330
  inputSchema: { type: 'object', properties: {} },
213
331
  requiresNatalChart: false,
214
332
  execute: (ctx, args) => {
215
- const timezone =
216
- (args.timezone as string | undefined) ?? ctx.natalChart?.location.timezone ?? 'UTC';
333
+ const timezone = ctx.service.resolveReportingTimezone(
334
+ args.timezone as string | undefined,
335
+ ctx.natalChart?.location?.timezone
336
+ );
217
337
  const result = ctx.service.getNextEclipses(timezone);
218
338
  return { kind: 'state', data: result.data, text: result.text };
219
339
  },
@@ -189,6 +189,50 @@ export function mapSweError(
189
189
  };
190
190
  }
191
191
 
192
+ /**
193
+ * Map generic error messages to structured tool issue codes.
194
+ *
195
+ * @remarks
196
+ * Primarily used at the MCP boundary when unknown errors are caught
197
+ * and need a best-effort classification.
198
+ */
199
+ export function mapToolErrorMessageToCode(errorMessage: string): ToolIssueCode {
200
+ if (
201
+ errorMessage.includes('Invalid date format') ||
202
+ errorMessage.includes('Invalid calendar date') ||
203
+ errorMessage.includes('Invalid month') ||
204
+ errorMessage.includes('Invalid day') ||
205
+ errorMessage.includes('days_ahead') ||
206
+ errorMessage.includes('max_orb') ||
207
+ errorMessage.includes('missing julianDay') ||
208
+ errorMessage.includes('Invalid mode') ||
209
+ errorMessage.includes('Invalid latitude') ||
210
+ errorMessage.includes('Invalid longitude')
211
+ ) {
212
+ return 'INVALID_INPUT';
213
+ }
214
+ if (errorMessage.includes('Invalid timezone') || errorMessage.includes('timezone')) {
215
+ return 'INVALID_TIMEZONE';
216
+ }
217
+ if (errorMessage.includes('Invalid house system')) {
218
+ return 'INVALID_HOUSE_SYSTEM';
219
+ }
220
+ if (errorMessage.includes('Ephemeris') || errorMessage.includes('ephemeris')) {
221
+ return 'EPHEMERIS_COMPUTE_FAILED';
222
+ }
223
+ if (
224
+ errorMessage.includes('write') ||
225
+ errorMessage.includes('ENOENT') ||
226
+ errorMessage.includes('EACCES')
227
+ ) {
228
+ return 'FILE_WRITE_FAILED';
229
+ }
230
+ if (errorMessage.includes('render') || errorMessage.includes('chart')) {
231
+ return 'CHART_RENDER_FAILED';
232
+ }
233
+ return 'INTERNAL_ERROR';
234
+ }
235
+
192
236
  /**
193
237
  * Create a structured error for missing rise/set events
194
238
  *
package/src/types.ts CHANGED
@@ -86,6 +86,12 @@ export interface NatalChart {
86
86
  * For polar latitudes (>66°), Whole Sign ('W') may be used as fallback.
87
87
  */
88
88
  houseSystem?: HouseSystem;
89
+ /**
90
+ * Explicit house system requested when the natal chart was created
91
+ * @remarks
92
+ * This stays undefined when the caller relied on runtime defaults.
93
+ */
94
+ requestedHouseSystem?: HouseSystem;
89
95
  /**
90
96
  * UTC equivalent of birth time
91
97
  * @remarks
@@ -217,6 +223,18 @@ export interface TransitData extends BaseTransit {
217
223
  * May be undefined if aspect is not within orb or exact time not calculated
218
224
  */
219
225
  exactTime?: string; // ISO timestamp
226
+ /** Zodiac sign of the transiting planet */
227
+ transitSign?: string;
228
+ /** Degree of the transiting planet within its sign (0-30) */
229
+ transitDegree?: number;
230
+ /** House occupied by the transiting planet at the transit calculation time */
231
+ transitHouse?: number;
232
+ /** Zodiac sign of the natal planet */
233
+ natalSign?: string;
234
+ /** Degree of the natal planet within its sign (0-30) */
235
+ natalDegree?: number;
236
+ /** House occupied by the natal planet in the natal chart */
237
+ natalHouse?: number;
220
238
  }
221
239
 
222
240
  /**
@@ -229,8 +247,12 @@ export interface TransitData extends BaseTransit {
229
247
  export interface TransitResponse {
230
248
  /** ISO date of the transit calculation (YYYY-MM-DD) */
231
249
  date: string;
232
- /** Timezone used for the calculation */
250
+ /** Timezone used for reporting and user-facing day labels */
233
251
  timezone: string;
252
+ /** Natal/local timezone used for date interpretation and astro calculations */
253
+ calculation_timezone?: string;
254
+ /** Timezone used for reporting and user-facing day labels */
255
+ reporting_timezone?: string;
234
256
  /** Array of all active transits for the date */
235
257
  transits: TransitData[];
236
258
  }
@@ -250,6 +272,8 @@ export interface PlanetPositionResponse {
250
272
  positions: PlanetPosition[];
251
273
  }
252
274
 
275
+ export type ElectionalHouseSystem = Extract<HouseSystem, 'P' | 'K' | 'W' | 'R'>;
276
+
253
277
  /**
254
278
  * Types of astrological aspects
255
279
  *
@@ -259,6 +283,72 @@ export interface PlanetPositionResponse {
259
283
  */
260
284
  export type AspectType = 'conjunction' | 'opposition' | 'square' | 'trine' | 'sextile';
261
285
 
286
+ export type ElectionalPhaseName =
287
+ | 'new'
288
+ | 'crescent'
289
+ | 'first_quarter'
290
+ | 'gibbous'
291
+ | 'full'
292
+ | 'disseminating'
293
+ | 'last_quarter'
294
+ | 'balsamic';
295
+
296
+ export interface ElectionalAspect {
297
+ from_body: PlanetName;
298
+ to_body: PlanetName;
299
+ aspect: AspectType;
300
+ orb: number;
301
+ applying: boolean;
302
+ exact_at_utc?: string;
303
+ }
304
+
305
+ export interface ElectionalContextResponse {
306
+ input: {
307
+ date: string;
308
+ time: string;
309
+ timezone: string;
310
+ latitude: number;
311
+ longitude: number;
312
+ house_system: ElectionalHouseSystem;
313
+ instant_utc: string;
314
+ jd_ut: number;
315
+ };
316
+ ascendant: {
317
+ longitude: number;
318
+ sign: string;
319
+ degree_in_sign: number;
320
+ };
321
+ sect: {
322
+ is_day_chart: boolean;
323
+ sun_altitude_degrees: number;
324
+ classification: 'day' | 'night';
325
+ };
326
+ moon: {
327
+ longitude: number;
328
+ sign: string;
329
+ phase_angle: number;
330
+ phase_name: ElectionalPhaseName;
331
+ is_void_of_course: boolean | null;
332
+ applying_aspects?: ElectionalAspect[];
333
+ };
334
+ applying_aspects?: ElectionalAspect[];
335
+ ruler_basics?: {
336
+ asc_sign_ruler: {
337
+ body: PlanetName;
338
+ longitude: number;
339
+ sign: string;
340
+ speed: number;
341
+ is_retrograde: boolean;
342
+ };
343
+ };
344
+ meta: {
345
+ deterministic: true;
346
+ requires_natal: false;
347
+ warnings: string[];
348
+ deferred_features: string[];
349
+ };
350
+ }
351
+
262
352
  /**
263
353
  * Aspect definitions with angles and default orbs
264
354
  *