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
@@ -25,6 +25,19 @@ describe.sequential('When exercising CLI command handlers end-to-end', () => {
25
25
  ['get-retrograde-planets'],
26
26
  ['get-asteroid-positions'],
27
27
  ['get-next-eclipses'],
28
+ [
29
+ 'get-electional-context',
30
+ '--date',
31
+ '2026-03-28',
32
+ '--time',
33
+ '09:30',
34
+ '--timezone',
35
+ 'America/Los_Angeles',
36
+ '--latitude',
37
+ '37.7749',
38
+ '--longitude',
39
+ '-122.4194',
40
+ ],
28
41
  ['get-transits', ...natalArgs, '--categories', 'all', '--days-ahead', '1', '--max-orb', '5'],
29
42
  ['get-houses', ...natalArgs, '--system', 'W'],
30
43
  ['get-rise-set-times', ...natalArgs],
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveEntrypoint } from '../../src/entrypoint.js';
3
+
4
+ describe('When resolving unified binary entrypoint mode', () => {
5
+ it('Given the e2a binary with no startup flags, then CLI mode receives the original argv', () => {
6
+ const resolved = resolveEntrypoint(['get-next-eclipses'], '/usr/local/bin/e2a');
7
+ expect(resolved.mode).toBe('cli');
8
+ expect(resolved.cliArgv).toEqual(['get-next-eclipses']);
9
+ expect(resolved.mcpHelpRequested).toBe(false);
10
+ expect(resolved.mcpStartupDefaults).toEqual({});
11
+ });
12
+
13
+ it('Given e2a --mcp with startup defaults, then MCP mode is selected and defaults are parsed', () => {
14
+ const resolved = resolveEntrypoint(
15
+ [
16
+ '--mcp',
17
+ '--preferred-tz',
18
+ 'America/Los_Angeles',
19
+ '--preferred-house-style=W',
20
+ '--weekday-labels',
21
+ ],
22
+ '/usr/local/bin/e2a'
23
+ );
24
+ expect(resolved.mode).toBe('mcp');
25
+ expect(resolved.cliArgv).toEqual([]);
26
+ expect(resolved.mcpHelpRequested).toBe(false);
27
+ expect(resolved.mcpStartupDefaults).toEqual({
28
+ preferredTimezone: 'America/Los_Angeles',
29
+ preferredHouseStyle: 'W',
30
+ weekdayLabels: true,
31
+ });
32
+ });
33
+
34
+ it('Given the compatibility alias, then MCP mode is selected without --mcp', () => {
35
+ const resolved = resolveEntrypoint(['--preferred-tz', 'UTC'], '/usr/local/bin/e2a-mcp');
36
+ expect(resolved.mode).toBe('mcp');
37
+ expect(resolved.mcpHelpRequested).toBe(false);
38
+ expect(resolved.mcpStartupDefaults).toEqual({
39
+ preferredTimezone: 'UTC',
40
+ });
41
+ });
42
+
43
+ it('Given MCP mode help flags, then help is allowed without treating them as CLI args', () => {
44
+ const resolved = resolveEntrypoint(['--mcp', '--help'], '/usr/local/bin/e2a');
45
+ expect(resolved.mode).toBe('mcp');
46
+ expect(resolved.mcpHelpRequested).toBe(true);
47
+ expect(resolved.cliArgv).toEqual(['--help']);
48
+ });
49
+
50
+ it('Given MCP-only flags outside MCP mode, then resolution fails clearly', () => {
51
+ expect(() =>
52
+ resolveEntrypoint(['get-next-eclipses', '--preferred-tz', 'UTC'], '/usr/local/bin/e2a')
53
+ ).toThrow(/MCP startup defaults require MCP mode/);
54
+ });
55
+
56
+ it('Given invalid startup defaults or CLI args in MCP mode, then resolution fails clearly', () => {
57
+ expect(() =>
58
+ resolveEntrypoint(['--mcp', '--preferred-house-style', 'Q'], '/usr/local/bin/e2a')
59
+ ).toThrow(/Invalid preferred house style/);
60
+ expect(() =>
61
+ resolveEntrypoint(['--mcp', '--preferred-tz', 'Nope/Bad'], '/usr/local/bin/e2a')
62
+ ).toThrow(/Invalid timezone/);
63
+ expect(() =>
64
+ resolveEntrypoint(['--mcp', 'get-next-eclipses'], '/usr/local/bin/e2a')
65
+ ).toThrow(/Unexpected CLI arguments in MCP mode/);
66
+ });
67
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mapToolErrorMessageToCode } from '../../src/tool-result.js';
3
+
4
+ describe('mapToolErrorMessageToCode', () => {
5
+ it('maps rising-sign mode and coordinates validation failures to INVALID_INPUT', () => {
6
+ expect(mapToolErrorMessageToCode('Invalid mode: fast')).toBe('INVALID_INPUT');
7
+ expect(mapToolErrorMessageToCode('Invalid latitude: 95 (must be between -90 and 90)')).toBe(
8
+ 'INVALID_INPUT'
9
+ );
10
+ expect(
11
+ mapToolErrorMessageToCode('Invalid longitude: -190 (must be between -180 and 180)')
12
+ ).toBe('INVALID_INPUT');
13
+ });
14
+
15
+ it('preserves existing timezone classification', () => {
16
+ expect(mapToolErrorMessageToCode('Invalid timezone: Nope/Not-A-Timezone')).toBe(
17
+ 'INVALID_TIMEZONE'
18
+ );
19
+ });
20
+ });
@@ -26,4 +26,10 @@ describe('When formatting timestamps for users', () => {
26
26
  expect(ny).toMatch(/AM|PM/);
27
27
  expect(la).toMatch(/AM|PM/);
28
28
  });
29
+
30
+ it('Given weekday formatting is requested, then output includes a weekday label', () => {
31
+ const date = new Date('2024-03-20T12:00:00Z');
32
+ const formatted = formatInTimezone(date, 'America/New_York', { weekday: true });
33
+ expect(formatted).toMatch(/\b(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\b/);
34
+ });
29
35
  });
@@ -4,8 +4,13 @@ import { createToolSpecIndex, getToolSpec, MCP_TOOL_SPECS } from '../../src/tool
4
4
  function makeService() {
5
5
  return {
6
6
  setNatalChart: vi.fn(() => ({ data: { ok: true }, text: 'set', chart: { name: 'x' } })),
7
+ getRisingSignWindows: vi.fn(() => ({ data: { windows: [] }, text: 'rising windows' })),
8
+ getElectionalContext: vi.fn(() => ({ data: { ascendant: {} }, text: 'electional' })),
7
9
  getTransits: vi.fn(() => ({ data: { transits: [] }, text: 'transits' })),
8
10
  getHouses: vi.fn(() => ({ data: { system: 'P' }, text: 'houses' })),
11
+ resolveReportingTimezone: vi.fn((explicit?: string, natal?: string) => {
12
+ return explicit ?? 'America/New_York' ?? natal ?? 'UTC';
13
+ }),
9
14
  getRetrogradePlanets: vi.fn(() => ({ data: { planets: [] }, text: 'retro' })),
10
15
  getRiseSetTimes: vi.fn(async () => ({ data: { times: [] }, text: 'rise' })),
11
16
  getAsteroidPositions: vi.fn(() => ({ data: { positions: [] }, text: 'asteroids' })),
@@ -47,6 +52,35 @@ describe('When resolving tool specs from the registry', () => {
47
52
  }
48
53
  });
49
54
 
55
+
56
+ it('Given get_transits args, then execute forwards mundane and filter options to service', async () => {
57
+ const service = makeService();
58
+ const ctx = { service: service as any, natalChart: { name: 'chart' } as any };
59
+
60
+ const spec = getToolSpec('get_transits');
61
+ expect(spec).toBeDefined();
62
+
63
+ await spec!.execute(ctx, {
64
+ include_mundane: true,
65
+ days_ahead: 3,
66
+ max_orb: 5,
67
+ exact_only: true,
68
+ applying_only: true,
69
+ categories: ['outer'],
70
+ date: '2026-01-02',
71
+ });
72
+
73
+ expect(service.getTransits).toHaveBeenCalledWith(ctx.natalChart, {
74
+ include_mundane: true,
75
+ days_ahead: 3,
76
+ max_orb: 5,
77
+ exact_only: true,
78
+ applying_only: true,
79
+ categories: ['outer'],
80
+ date: '2026-01-02',
81
+ });
82
+ });
83
+
50
84
  it('Given simple state tools, then calls route to matching service methods', async () => {
51
85
  const service = makeService();
52
86
  const ctx = { service: service as any, natalChart: { name: 'chart' } as any };
@@ -58,6 +92,60 @@ describe('When resolving tool specs from the registry', () => {
58
92
  expect(service.getServerStatus).toHaveBeenCalled();
59
93
  });
60
94
 
95
+ it('Given rising-sign window arguments, then tool routes to the shared service with deterministic shape', async () => {
96
+ const service = makeService();
97
+ const result = await getToolSpec('get_rising_sign_windows')!.execute(
98
+ { service: service as any, natalChart: null },
99
+ {
100
+ date: '2026-03-28',
101
+ latitude: 40.7128,
102
+ longitude: -74.006,
103
+ timezone: 'America/New_York',
104
+ mode: 'exact',
105
+ }
106
+ );
107
+
108
+ expect(result.kind).toBe('state');
109
+ expect(service.getRisingSignWindows).toHaveBeenCalledWith({
110
+ date: '2026-03-28',
111
+ latitude: 40.7128,
112
+ longitude: -74.006,
113
+ timezone: 'America/New_York',
114
+ mode: 'exact',
115
+ });
116
+ });
117
+
118
+ it('Given get_electional_context args, then the stateless tool forwards them without natal state', async () => {
119
+ const service = makeService();
120
+ const result = await getToolSpec('get_electional_context')!.execute(
121
+ { service: service as any, natalChart: null },
122
+ {
123
+ date: '2026-03-28',
124
+ time: '09:30',
125
+ timezone: 'America/Los_Angeles',
126
+ latitude: 37.7749,
127
+ longitude: -122.4194,
128
+ house_system: 'R',
129
+ include_ruler_basics: true,
130
+ include_planetary_applications: false,
131
+ orb_degrees: 2.5,
132
+ }
133
+ );
134
+
135
+ expect(result.kind).toBe('state');
136
+ expect(service.getElectionalContext).toHaveBeenCalledWith({
137
+ date: '2026-03-28',
138
+ time: '09:30',
139
+ timezone: 'America/Los_Angeles',
140
+ latitude: 37.7749,
141
+ longitude: -122.4194,
142
+ house_system: 'R',
143
+ include_ruler_basics: true,
144
+ include_planetary_applications: false,
145
+ orb_degrees: 2.5,
146
+ });
147
+ });
148
+
61
149
  it('Given async state tool handlers, then they resolve to state payloads', async () => {
62
150
  const service = makeService();
63
151
  const ctx = { service: service as any, natalChart: { name: 'chart' } as any };
@@ -176,6 +264,27 @@ describe('When resolving tool specs from the registry', () => {
176
264
  expect(required.has('get_rise_set_times')).toBe(true);
177
265
  expect(required.has('generate_natal_chart')).toBe(true);
178
266
  expect(required.has('generate_transit_chart')).toBe(true);
267
+ expect(required.has('get_rising_sign_windows')).toBe(false);
268
+ expect(required.has('get_electional_context')).toBe(false);
269
+ });
270
+
271
+ it('Given get_transits schema and execution, then mode semantics are exposed and forwarded', async () => {
272
+ const spec = getToolSpec('get_transits');
273
+ expect(spec).toBeDefined();
274
+ const modeProp = spec?.inputSchema.properties?.mode as any;
275
+ expect(modeProp?.enum).toEqual(['snapshot', 'best_hit', 'forecast']);
276
+ expect(modeProp?.default).toBeUndefined();
277
+ expect(modeProp?.description).toContain('If omitted, legacy behavior is preserved');
278
+
279
+ const service = makeService();
280
+ await spec!.execute(
281
+ { service: service as any, natalChart: { name: 'chart' } as any },
282
+ { mode: 'forecast' }
283
+ );
284
+ expect(service.getTransits).toHaveBeenCalledWith(
285
+ expect.anything(),
286
+ expect.objectContaining({ mode: 'forecast' })
287
+ );
179
288
  });
180
289
 
181
290
  it('Given chart tools return no payload, then execution throws explicit errors', async () => {
@@ -215,8 +324,11 @@ describe('When resolving tool specs from the registry', () => {
215
324
  { timezone: 'UTC' }
216
325
  );
217
326
 
218
- expect(service.getRetrogradePlanets).toHaveBeenCalledWith('Asia/Tokyo');
219
- expect(service.getAsteroidPositions).toHaveBeenCalledWith('UTC');
327
+ expect(service.resolveReportingTimezone).toHaveBeenCalledWith(undefined, 'Asia/Tokyo');
328
+ expect(service.resolveReportingTimezone).toHaveBeenCalledWith(undefined, undefined);
329
+ expect(service.resolveReportingTimezone).toHaveBeenCalledWith('UTC', 'America/New_York');
330
+ expect(service.getRetrogradePlanets).toHaveBeenCalledWith('America/New_York');
331
+ expect(service.getAsteroidPositions).toHaveBeenCalledWith('America/New_York');
220
332
  expect(service.getNextEclipses).toHaveBeenCalledWith('UTC');
221
333
  });
222
334
  });
package/setup.sh DELETED
@@ -1,21 +0,0 @@
1
- #!/bin/bash
2
-
3
- echo "Setting up Astro MCP Server..."
4
-
5
- echo "Installing npm dependencies..."
6
- npm install
7
-
8
- echo "Building TypeScript..."
9
- npm run build
10
-
11
- echo ""
12
- echo "✅ Setup complete!"
13
- echo ""
14
- echo "The server uses WebAssembly Swiss Ephemeris - no data files needed!"
15
- echo ""
16
- echo "Next steps:"
17
- echo "1. Add this server to your MCP settings"
18
- echo "2. Use set_natal_chart to store birth data"
19
- echo "3. Query transits with your AI agent"
20
- echo ""
21
- echo "See README.md for detailed usage instructions."