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
package/dist/cli.js ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { Command, Option } from 'commander';
4
+ import pc from 'picocolors';
5
+ import { loadResolvedProfileFile, ProfileStoreError, resolveProfileSelection, toNatalInput, } from './profile-store.js';
6
+ import { getToolSpec } from './tool-registry.js';
7
+ const E2A_BANNER = `
8
+ ███████╗██████╗ █████╗
9
+ ██╔════╝╚════██╗██╔══██╗
10
+ █████╗ █████╔╝███████║
11
+ ██╔══╝ ██╔═══╝ ██╔══██║
12
+ ███████╗███████╗██║ ██║
13
+ ╚══════╝╚══════╝╚═╝ ╚═╝
14
+ ether-to-astro
15
+ `;
16
+ function mustTool(name) {
17
+ const spec = getToolSpec(name);
18
+ if (!spec) {
19
+ throw new Error(`Missing tool specification: ${name}`);
20
+ }
21
+ return spec;
22
+ }
23
+ function toolSchemaProperty(toolName, propertyName) {
24
+ const schema = mustTool(toolName).inputSchema;
25
+ const prop = schema.properties?.[propertyName];
26
+ if (!prop) {
27
+ throw new Error(`Missing schema property: ${toolName}.${propertyName}`);
28
+ }
29
+ return prop;
30
+ }
31
+ function toFlag(propertyName) {
32
+ return propertyName.replaceAll('_', '-');
33
+ }
34
+ function addSchemaOption(command, toolName, propertyName, override) {
35
+ const prop = toolSchemaProperty(toolName, propertyName);
36
+ const flag = toFlag(propertyName);
37
+ const description = prop.description ?? propertyName;
38
+ const choices = override?.choices ?? prop.enum;
39
+ if (prop.type === 'boolean') {
40
+ command.option(`--${flag}`, description);
41
+ return command;
42
+ }
43
+ const valueHint = override?.valueHint ?? 'value';
44
+ if (choices && choices.length > 0) {
45
+ command.addOption(new Option(`--${flag} <${valueHint}>`, description).choices(choices));
46
+ return command;
47
+ }
48
+ command.option(`--${flag} <${valueHint}>`, description);
49
+ return command;
50
+ }
51
+ function toNumber(raw, field) {
52
+ if (raw == null) {
53
+ throw new Error(`Missing required argument --${field}`);
54
+ }
55
+ const parsed = Number(raw);
56
+ if (!Number.isFinite(parsed)) {
57
+ throw new Error(`Invalid numeric value for --${field}: ${raw}`);
58
+ }
59
+ return parsed;
60
+ }
61
+ async function loadNatalFromFile(path) {
62
+ const raw = await readFile(path, 'utf8');
63
+ const parsed = JSON.parse(raw);
64
+ return {
65
+ name: parsed.name ?? 'CLI User',
66
+ year: Number(parsed.year),
67
+ month: Number(parsed.month),
68
+ day: Number(parsed.day),
69
+ hour: Number(parsed.hour),
70
+ minute: Number(parsed.minute),
71
+ latitude: Number(parsed.latitude),
72
+ longitude: Number(parsed.longitude),
73
+ timezone: String(parsed.timezone),
74
+ house_system: parsed.house_system,
75
+ birth_time_disambiguation: parsed.birth_time_disambiguation,
76
+ };
77
+ }
78
+ async function loadPackageMeta() {
79
+ try {
80
+ const raw = await readFile(new URL('../package.json', import.meta.url), 'utf8');
81
+ return JSON.parse(raw);
82
+ }
83
+ catch {
84
+ return {};
85
+ }
86
+ }
87
+ async function resolveNatalInput(options) {
88
+ if (options.natalFile) {
89
+ return loadNatalFromFile(options.natalFile);
90
+ }
91
+ return {
92
+ name: options.name ?? 'CLI User',
93
+ year: toNumber(options.year, 'year'),
94
+ month: toNumber(options.month, 'month'),
95
+ day: toNumber(options.day, 'day'),
96
+ hour: toNumber(options.hour, 'hour'),
97
+ minute: toNumber(options.minute, 'minute'),
98
+ latitude: toNumber(options.latitude, 'latitude'),
99
+ longitude: toNumber(options.longitude, 'longitude'),
100
+ timezone: options.timezone ??
101
+ (() => {
102
+ throw new Error('Missing required argument --timezone');
103
+ })(),
104
+ house_system: options.houseSystem,
105
+ birth_time_disambiguation: options.birthTimeDisambiguation,
106
+ };
107
+ }
108
+ function hasInlineNatalInput(options) {
109
+ return (options.year !== undefined ||
110
+ options.month !== undefined ||
111
+ options.day !== undefined ||
112
+ options.hour !== undefined ||
113
+ options.minute !== undefined ||
114
+ options.latitude !== undefined ||
115
+ options.longitude !== undefined ||
116
+ options.timezone !== undefined);
117
+ }
118
+ function withProfileOptions(command) {
119
+ return command
120
+ .option('--profile <name>', 'Profile name to load from profile file')
121
+ .option('--profile-file <path>', 'Explicit path to profile file');
122
+ }
123
+ function emit(io, data, text, pretty) {
124
+ if (pretty) {
125
+ io.stdout(`${pc.bold(pc.magenta(E2A_BANNER))}\n${text}`);
126
+ return;
127
+ }
128
+ io.stdout(JSON.stringify(data, null, 2));
129
+ }
130
+ function emitExecution(io, result, pretty) {
131
+ if (result.kind === 'state') {
132
+ emit(io, result.data, result.text, pretty);
133
+ return;
134
+ }
135
+ const text = result.content
136
+ .filter((item) => item.type === 'text')
137
+ .map((item) => item.text)
138
+ .join('\n');
139
+ emit(io, { content: result.content }, text, pretty);
140
+ }
141
+ function errorPayload(error) {
142
+ if (error instanceof ProfileStoreError) {
143
+ return { code: error.code, message: error.message };
144
+ }
145
+ if (typeof error === 'object' && error !== null && 'code' in error && 'message' in error) {
146
+ const code = error.code;
147
+ const message = error.message;
148
+ if (typeof code === 'string' && typeof message === 'string') {
149
+ return { code, message };
150
+ }
151
+ }
152
+ if (error instanceof Error) {
153
+ return { code: 'CLI_ERROR', message: error.message };
154
+ }
155
+ return { code: 'CLI_ERROR', message: String(error) };
156
+ }
157
+ function commonNatalOptions(command) {
158
+ withProfileOptions(command);
159
+ command.option('--natal-file <path>', 'JSON file containing natal inputs');
160
+ addSchemaOption(command, 'set_natal_chart', 'name', { valueHint: 'name' });
161
+ addSchemaOption(command, 'set_natal_chart', 'year', { valueHint: 'number' });
162
+ addSchemaOption(command, 'set_natal_chart', 'month', { valueHint: 'number' });
163
+ addSchemaOption(command, 'set_natal_chart', 'day', { valueHint: 'number' });
164
+ addSchemaOption(command, 'set_natal_chart', 'hour', { valueHint: 'number' });
165
+ addSchemaOption(command, 'set_natal_chart', 'minute', { valueHint: 'number' });
166
+ addSchemaOption(command, 'set_natal_chart', 'latitude', { valueHint: 'number' });
167
+ addSchemaOption(command, 'set_natal_chart', 'longitude', { valueHint: 'number' });
168
+ addSchemaOption(command, 'set_natal_chart', 'timezone', { valueHint: 'tz' });
169
+ addSchemaOption(command, 'set_natal_chart', 'house_system', { valueHint: 'system' });
170
+ addSchemaOption(command, 'set_natal_chart', 'birth_time_disambiguation', { valueHint: 'mode' });
171
+ command.option('--pretty', 'Human-readable output instead of JSON');
172
+ return command;
173
+ }
174
+ async function withNatalChart(service, options, fn) {
175
+ let natalInput = null;
176
+ if (options.natalFile || hasInlineNatalInput(options)) {
177
+ natalInput = await resolveNatalInput(options);
178
+ }
179
+ else {
180
+ const profileSelection = await resolveProfileSelection({
181
+ profileName: options.profile,
182
+ profileFile: options.profileFile,
183
+ });
184
+ natalInput = profileSelection ? toNatalInput(profileSelection.profile) : null;
185
+ }
186
+ if (!natalInput) {
187
+ throw new ProfileStoreError('PROFILE_NOT_FOUND', 'No natal context resolved. Provide natal fields, --natal-file, or --profile.');
188
+ }
189
+ const setNatal = mustTool('set_natal_chart');
190
+ const result = await setNatal.execute({ service, natalChart: null }, natalInput);
191
+ if (result.kind !== 'state' || !result.natalChart) {
192
+ throw new Error('set_natal_chart did not return a natal chart');
193
+ }
194
+ await fn(result.natalChart, options.pretty ?? false);
195
+ }
196
+ async function resolveCommandTimezone(options) {
197
+ if (options.timezone) {
198
+ return options.timezone;
199
+ }
200
+ const explicitProfileRequested = Boolean(options.profile ||
201
+ options.profileFile ||
202
+ process.env.ASTRO_PROFILE ||
203
+ process.env.ASTRO_PROFILE_FILE);
204
+ try {
205
+ const profileSelection = await resolveProfileSelection({
206
+ profileName: options.profile,
207
+ profileFile: options.profileFile,
208
+ });
209
+ return profileSelection?.profile.timezone ?? 'UTC';
210
+ }
211
+ catch (error) {
212
+ if (error instanceof ProfileStoreError && !explicitProfileRequested) {
213
+ return 'UTC';
214
+ }
215
+ throw error;
216
+ }
217
+ }
218
+ function emitCliError(io, pretty, err) {
219
+ const payload = errorPayload(err);
220
+ if (pretty) {
221
+ io.stderr(pc.red(payload.message));
222
+ }
223
+ else {
224
+ io.stderr(JSON.stringify(payload, null, 2));
225
+ }
226
+ return 1;
227
+ }
228
+ export async function runCli(argv, io = {
229
+ stdout: (msg) => console.log(msg),
230
+ stderr: (msg) => console.error(msg),
231
+ }) {
232
+ globalThis.self ??= globalThis;
233
+ const { AstroService } = await import('./astro-service.js');
234
+ const service = new AstroService();
235
+ await service.init();
236
+ const pkg = await loadPackageMeta();
237
+ const program = new Command();
238
+ const isHelpInvocation = argv.includes('--help') || argv.includes('-h');
239
+ if (isHelpInvocation) {
240
+ io.stdout(pc.bold(pc.magenta(E2A_BANNER)));
241
+ }
242
+ program
243
+ .name('e2a')
244
+ .description(pkg.description ?? 'Single-shot astrology CLI (JSON-first, stateless)')
245
+ .version(pkg.version ?? '0.0.0', '-v, --version', 'Show package version')
246
+ .showHelpAfterError('(add --help for more details)')
247
+ .configureOutput({
248
+ writeErr: (str) => io.stderr(str.trimEnd()),
249
+ writeOut: (str) => {
250
+ const spaced = str.replace(/\n( {2}[\w-]+(?: \[[^\]]+\])?\s{2,})/g, '\n\n$1');
251
+ io.stdout(spaced.trimEnd());
252
+ },
253
+ });
254
+ commonNatalOptions(program.command('set-natal-chart').description(mustTool('set_natal_chart').description)).action(async (options) => {
255
+ const setNatal = mustTool('set_natal_chart');
256
+ const profileSelection = options.natalFile || hasInlineNatalInput(options)
257
+ ? null
258
+ : await resolveProfileSelection({
259
+ profileName: options.profile,
260
+ profileFile: options.profileFile,
261
+ });
262
+ const natalInput = options.natalFile || hasInlineNatalInput(options)
263
+ ? await resolveNatalInput(options)
264
+ : profileSelection
265
+ ? toNatalInput(profileSelection.profile)
266
+ : (() => {
267
+ throw new ProfileStoreError('PROFILE_NOT_FOUND', 'No natal context resolved. Provide natal fields, --natal-file, or --profile.');
268
+ })();
269
+ const result = await setNatal.execute({ service, natalChart: null }, natalInput);
270
+ emitExecution(io, result, options.pretty ?? false);
271
+ });
272
+ program
273
+ .command('get-retrograde-planets')
274
+ .description(mustTool('get_retrograde_planets').description)
275
+ .option('--timezone <tz>', 'Timezone label for output')
276
+ .option('--profile <name>', 'Profile name to load from profile file')
277
+ .option('--profile-file <path>', 'Explicit path to profile file')
278
+ .option('--pretty', 'Human-readable output instead of JSON')
279
+ .action(async (options) => {
280
+ const spec = mustTool('get_retrograde_planets');
281
+ const timezone = await resolveCommandTimezone(options);
282
+ const result = await spec.execute({ service, natalChart: null }, { timezone });
283
+ emitExecution(io, result, options.pretty ?? false);
284
+ });
285
+ program
286
+ .command('get-asteroid-positions')
287
+ .description(mustTool('get_asteroid_positions').description)
288
+ .option('--timezone <tz>', 'Timezone label for output')
289
+ .option('--profile <name>', 'Profile name to load from profile file')
290
+ .option('--profile-file <path>', 'Explicit path to profile file')
291
+ .option('--pretty', 'Human-readable output instead of JSON')
292
+ .action(async (options) => {
293
+ const spec = mustTool('get_asteroid_positions');
294
+ const timezone = await resolveCommandTimezone(options);
295
+ const result = await spec.execute({ service, natalChart: null }, { timezone });
296
+ emitExecution(io, result, options.pretty ?? false);
297
+ });
298
+ program
299
+ .command('get-next-eclipses')
300
+ .description(mustTool('get_next_eclipses').description)
301
+ .option('--timezone <tz>', 'Timezone label for output')
302
+ .option('--profile <name>', 'Profile name to load from profile file')
303
+ .option('--profile-file <path>', 'Explicit path to profile file')
304
+ .option('--pretty', 'Human-readable output instead of JSON')
305
+ .action(async (options) => {
306
+ const spec = mustTool('get_next_eclipses');
307
+ const timezone = await resolveCommandTimezone(options);
308
+ const result = await spec.execute({ service, natalChart: null }, { timezone });
309
+ emitExecution(io, result, options.pretty ?? false);
310
+ });
311
+ const profiles = program
312
+ .command('profiles')
313
+ .description('Inspect and validate CLI profile files');
314
+ profiles
315
+ .command('list')
316
+ .description('List available profiles')
317
+ .option('--profile-file <path>', 'Explicit path to profile file')
318
+ .option('--pretty', 'Human-readable output instead of JSON')
319
+ .action(async (options) => {
320
+ const { filePath, file } = await loadResolvedProfileFile({
321
+ profileFile: options.profileFile,
322
+ });
323
+ const names = Object.keys(file.profiles).sort();
324
+ const data = {
325
+ filePath,
326
+ defaultProfile: file.defaultProfile ?? null,
327
+ profiles: names,
328
+ };
329
+ const text = names.length === 0
330
+ ? `No profiles found in ${filePath}`
331
+ : `Profiles in ${filePath}:\n- ${names.join('\n- ')}`;
332
+ emit(io, data, text, options.pretty ?? false);
333
+ });
334
+ profiles
335
+ .command('show')
336
+ .description('Show a profile')
337
+ .requiredOption('--profile <name>', 'Profile name to show')
338
+ .option('--profile-file <path>', 'Explicit path to profile file')
339
+ .option('--pretty', 'Human-readable output instead of JSON')
340
+ .action(async (options) => {
341
+ const { filePath, file } = await loadResolvedProfileFile({
342
+ profileFile: options.profileFile,
343
+ });
344
+ const profileName = options.profile;
345
+ const profile = file.profiles[profileName];
346
+ if (!profile) {
347
+ throw new ProfileStoreError('PROFILE_NOT_FOUND', `Profile "${profileName}" not found in ${filePath}`);
348
+ }
349
+ const data = {
350
+ filePath,
351
+ profileName,
352
+ profile,
353
+ };
354
+ const text = `Profile "${profileName}" from ${filePath}`;
355
+ emit(io, data, text, options.pretty ?? false);
356
+ });
357
+ profiles
358
+ .command('validate')
359
+ .description('Validate profile file schema')
360
+ .option('--profile-file <path>', 'Explicit path to profile file')
361
+ .option('--pretty', 'Human-readable output instead of JSON')
362
+ .action(async (options) => {
363
+ const { filePath, file } = await loadResolvedProfileFile({
364
+ profileFile: options.profileFile,
365
+ });
366
+ const data = {
367
+ valid: true,
368
+ filePath,
369
+ profileCount: Object.keys(file.profiles).length,
370
+ defaultProfile: file.defaultProfile ?? null,
371
+ };
372
+ const text = `Profile file is valid: ${filePath}`;
373
+ emit(io, data, text, options.pretty ?? false);
374
+ });
375
+ commonNatalOptions(program.command('get-transits').description(mustTool('get_transits').description))
376
+ .option('--date <yyyy-mm-dd>', toolSchemaProperty('get_transits', 'date').description ?? 'Date for transits')
377
+ .option('--categories <list>', toolSchemaProperty('get_transits', 'categories').description ?? 'Categories')
378
+ .option('--include-mundane', toolSchemaProperty('get_transits', 'include_mundane').description ??
379
+ 'Include mundane positions')
380
+ .option('--days-ahead <number>', toolSchemaProperty('get_transits', 'days_ahead').description ?? 'Days ahead')
381
+ .option('--max-orb <number>', toolSchemaProperty('get_transits', 'max_orb').description ?? 'Max orb')
382
+ .option('--exact-only', toolSchemaProperty('get_transits', 'exact_only').description ?? 'Exact only')
383
+ .option('--applying-only', toolSchemaProperty('get_transits', 'applying_only').description ?? 'Applying only')
384
+ .action(async (options) => {
385
+ await withNatalChart(service, options, async (chart, pretty) => {
386
+ const categories = options.categories
387
+ ?.split(',')
388
+ .map((v) => v.trim())
389
+ .filter(Boolean);
390
+ const input = {
391
+ date: options.date,
392
+ categories,
393
+ include_mundane: options.includeMundane,
394
+ days_ahead: options.daysAhead == null ? undefined : toNumber(options.daysAhead, 'days-ahead'),
395
+ max_orb: options.maxOrb == null ? undefined : toNumber(options.maxOrb, 'max-orb'),
396
+ exact_only: options.exactOnly,
397
+ applying_only: options.applyingOnly,
398
+ };
399
+ const spec = mustTool('get_transits');
400
+ const result = await spec.execute({ service, natalChart: chart }, input);
401
+ emitExecution(io, result, pretty);
402
+ });
403
+ });
404
+ commonNatalOptions(program.command('get-houses').description(mustTool('get_houses').description))
405
+ .addOption(new Option('--system <system>', toolSchemaProperty('get_houses', 'system').description ?? 'House system override').choices(['P', 'W', 'K', 'E']))
406
+ .action(async (options) => {
407
+ await withNatalChart(service, options, async (chart, pretty) => {
408
+ const spec = mustTool('get_houses');
409
+ const result = await spec.execute({ service, natalChart: chart }, { system: options.system ?? options.houseSystem });
410
+ emitExecution(io, result, pretty);
411
+ });
412
+ });
413
+ commonNatalOptions(program.command('get-rise-set-times').description(mustTool('get_rise_set_times').description)).action(async (options) => {
414
+ await withNatalChart(service, options, async (chart, pretty) => {
415
+ const spec = mustTool('get_rise_set_times');
416
+ const result = await spec.execute({ service, natalChart: chart }, {});
417
+ emitExecution(io, result, pretty);
418
+ });
419
+ });
420
+ commonNatalOptions(program
421
+ .command('generate-natal-chart')
422
+ .description(mustTool('generate_natal_chart').description))
423
+ .addOption(new Option('--theme <theme>', toolSchemaProperty('generate_natal_chart', 'theme').description ?? 'Chart theme').choices(['light', 'dark']))
424
+ .addOption(new Option('--format <format>', toolSchemaProperty('generate_natal_chart', 'format').description ?? 'Output format')
425
+ .choices(['svg', 'png', 'webp'])
426
+ .default('svg'))
427
+ .option('--output-path <path>', toolSchemaProperty('generate_natal_chart', 'output_path').description ?? 'Output path')
428
+ .action(async (options) => {
429
+ await withNatalChart(service, options, async (chart, pretty) => {
430
+ const spec = mustTool('generate_natal_chart');
431
+ const result = await spec.execute({ service, natalChart: chart }, {
432
+ theme: options.theme,
433
+ format: options.format,
434
+ output_path: options.outputPath,
435
+ });
436
+ emitExecution(io, result, pretty);
437
+ });
438
+ });
439
+ commonNatalOptions(program
440
+ .command('generate-transit-chart')
441
+ .description(mustTool('generate_transit_chart').description))
442
+ .option('--date <yyyy-mm-dd>', toolSchemaProperty('generate_transit_chart', 'date').description ?? 'Transit date')
443
+ .addOption(new Option('--theme <theme>', toolSchemaProperty('generate_transit_chart', 'theme').description ?? 'Chart theme').choices(['light', 'dark']))
444
+ .addOption(new Option('--format <format>', toolSchemaProperty('generate_transit_chart', 'format').description ?? 'Output format')
445
+ .choices(['svg', 'png', 'webp'])
446
+ .default('svg'))
447
+ .option('--output-path <path>', toolSchemaProperty('generate_transit_chart', 'output_path').description ?? 'Output path')
448
+ .action(async (options) => {
449
+ await withNatalChart(service, options, async (chart, pretty) => {
450
+ const spec = mustTool('generate_transit_chart');
451
+ const result = await spec.execute({ service, natalChart: chart }, {
452
+ date: options.date,
453
+ theme: options.theme,
454
+ format: options.format,
455
+ output_path: options.outputPath,
456
+ });
457
+ emitExecution(io, result, pretty);
458
+ });
459
+ });
460
+ try {
461
+ await program.parseAsync(argv, { from: 'user' });
462
+ return 0;
463
+ }
464
+ catch (err) {
465
+ return emitCliError(io, argv.includes('--pretty'), err);
466
+ }
467
+ }
468
+ if (import.meta.url === `file://${process.argv[1]}`) {
469
+ runCli(process.argv.slice(2)).then((code) => {
470
+ process.exit(code);
471
+ });
472
+ }
@@ -0,0 +1,81 @@
1
+ export declare const LogLevel: {
2
+ readonly DEBUG: "DEBUG";
3
+ readonly INFO: "INFO";
4
+ readonly WARN: "WARN";
5
+ readonly ERROR: "ERROR";
6
+ };
7
+ export type LogLevelType = (typeof LogLevel)[keyof typeof LogLevel];
8
+ export declare const ErrorCategory: {
9
+ readonly EPHEMERIS: "EPHEMERIS";
10
+ readonly CALCULATION: "CALCULATION";
11
+ readonly STORAGE: "STORAGE";
12
+ readonly VALIDATION: "VALIDATION";
13
+ readonly CHART_RENDERING: "CHART_RENDERING";
14
+ readonly SERVER: "SERVER";
15
+ };
16
+ export type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory];
17
+ export declare const LIGHT_THEME_COLORS: string[];
18
+ export declare const DARK_THEME_COLORS: string[];
19
+ export declare const LIGHT_ASPECT_COLORS: {
20
+ conjunction: {
21
+ degree: number;
22
+ orbit: number;
23
+ color: string;
24
+ };
25
+ square: {
26
+ degree: number;
27
+ orbit: number;
28
+ color: string;
29
+ };
30
+ trine: {
31
+ degree: number;
32
+ orbit: number;
33
+ color: string;
34
+ };
35
+ opposition: {
36
+ degree: number;
37
+ orbit: number;
38
+ color: string;
39
+ };
40
+ sextile: {
41
+ degree: number;
42
+ orbit: number;
43
+ color: string;
44
+ };
45
+ };
46
+ export declare const DARK_ASPECT_COLORS: {
47
+ conjunction: {
48
+ degree: number;
49
+ orbit: number;
50
+ color: string;
51
+ };
52
+ square: {
53
+ degree: number;
54
+ orbit: number;
55
+ color: string;
56
+ };
57
+ trine: {
58
+ degree: number;
59
+ orbit: number;
60
+ color: string;
61
+ };
62
+ opposition: {
63
+ degree: number;
64
+ orbit: number;
65
+ color: string;
66
+ };
67
+ sextile: {
68
+ degree: number;
69
+ orbit: number;
70
+ color: string;
71
+ };
72
+ };
73
+ /**
74
+ * Determine chart theme based on time of day
75
+ * Dark theme: 6 PM - 6 AM (18:00 - 06:00)
76
+ * Light theme: 6 AM - 6 PM (06:00 - 18:00)
77
+ *
78
+ * @param timezone - Optional IANA timezone to use (e.g., 'America/New_York'). Defaults to server local time.
79
+ * @returns 'dark' or 'light' theme
80
+ */
81
+ export declare function getDefaultTheme(timezone?: string): 'light' | 'dark';
@@ -0,0 +1,76 @@
1
+ // Log levels
2
+ export const LogLevel = {
3
+ DEBUG: 'DEBUG',
4
+ INFO: 'INFO',
5
+ WARN: 'WARN',
6
+ ERROR: 'ERROR',
7
+ };
8
+ // Error categories
9
+ export const ErrorCategory = {
10
+ EPHEMERIS: 'EPHEMERIS',
11
+ CALCULATION: 'CALCULATION',
12
+ STORAGE: 'STORAGE',
13
+ VALIDATION: 'VALIDATION',
14
+ CHART_RENDERING: 'CHART_RENDERING',
15
+ SERVER: 'SERVER',
16
+ };
17
+ // Chart theme colors
18
+ export const LIGHT_THEME_COLORS = [
19
+ '#ffffff', // Aries - White (fire)
20
+ '#c1e6d1', // Taurus - Mint (earth)
21
+ '#ffffff', // Gemini - White (air)
22
+ '#c1e6d1', // Cancer - Mint (water)
23
+ '#ffffff', // Leo - White (fire)
24
+ '#c1e6d1', // Virgo - Mint (earth)
25
+ '#ffffff', // Libra - White (air)
26
+ '#c1e6d1', // Scorpio - Mint (water)
27
+ '#ffffff', // Sagittarius - White (fire)
28
+ '#c1e6d1', // Capricorn - Mint (earth)
29
+ '#ffffff', // Aquarius - White (air)
30
+ '#c1e6d1', // Pisces - Mint (water)
31
+ ];
32
+ export const DARK_THEME_COLORS = [
33
+ '#282c34', // Aries - Dark (fire)
34
+ '#8545b0', // Taurus - Purple (earth)
35
+ '#282c34', // Gemini - Dark (air)
36
+ '#8545b0', // Cancer - Purple (water)
37
+ '#282c34', // Leo - Dark (fire)
38
+ '#8545b0', // Virgo - Purple (earth)
39
+ '#282c34', // Libra - Dark (air)
40
+ '#8545b0', // Scorpio - Purple (water)
41
+ '#282c34', // Sagittarius - Dark (fire)
42
+ '#8545b0', // Capricorn - Purple (earth)
43
+ '#282c34', // Aquarius - Dark (air)
44
+ '#8545b0', // Pisces - Purple (water)
45
+ ];
46
+ // Aspect colors for light theme
47
+ export const LIGHT_ASPECT_COLORS = {
48
+ conjunction: { degree: 0, orbit: 10, color: 'transparent' },
49
+ square: { degree: 90, orbit: 8, color: '#fb923c' }, // Orange
50
+ trine: { degree: 120, orbit: 8, color: '#34d399' }, // Emerald
51
+ opposition: { degree: 180, orbit: 10, color: '#a78bfa' }, // Purple
52
+ sextile: { degree: 60, orbit: 6, color: '#22d3ee' }, // Cyan
53
+ };
54
+ // Aspect colors for dark theme
55
+ export const DARK_ASPECT_COLORS = {
56
+ conjunction: { degree: 0, orbit: 10, color: 'transparent' },
57
+ square: { degree: 90, orbit: 8, color: '#f97316' }, // Orange
58
+ trine: { degree: 120, orbit: 8, color: '#10b981' }, // Emerald
59
+ opposition: { degree: 180, orbit: 10, color: '#8b5cf6' }, // Purple
60
+ sextile: { degree: 60, orbit: 6, color: '#06b6d4' }, // Cyan
61
+ };
62
+ /**
63
+ * Determine chart theme based on time of day
64
+ * Dark theme: 6 PM - 6 AM (18:00 - 06:00)
65
+ * Light theme: 6 AM - 6 PM (06:00 - 18:00)
66
+ *
67
+ * @param timezone - Optional IANA timezone to use (e.g., 'America/New_York'). Defaults to server local time.
68
+ * @returns 'dark' or 'light' theme
69
+ */
70
+ export function getDefaultTheme(timezone) {
71
+ const now = new Date();
72
+ const hour = timezone
73
+ ? Number.parseInt(now.toLocaleString('en-US', { timeZone: timezone, hour: '2-digit', hour12: false }), 10)
74
+ : now.getHours();
75
+ return hour >= 18 || hour < 6 ? 'dark' : 'light';
76
+ }