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.
- package/.github/ISSUE_TEMPLATE/bug-report.yml +87 -0
- package/.github/ISSUE_TEMPLATE/capability-request.yml +117 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/paper-cut.yml +59 -0
- package/.github/pull_request_template.md +1 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/test.yml +2 -2
- package/AGENTS.md +46 -1
- package/DEVELOPER.md +78 -0
- package/README.md +128 -75
- package/SETUP.md +100 -41
- package/dist/astro-service.d.ts +51 -2
- package/dist/astro-service.js +660 -56
- package/dist/cli.js +31 -0
- package/dist/entrypoint.d.ts +13 -0
- package/dist/entrypoint.js +78 -0
- package/dist/ephemeris.d.ts +15 -0
- package/dist/ephemeris.js +33 -0
- package/dist/formatter.d.ts +5 -1
- package/dist/formatter.js +4 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +63 -114
- package/dist/loader.d.ts +1 -1
- package/dist/loader.js +61 -23
- package/dist/mcp-alias.d.ts +2 -0
- package/dist/mcp-alias.js +8 -0
- package/dist/time-utils.d.ts +8 -0
- package/dist/time-utils.js +16 -0
- package/dist/tool-registry.js +111 -5
- package/dist/tool-result.d.ts +8 -0
- package/dist/tool-result.js +39 -0
- package/dist/types.d.ts +79 -1
- package/docs/product/adrs/0001-mcp-vs-skill-boundary.md +96 -0
- package/docs/product/architecture-boundaries.md +223 -0
- package/docs/product/product-tenets.md +174 -0
- package/docs/releases/1.2.0-draft.md +48 -0
- package/package.json +7 -7
- package/skills/.curated/daily-brief/SKILL.md +75 -0
- package/skills/.curated/electional-overlay/SKILL.md +67 -0
- package/skills/.curated/weekly-overview/SKILL.md +73 -0
- package/skills/.system/write-skill/SKILL.md +90 -0
- package/src/astro-service.ts +861 -60
- package/src/cli.ts +84 -0
- package/src/entrypoint.ts +118 -0
- package/src/ephemeris.ts +44 -0
- package/src/formatter.ts +13 -1
- package/src/index.ts +77 -121
- package/src/loader.ts +69 -25
- package/src/mcp-alias.ts +10 -0
- package/src/time-utils.ts +18 -0
- package/src/tool-registry.ts +129 -9
- package/src/tool-result.ts +44 -0
- package/src/types.ts +91 -1
- package/tests/unit/astro-service.test.ts +751 -5
- package/tests/unit/cli-commands.test.ts +13 -0
- package/tests/unit/entrypoint.test.ts +67 -0
- package/tests/unit/error-mapping.test.ts +20 -0
- package/tests/unit/formatter.test.ts +6 -0
- package/tests/unit/tool-registry.test.ts +114 -2
- package/setup.sh +0 -21
package/dist/cli.js
CHANGED
|
@@ -308,6 +308,35 @@ 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-electional-context')
|
|
313
|
+
.description(mustTool('get_electional_context').description)
|
|
314
|
+
.requiredOption('--date <yyyy-mm-dd>', toolSchemaProperty('get_electional_context', 'date').description ?? 'Target local date')
|
|
315
|
+
.requiredOption('--time <hh:mm[:ss]>', toolSchemaProperty('get_electional_context', 'time').description ?? 'Target local time')
|
|
316
|
+
.requiredOption('--timezone <tz>', toolSchemaProperty('get_electional_context', 'timezone').description ?? 'Timezone')
|
|
317
|
+
.requiredOption('--latitude <number>', toolSchemaProperty('get_electional_context', 'latitude').description ?? 'Latitude')
|
|
318
|
+
.requiredOption('--longitude <number>', toolSchemaProperty('get_electional_context', 'longitude').description ?? 'Longitude')
|
|
319
|
+
.addOption(new Option('--house-system <system>', toolSchemaProperty('get_electional_context', 'house_system').description ?? 'House system').choices(['P', 'K', 'W', 'R']))
|
|
320
|
+
.option('--include-ruler-basics', toolSchemaProperty('get_electional_context', 'include_ruler_basics').description ??
|
|
321
|
+
'Include ascendant-ruler basics')
|
|
322
|
+
.option('--no-planetary-applications', 'Exclude applying major aspects from the electional context response')
|
|
323
|
+
.option('--orb-degrees <number>', toolSchemaProperty('get_electional_context', 'orb_degrees').description ?? 'Aspect orb')
|
|
324
|
+
.option('--pretty', 'Human-readable output instead of JSON')
|
|
325
|
+
.action(async (options) => {
|
|
326
|
+
const spec = mustTool('get_electional_context');
|
|
327
|
+
const result = await spec.execute({ service, natalChart: null }, {
|
|
328
|
+
date: options.date,
|
|
329
|
+
time: options.time,
|
|
330
|
+
timezone: options.timezone,
|
|
331
|
+
latitude: toNumber(options.latitude, 'latitude'),
|
|
332
|
+
longitude: toNumber(options.longitude, 'longitude'),
|
|
333
|
+
house_system: options.houseSystem,
|
|
334
|
+
include_ruler_basics: options.includeRulerBasics,
|
|
335
|
+
include_planetary_applications: options.planetaryApplications,
|
|
336
|
+
orb_degrees: options.orbDegrees == null ? undefined : toNumber(options.orbDegrees, 'orb-degrees'),
|
|
337
|
+
});
|
|
338
|
+
emitExecution(io, result, options.pretty ?? false);
|
|
339
|
+
});
|
|
311
340
|
const profiles = program
|
|
312
341
|
.command('profiles')
|
|
313
342
|
.description('Inspect and validate CLI profile files');
|
|
@@ -378,6 +407,7 @@ export async function runCli(argv, io = {
|
|
|
378
407
|
.option('--include-mundane', toolSchemaProperty('get_transits', 'include_mundane').description ??
|
|
379
408
|
'Include mundane positions')
|
|
380
409
|
.option('--days-ahead <number>', toolSchemaProperty('get_transits', 'days_ahead').description ?? 'Days ahead')
|
|
410
|
+
.addOption(new Option('--mode <mode>', toolSchemaProperty('get_transits', 'mode').description ?? 'Transit mode').choices(['snapshot', 'best_hit', 'forecast']))
|
|
381
411
|
.option('--max-orb <number>', toolSchemaProperty('get_transits', 'max_orb').description ?? 'Max orb')
|
|
382
412
|
.option('--exact-only', toolSchemaProperty('get_transits', 'exact_only').description ?? 'Exact only')
|
|
383
413
|
.option('--applying-only', toolSchemaProperty('get_transits', 'applying_only').description ?? 'Applying only')
|
|
@@ -392,6 +422,7 @@ export async function runCli(argv, io = {
|
|
|
392
422
|
categories,
|
|
393
423
|
include_mundane: options.includeMundane,
|
|
394
424
|
days_ahead: options.daysAhead == null ? undefined : toNumber(options.daysAhead, 'days-ahead'),
|
|
425
|
+
mode: options.mode,
|
|
395
426
|
max_orb: options.maxOrb == null ? undefined : toNumber(options.maxOrb, 'max-orb'),
|
|
396
427
|
exact_only: options.exactOnly,
|
|
397
428
|
applying_only: options.applyingOnly,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HouseSystem } from './types.js';
|
|
2
|
+
export interface McpStartupDefaults {
|
|
3
|
+
preferredTimezone?: string;
|
|
4
|
+
preferredHouseStyle?: HouseSystem;
|
|
5
|
+
weekdayLabels?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface EntrypointResolution {
|
|
8
|
+
mode: 'cli' | 'mcp';
|
|
9
|
+
cliArgv: string[];
|
|
10
|
+
mcpHelpRequested: boolean;
|
|
11
|
+
mcpStartupDefaults: Readonly<McpStartupDefaults>;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveEntrypoint(argv: string[], invokedPath?: string): EntrypointResolution;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isValidTimezone } from './time-utils.js';
|
|
3
|
+
const VALID_HOUSE_STYLES = new Set(['P', 'W', 'K', 'E']);
|
|
4
|
+
function readOptionValue(argv, index, flag) {
|
|
5
|
+
const current = argv[index];
|
|
6
|
+
const prefix = `${flag}=`;
|
|
7
|
+
if (current.startsWith(prefix)) {
|
|
8
|
+
return {
|
|
9
|
+
value: current.slice(prefix.length),
|
|
10
|
+
nextIndex: index,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const next = argv[index + 1];
|
|
14
|
+
if (next === undefined || next.startsWith('--')) {
|
|
15
|
+
throw new Error(`Missing value for ${flag}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
value: next,
|
|
19
|
+
nextIndex: index + 1,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function resolveEntrypoint(argv, invokedPath = process.argv[1] ?? '') {
|
|
23
|
+
let mode = path.basename(invokedPath).startsWith('e2a-mcp') ? 'mcp' : 'cli';
|
|
24
|
+
const cliArgv = [];
|
|
25
|
+
const defaults = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const arg = argv[i];
|
|
28
|
+
if (arg === '--mcp') {
|
|
29
|
+
mode = 'mcp';
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === '--weekday-labels') {
|
|
33
|
+
defaults.weekdayLabels = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === '--no-weekday-labels') {
|
|
37
|
+
defaults.weekdayLabels = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === '--preferred-tz' || arg.startsWith('--preferred-tz=')) {
|
|
41
|
+
const { value, nextIndex } = readOptionValue(argv, i, '--preferred-tz');
|
|
42
|
+
defaults.preferredTimezone = value;
|
|
43
|
+
i = nextIndex;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === '--preferred-house-style' || arg.startsWith('--preferred-house-style=')) {
|
|
47
|
+
const { value, nextIndex } = readOptionValue(argv, i, '--preferred-house-style');
|
|
48
|
+
defaults.preferredHouseStyle = value;
|
|
49
|
+
i = nextIndex;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
cliArgv.push(arg);
|
|
53
|
+
}
|
|
54
|
+
const usedMcpOnlyFlag = defaults.preferredTimezone !== undefined ||
|
|
55
|
+
defaults.preferredHouseStyle !== undefined ||
|
|
56
|
+
defaults.weekdayLabels !== undefined;
|
|
57
|
+
if (mode !== 'mcp' && usedMcpOnlyFlag) {
|
|
58
|
+
throw new Error('MCP startup defaults require MCP mode. Use e2a --mcp, or launch via the e2a-mcp compatibility alias.');
|
|
59
|
+
}
|
|
60
|
+
if (defaults.preferredTimezone && !isValidTimezone(defaults.preferredTimezone)) {
|
|
61
|
+
throw new Error(`Invalid timezone: ${defaults.preferredTimezone}`);
|
|
62
|
+
}
|
|
63
|
+
if (defaults.preferredHouseStyle && !VALID_HOUSE_STYLES.has(defaults.preferredHouseStyle)) {
|
|
64
|
+
throw new Error(`Invalid preferred house style: ${defaults.preferredHouseStyle} (must be one of P, W, K, E)`);
|
|
65
|
+
}
|
|
66
|
+
const mcpHelpRequested = mode === 'mcp' &&
|
|
67
|
+
cliArgv.length > 0 &&
|
|
68
|
+
cliArgv.every((arg) => arg === '--help' || arg === '-h');
|
|
69
|
+
if (mode === 'mcp' && cliArgv.length > 0 && !mcpHelpRequested) {
|
|
70
|
+
throw new Error(`Unexpected CLI arguments in MCP mode: ${cliArgv.join(' ')}`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
mode,
|
|
74
|
+
cliArgv,
|
|
75
|
+
mcpHelpRequested,
|
|
76
|
+
mcpStartupDefaults: Object.freeze({ ...defaults }),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/ephemeris.d.ts
CHANGED
|
@@ -85,6 +85,21 @@ export declare class EphemerisCalculator {
|
|
|
85
85
|
* For example, 350° and 10° have a distance of 20°, not 340°.
|
|
86
86
|
*/
|
|
87
87
|
calculateAspectAngle(lon1: number, lon2: number): number;
|
|
88
|
+
/**
|
|
89
|
+
* Convert ecliptic coordinates to local horizontal coordinates
|
|
90
|
+
*
|
|
91
|
+
* @param jd - Julian Day in UT
|
|
92
|
+
* @param position - Planetary ecliptic coordinates
|
|
93
|
+
* @param longitude - Observer longitude in degrees
|
|
94
|
+
* @param latitude - Observer latitude in degrees
|
|
95
|
+
* @param altitude - Observer altitude in meters (default: 0)
|
|
96
|
+
* @returns Horizontal coordinates including true and apparent altitude
|
|
97
|
+
*/
|
|
98
|
+
getHorizontalCoordinates(jd: number, position: Pick<PlanetPosition, 'longitude' | 'latitude' | 'distance'>, longitude: number, latitude: number, altitude?: number): {
|
|
99
|
+
azimuth: number;
|
|
100
|
+
trueAltitude: number;
|
|
101
|
+
apparentAltitude: number;
|
|
102
|
+
};
|
|
88
103
|
/**
|
|
89
104
|
* Find all exact times when planet reaches a specific longitude
|
|
90
105
|
*
|
package/dist/ephemeris.js
CHANGED
|
@@ -180,6 +180,39 @@ export class EphemerisCalculator {
|
|
|
180
180
|
}
|
|
181
181
|
return diff;
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Convert ecliptic coordinates to local horizontal coordinates
|
|
185
|
+
*
|
|
186
|
+
* @param jd - Julian Day in UT
|
|
187
|
+
* @param position - Planetary ecliptic coordinates
|
|
188
|
+
* @param longitude - Observer longitude in degrees
|
|
189
|
+
* @param latitude - Observer latitude in degrees
|
|
190
|
+
* @param altitude - Observer altitude in meters (default: 0)
|
|
191
|
+
* @returns Horizontal coordinates including true and apparent altitude
|
|
192
|
+
*/
|
|
193
|
+
getHorizontalCoordinates(jd, position, longitude, latitude, altitude = 0) {
|
|
194
|
+
if (!this.eph)
|
|
195
|
+
throw new Error('Ephemeris not initialized');
|
|
196
|
+
if (!Number.isFinite(jd)) {
|
|
197
|
+
throw new Error(`Invalid Julian Day: ${jd} (must be finite)`);
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isFinite(longitude) || !Number.isFinite(latitude) || !Number.isFinite(altitude)) {
|
|
200
|
+
throw new Error('Invalid geographic coordinates: longitude, latitude, and altitude must be finite');
|
|
201
|
+
}
|
|
202
|
+
const result = this.eph.azalt(jd, Constants.SE_ECL2HOR, [longitude, latitude, altitude], 0, 0, [
|
|
203
|
+
position.longitude,
|
|
204
|
+
position.latitude,
|
|
205
|
+
position.distance,
|
|
206
|
+
]);
|
|
207
|
+
if (!Array.isArray(result) || result.length < 3) {
|
|
208
|
+
throw new Error('Failed to calculate horizontal coordinates');
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
azimuth: result[0],
|
|
212
|
+
trueAltitude: result[1],
|
|
213
|
+
apparentAltitude: result[2],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
183
216
|
/**
|
|
184
217
|
* Find all exact times when planet reaches a specific longitude
|
|
185
218
|
*
|
package/dist/formatter.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
interface FormatInTimezoneOptions {
|
|
2
|
+
weekday?: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function formatInTimezone(date: Date, timezone: string, formatOptions?: FormatInTimezoneOptions): string;
|
|
2
5
|
export declare function formatDateOnly(date: Date, timezone: string): string;
|
|
6
|
+
export {};
|
package/dist/formatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function formatInTimezone(date, timezone) {
|
|
1
|
+
export function formatInTimezone(date, timezone, formatOptions = {}) {
|
|
2
2
|
const options = {
|
|
3
3
|
timeZone: timezone,
|
|
4
4
|
year: 'numeric',
|
|
@@ -9,6 +9,9 @@ export function formatInTimezone(date, timezone) {
|
|
|
9
9
|
hour12: true,
|
|
10
10
|
timeZoneName: 'short',
|
|
11
11
|
};
|
|
12
|
+
if (formatOptions.weekday) {
|
|
13
|
+
options.weekday = 'short';
|
|
14
|
+
}
|
|
12
15
|
return new Intl.DateTimeFormat('en-US', options).format(date);
|
|
13
16
|
}
|
|
14
17
|
export function formatDateOnly(date, timezone) {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,5 @@
|
|
|
11
11
|
* Uses Swiss Ephemeris for accurate astronomical calculations.
|
|
12
12
|
* All calculations are tropical (not sidereal) and geocentric.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
import type { McpStartupDefaults } from './entrypoint.js';
|
|
15
|
+
export declare function main(startupDefaults?: Readonly<McpStartupDefaults>): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -17,123 +17,72 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
17
17
|
import { AstroService } from './astro-service.js';
|
|
18
18
|
import { logger } from './logger.js';
|
|
19
19
|
import { getToolSpec, MCP_TOOL_SPECS } from './tool-registry.js';
|
|
20
|
-
import { mcpError, mcpResult, missingNatalChart } from './tool-result.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
description: spec.description,
|
|
53
|
-
inputSchema: spec.inputSchema,
|
|
54
|
-
})),
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
/**
|
|
58
|
-
* Handle MCP tool requests
|
|
59
|
-
*
|
|
60
|
-
* @param request - The MCP tool request
|
|
61
|
-
* @returns Tool response with data or error
|
|
62
|
-
* @throws Error for unhandled tools
|
|
63
|
-
*
|
|
64
|
-
* @remarks
|
|
65
|
-
* Routes requests to appropriate handlers. Initializes ephemeris on first use.
|
|
66
|
-
* All handlers return structured responses suitable for MCP clients.
|
|
67
|
-
*/
|
|
68
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
69
|
-
const { name, arguments: args = {} } = request.params;
|
|
70
|
-
try {
|
|
71
|
-
const spec = getToolSpec(name);
|
|
72
|
-
if (!spec) {
|
|
73
|
-
return mcpError({
|
|
74
|
-
code: 'INVALID_INPUT',
|
|
75
|
-
message: `Unknown tool: ${name}`,
|
|
76
|
-
retryable: false,
|
|
77
|
-
suggestedFix: 'Check the tool name against the list returned by ListTools.',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (spec.requiresNatalChart && !natalChart) {
|
|
81
|
-
return mcpError(missingNatalChart());
|
|
82
|
-
}
|
|
83
|
-
const result = await spec.execute({
|
|
84
|
-
service: astroService,
|
|
85
|
-
natalChart,
|
|
86
|
-
}, args);
|
|
87
|
-
if (result.kind === 'state') {
|
|
88
|
-
if (result.natalChart !== undefined) {
|
|
89
|
-
natalChart = result.natalChart;
|
|
20
|
+
import { mapToolErrorMessageToCode, mcpError, mcpResult, missingNatalChart, } from './tool-result.js';
|
|
21
|
+
function createServer(startupDefaults = {}) {
|
|
22
|
+
const server = new Server({
|
|
23
|
+
name: 'e2a-mcp',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
}, {
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
let natalChart = null;
|
|
31
|
+
const astroService = new AstroService({ mcpStartupDefaults: startupDefaults });
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
33
|
+
return {
|
|
34
|
+
tools: MCP_TOOL_SPECS.map((spec) => ({
|
|
35
|
+
name: spec.name,
|
|
36
|
+
description: spec.description,
|
|
37
|
+
inputSchema: spec.inputSchema,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
+
const { name, arguments: args = {} } = request.params;
|
|
43
|
+
try {
|
|
44
|
+
const spec = getToolSpec(name);
|
|
45
|
+
if (!spec) {
|
|
46
|
+
return mcpError({
|
|
47
|
+
code: 'INVALID_INPUT',
|
|
48
|
+
message: `Unknown tool: ${name}`,
|
|
49
|
+
retryable: false,
|
|
50
|
+
suggestedFix: 'Check the tool name against the list returned by ListTools.',
|
|
51
|
+
});
|
|
90
52
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
errorMessage.includes('missing julianDay')) {
|
|
106
|
-
code = 'INVALID_INPUT';
|
|
107
|
-
}
|
|
108
|
-
else if (errorMessage.includes('Invalid timezone') || errorMessage.includes('timezone')) {
|
|
109
|
-
code = 'INVALID_TIMEZONE';
|
|
110
|
-
}
|
|
111
|
-
else if (errorMessage.includes('Invalid house system')) {
|
|
112
|
-
code = 'INVALID_HOUSE_SYSTEM';
|
|
113
|
-
}
|
|
114
|
-
else if (errorMessage.includes('Ephemeris') || errorMessage.includes('ephemeris')) {
|
|
115
|
-
code = 'EPHEMERIS_COMPUTE_FAILED';
|
|
116
|
-
}
|
|
117
|
-
else if (errorMessage.includes('write') ||
|
|
118
|
-
errorMessage.includes('ENOENT') ||
|
|
119
|
-
errorMessage.includes('EACCES')) {
|
|
120
|
-
code = 'FILE_WRITE_FAILED';
|
|
121
|
-
}
|
|
122
|
-
else if (errorMessage.includes('render') || errorMessage.includes('chart')) {
|
|
123
|
-
code = 'CHART_RENDER_FAILED';
|
|
53
|
+
if (spec.requiresNatalChart && !natalChart) {
|
|
54
|
+
return mcpError(missingNatalChart());
|
|
55
|
+
}
|
|
56
|
+
const result = await spec.execute({
|
|
57
|
+
service: astroService,
|
|
58
|
+
natalChart,
|
|
59
|
+
}, args);
|
|
60
|
+
if (result.kind === 'state') {
|
|
61
|
+
if (result.natalChart !== undefined) {
|
|
62
|
+
natalChart = result.natalChart;
|
|
63
|
+
}
|
|
64
|
+
return mcpResult(result.data, result.text);
|
|
65
|
+
}
|
|
66
|
+
return { content: result.content };
|
|
124
67
|
}
|
|
125
|
-
|
|
126
|
-
|
|
68
|
+
catch (error) {
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
70
|
+
const code = mapToolErrorMessageToCode(errorMessage);
|
|
71
|
+
return mcpError({
|
|
72
|
+
code,
|
|
73
|
+
message: errorMessage,
|
|
74
|
+
retryable: code === 'EPHEMERIS_COMPUTE_FAILED' || code === 'FILE_WRITE_FAILED',
|
|
75
|
+
details: { tool: name },
|
|
76
|
+
});
|
|
127
77
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
export async function main() {
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
server,
|
|
81
|
+
astroService,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function main(startupDefaults = {}) {
|
|
85
|
+
const { server, astroService } = createServer(startupDefaults);
|
|
137
86
|
logger.info('Initializing Swiss Ephemeris');
|
|
138
87
|
await astroService.init();
|
|
139
88
|
logger.info('Ephemeris initialized');
|
package/dist/loader.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export
|
|
2
|
+
export declare function runEntrypoint(argv?: string[], invokedPath?: string): Promise<void>;
|
package/dist/loader.js
CHANGED
|
@@ -3,29 +3,67 @@ 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
globalThis.
|
|
10
|
-
globalThis.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
import { resolveEntrypoint } from './entrypoint.js';
|
|
7
|
+
function initializeRuntime() {
|
|
8
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
9
|
+
globalThis.window = dom.window;
|
|
10
|
+
globalThis.document = dom.window.document;
|
|
11
|
+
globalThis.self = globalThis; // Critical: astrochart checks for 'self' - see https://github.com/AstroDraw/AstroChart/issues/85
|
|
12
|
+
globalThis.SVGElement = dom.window.SVGElement;
|
|
13
|
+
// Polyfill fetch for file:// URLs used by chart tooling in Node.
|
|
14
|
+
const originalFetch = globalThis.fetch;
|
|
15
|
+
globalThis.fetch = async (url, ...args) => {
|
|
16
|
+
const urlStr = url.toString();
|
|
17
|
+
if (urlStr.startsWith('file://')) {
|
|
18
|
+
const filePath = fileURLToPath(urlStr);
|
|
19
|
+
const buffer = await readFile(filePath);
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return originalFetch(url, ...args);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function emitMcpHelp(invokedPath = process.argv[1] ?? '') {
|
|
29
|
+
const invokedName = invokedPath.includes('e2a-mcp') ? 'e2a-mcp' : 'e2a';
|
|
30
|
+
const launchExample = invokedName === 'e2a-mcp'
|
|
31
|
+
? 'e2a-mcp --preferred-tz America/Los_Angeles --preferred-house-style W --weekday-labels'
|
|
32
|
+
: 'e2a --mcp --preferred-tz America/Los_Angeles --preferred-house-style W --weekday-labels';
|
|
33
|
+
console.log(`Usage: ${invokedName === 'e2a-mcp' ? 'e2a-mcp' : 'e2a --mcp'} [options]
|
|
34
|
+
|
|
35
|
+
Start the ether-to-astro MCP server with optional deterministic startup defaults.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--preferred-tz <iana> Default reporting timezone for MCP surfaces
|
|
39
|
+
--preferred-house-style <P|W|K|E>
|
|
40
|
+
Default house-style preference for MCP surfaces
|
|
41
|
+
--weekday-labels Include weekday labels in human-readable MCP text output
|
|
42
|
+
--no-weekday-labels Disable weekday labels in human-readable MCP text output
|
|
43
|
+
-h, --help Show this help
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
${launchExample}`);
|
|
47
|
+
}
|
|
48
|
+
export async function runEntrypoint(argv = process.argv.slice(2), invokedPath = process.argv[1]) {
|
|
49
|
+
initializeRuntime();
|
|
50
|
+
const resolved = resolveEntrypoint(argv, invokedPath);
|
|
51
|
+
if (resolved.mode === 'mcp') {
|
|
52
|
+
if (resolved.mcpHelpRequested) {
|
|
53
|
+
emitMcpHelp(invokedPath);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { main } = await import('./index.js');
|
|
57
|
+
await main(resolved.mcpStartupDefaults);
|
|
58
|
+
return;
|
|
22
59
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
const { runCli } = await import('./cli.js');
|
|
61
|
+
const code = await runCli(resolved.cliArgv);
|
|
62
|
+
process.exit(code);
|
|
63
|
+
}
|
|
64
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
65
|
+
runEntrypoint().catch((error) => {
|
|
66
|
+
console.error('[ERROR] Failed to start program:', error);
|
|
29
67
|
process.exit(1);
|
|
30
68
|
});
|
|
31
|
-
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runEntrypoint } from './loader.js';
|
|
3
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
4
|
+
runEntrypoint(['--mcp', ...process.argv.slice(2)], process.argv[1]).catch((error) => {
|
|
5
|
+
console.error('[ERROR] Failed to start program:', error);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
}
|
package/dist/time-utils.d.ts
CHANGED
|
@@ -66,3 +66,11 @@ export declare function getTimezoneOffset(date: Date, timezone: string): number;
|
|
|
66
66
|
* @returns UTC Date representing the new local date/time
|
|
67
67
|
*/
|
|
68
68
|
export declare function addLocalDays(local: LocalDateTime, timezone: string, days: number): Date;
|
|
69
|
+
/**
|
|
70
|
+
* Format a UTC instant as a local ISO-8601 timestamp with timezone offset.
|
|
71
|
+
*
|
|
72
|
+
* @param utc - UTC Date object
|
|
73
|
+
* @param timezone - IANA timezone string
|
|
74
|
+
* @returns Local timestamp in the form YYYY-MM-DDTHH:mm:ss±HH:MM
|
|
75
|
+
*/
|
|
76
|
+
export declare function formatLocalTimestampWithOffset(utc: Date, timezone: string): string;
|
package/dist/time-utils.js
CHANGED
|
@@ -134,3 +134,19 @@ export function addLocalDays(local, timezone, days) {
|
|
|
134
134
|
const zonedDateTime = newPlainDateTime.toZonedDateTime(timezone);
|
|
135
135
|
return new Date(zonedDateTime.epochMilliseconds);
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Format a UTC instant as a local ISO-8601 timestamp with timezone offset.
|
|
139
|
+
*
|
|
140
|
+
* @param utc - UTC Date object
|
|
141
|
+
* @param timezone - IANA timezone string
|
|
142
|
+
* @returns Local timestamp in the form YYYY-MM-DDTHH:mm:ss±HH:MM
|
|
143
|
+
*/
|
|
144
|
+
export function formatLocalTimestampWithOffset(utc, timezone) {
|
|
145
|
+
const local = utcToLocal(utc, timezone);
|
|
146
|
+
const offsetMinutes = getTimezoneOffset(utc, timezone);
|
|
147
|
+
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
|
|
148
|
+
const offsetAbs = Math.abs(offsetMinutes);
|
|
149
|
+
const offsetHours = String(Math.floor(offsetAbs / 60)).padStart(2, '0');
|
|
150
|
+
const offsetMins = String(offsetAbs % 60).padStart(2, '0');
|
|
151
|
+
return `${String(local.year).padStart(4, '0')}-${String(local.month).padStart(2, '0')}-${String(local.day).padStart(2, '0')}T${String(local.hour).padStart(2, '0')}:${String(local.minute).padStart(2, '0')}:${String(local.second ?? 0).padStart(2, '0')}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
152
|
+
}
|