klio 1.5.6 → 1.5.7

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/README.md CHANGED
@@ -36,6 +36,25 @@ npm install -g klio
36
36
  klio [options]
37
37
  ```
38
38
 
39
+ ### Planet Selection
40
+
41
+ By default, Klio uses traditional planets plus Chiron. You can customize which planets and asteroids are active:
42
+
43
+ ```bash
44
+ # Show currently active planets
45
+ klio --active-planets
46
+
47
+ # Set custom active planets (comma-separated)
48
+ klio --planets "Sun,Moon,Mercury,Venus,Chiron,Hygiea"
49
+
50
+ klio --planets "Sun,Moon,Mercury,Venus,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto"
51
+ ```
52
+
53
+ Available planets: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, Chiron, Hygiea
54
+
55
+ > **Note:** By default, only Chiron is active among asteroids. Other asteroids like Hygiea must be explicitly enabled.
56
+ =======
57
+
39
58
  ### Web Interface
40
59
 
41
60
  AstroCLI includes a web-based GUI that you can launch with the `--gui` flag:
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.5.6",
3
+ "version": "1.5.7",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -13,6 +13,25 @@ const planets = {
13
13
  chiron: 15
14
14
  };
15
15
 
16
+ // Asteroid constants for Swiss Ephemeris
17
+ // Note: These use the SE_AST_OFFSET (10000) + asteroid number
18
+ const asteroidConstants = {
19
+ ceres: 10001, // Ceres = 10000 + 1
20
+ pallas: 10002, // Pallas = 10000 + 2
21
+ hygiea: 10010, // Hygiea = 10000 + 10
22
+ juno: 10003, // Juno = 10000 + 3
23
+ vesta: 10004 // Vesta = 10000 + 4
24
+ };
25
+
26
+ // Asteroid names for Swiss Ephemeris (used as string identifiers)
27
+ const asteroidNames = {
28
+ ceres: 'Ceres',
29
+ pallas: 'Pallas',
30
+ hygiea: 'Hygiea',
31
+ juno: 'Juno',
32
+ vesta: 'Vesta'
33
+ };
34
+
16
35
  // Zodiac signs (English)
17
36
  const signs = [
18
37
  'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
@@ -54,8 +73,18 @@ const dignities = {
54
73
  15: { sign: 'Virgo', exaltation: 'Pisces', fall: 'Virgo', detriment: 'Pisces' } // Chiron
55
74
  };
56
75
 
76
+ // Asteroid dignities (using asteroid constants as keys)
77
+ const asteroidDignities = {
78
+ 10001: { sign: 'Taurus', exaltation: 'Virgo', fall: 'Scorpio', detriment: 'Scorpio' }, // Ceres
79
+ 10002: { sign: 'Libra', exaltation: 'Aries', fall: 'Libra', detriment: 'Aries' }, // Pallas
80
+ 10010: { sign: 'Virgo', exaltation: 'Pisces', fall: 'Virgo', detriment: 'Pisces' } // Hygiea
81
+ };
82
+
57
83
  module.exports = {
58
84
  planets,
85
+ asteroidNames,
86
+ asteroidConstants,
87
+ asteroidDignities,
59
88
  signs,
60
89
  elements,
61
90
  signTypes,
@@ -3,7 +3,7 @@ const fs = require('fs');
3
3
  const moment = require('moment-timezone');
4
4
  const csvParser = require('csv-parser');
5
5
  const { parse } = require('csv-parse/sync');
6
- const { planets, signs, elements, signTypes, decans, dignities } = require('./astrologyConstants');
6
+ const { planets, asteroidNames, asteroidConstants, asteroidDignities, signs, elements, signTypes, decans, dignities } = require('./astrologyConstants');
7
7
  const { loadConfig } = require('../config/configService');
8
8
  const path = require('path');
9
9
 
@@ -49,6 +49,17 @@ function getCurrentTimeInTimezone() {
49
49
  };
50
50
  }
51
51
 
52
+ // Function to get active planets (filtered by configuration)
53
+ function getActivePlanets() {
54
+ try {
55
+ const { getActivePlanets } = require('../config/configService');
56
+ return getActivePlanets();
57
+ } catch (error) {
58
+ // Fallback to default active planets if config service is not available
59
+ return ['sun', 'moon', 'mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune', 'pluto', 'chiron'];
60
+ }
61
+ }
62
+
52
63
  // Function to load birth data from configuration
53
64
  function getBirthDataFromConfig(userId = null) {
54
65
  try {
@@ -377,7 +388,14 @@ function getPlanetHouse(planetLongitude, houseCusps) {
377
388
  // Function to calculate astrological data
378
389
  function getAstrologicalData(planetName, customDate = null) {
379
390
  const planet = planets[planetName];
380
- if (planet === undefined) {
391
+ const isAsteroid = asteroidNames[planetName] !== undefined;
392
+ let asteroidId = null;
393
+
394
+ if (isAsteroid) {
395
+ asteroidId = asteroidConstants[planetName];
396
+ }
397
+
398
+ if (planet === undefined && !isAsteroid) {
381
399
  console.error(`Invalid planet: ${planetName}. Available planets:`, Object.keys(planets).join(', '));
382
400
  process.exit(1);
383
401
  }
@@ -433,16 +451,53 @@ function getAstrologicalData(planetName, customDate = null) {
433
451
 
434
452
  const julianDay = calculateJulianDayUTC({ year: calcYear, month: calcMonth, day: calcDay, hour: calcHour, minute: calcMinute }, timezoneOffsetMinutes);
435
453
  const flag = swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED;
436
- let result = swisseph.swe_calc_ut(julianDay, planet, flag);
454
+ let result;
437
455
 
438
- if (result.error && result.error.includes('not found')) {
439
- // Fallback to Moshier if ephemeris files are missing
440
- const moshierFlag = swisseph.SEFLG_MOSEPH | swisseph.SEFLG_SPEED;
441
- result = swisseph.swe_calc_ut(julianDay, planet, moshierFlag);
442
- }
456
+ if (isAsteroid) {
457
+ // Handle asteroid calculations using proper Swiss Ephemeris constants
458
+ if (asteroidId) {
459
+ // Use the same swe_calc_ut function but with asteroid ID
460
+ result = swisseph.swe_calc_ut(julianDay, asteroidId, flag);
461
+
462
+ if (result.error && result.error.includes('not found')) {
463
+ // Fallback to Moshier if ephemeris files are missing
464
+ const moshierFlag = swisseph.SEFLG_MOSEPH | swisseph.SEFLG_SPEED;
465
+ result = swisseph.swe_calc_ut(julianDay, asteroidId, moshierFlag);
466
+ }
467
+
468
+ if (result.error) {
469
+ console.warn(`⚠️ Could not calculate asteroid ${planetName}: ${result.error}`);
470
+ // Fallback to placeholder data if calculation fails
471
+ result = {
472
+ longitude: 0,
473
+ latitude: 0,
474
+ distance: 0,
475
+ longitudeSpeed: 0
476
+ };
477
+ }
478
+ } else {
479
+ console.warn(`⚠️ Unknown asteroid: ${planetName}`);
480
+ // Fallback to placeholder data for unknown asteroids
481
+ result = {
482
+ longitude: 0,
483
+ latitude: 0,
484
+ distance: 0,
485
+ longitudeSpeed: 0
486
+ };
487
+ }
488
+ } else {
489
+ // Handle regular planet calculations
490
+ result = swisseph.swe_calc_ut(julianDay, planet, flag);
443
491
 
444
- if (result.error) {
445
- throw new Error(result.error);
492
+ if (result.error && result.error.includes('not found')) {
493
+ // Fallback to Moshier if ephemeris files are missing
494
+ const moshierFlag = swisseph.SEFLG_MOSEPH | swisseph.SEFLG_SPEED;
495
+ result = swisseph.swe_calc_ut(julianDay, planet, moshierFlag);
496
+ }
497
+
498
+ if (result.error) {
499
+ throw new Error(result.error);
500
+ }
446
501
  }
447
502
 
448
503
  const longitude = result.longitude;
@@ -452,16 +507,20 @@ function getAstrologicalData(planetName, customDate = null) {
452
507
  const element = elements[signIndex];
453
508
  const decan = decans[Math.floor((longitude % 30) / 10)];
454
509
 
455
- const dignityInfo = dignities[planet];
510
+ // Get dignity info - use asteroid dignities for asteroids, regular dignities for planets
511
+ const dignityInfo = isAsteroid ? asteroidDignities[asteroidId] : dignities[planet];
456
512
  let dignity = 'Neutral';
457
- if (sign === dignityInfo.sign) {
458
- dignity = 'Ruler';
459
- } else if (sign === dignityInfo.exaltation) {
460
- dignity = 'Exaltation';
461
- } else if (sign === dignityInfo.fall) {
462
- dignity = 'Fall';
463
- } else if (sign === dignityInfo.detriment) {
464
- dignity = 'Detriment';
513
+
514
+ if (dignityInfo) {
515
+ if (sign === dignityInfo.sign) {
516
+ dignity = 'Ruler';
517
+ } else if (sign === dignityInfo.exaltation) {
518
+ dignity = 'Exaltation';
519
+ } else if (sign === dignityInfo.fall) {
520
+ dignity = 'Fall';
521
+ } else if (sign === dignityInfo.detriment) {
522
+ dignity = 'Detriment';
523
+ }
465
524
  }
466
525
 
467
526
  return {
@@ -510,8 +569,16 @@ function getCriticalPlanets(customDate = null) {
510
569
  const offsetMinutes = timezone ? getTimezoneOffset(timeData, timezone) : -new Date().getTimezoneOffset();
511
570
  const julianDay = calculateJulianDayUTC(timeData, offsetMinutes);
512
571
 
513
- // Calculate positions of all planets
572
+ // Get active planets from configuration
573
+ const activePlanets = getActivePlanets();
574
+
575
+ // Calculate positions of active planets only
514
576
  for (const [name, planetId] of Object.entries(planets)) {
577
+ // Skip planets that are not in the active list
578
+ if (!activePlanets.includes(name)) {
579
+ continue;
580
+ }
581
+
515
582
  const flag = swisseph.SEFLG_SWIEPH | swisseph.SEFLG_SPEED;
516
583
  let result = swisseph.swe_calc_ut(julianDay, planetId, flag);
517
584
 
@@ -772,12 +839,12 @@ function showPlanetComboAspects(planetNames, dateComponents, useBirthData = fals
772
839
  // Function to calculate all active aspects between all planets
773
840
  function getAllActiveAspects(dateComponents) {
774
841
  const allAspects = [];
775
- const planetNames = Object.keys(planets);
842
+ const activePlanets = getActivePlanets();
776
843
  const seenAspects = new Set(); // To avoid duplicates
777
844
 
778
- // Calculate aspects for all planet pairs
779
- for (let i = 0; i < planetNames.length; i++) {
780
- const planet1 = planetNames[i];
845
+ // Calculate aspects for active planet pairs only
846
+ for (let i = 0; i < activePlanets.length; i++) {
847
+ const planet1 = activePlanets[i];
781
848
  const aspects = calculatePlanetAspects(planet1, dateComponents, true); // Always use Huber orbs
782
849
 
783
850
  aspects.forEach(aspect => {
@@ -1349,16 +1416,25 @@ async function calculatePersonalTransits(transitDate = null, birthData = null) {
1349
1416
  const birthJulianDay = calculateJulianDayUTC(birthData, getTimezoneOffset(birthData, birthData.location?.timezone || 'Europe/Zurich'));
1350
1417
  const houses = await calculateHouses(birthJulianDay, 'K', true, birthData.location || null); // Always Koch house system for transits
1351
1418
 
1352
- // Calculate current planet positions (Transit)
1419
+ // Get active planets from configuration
1420
+ const activePlanets = getActivePlanets();
1421
+
1422
+ // Calculate current planet positions (Transit) for active planets only
1353
1423
  const transitPlanets = {};
1354
1424
  for (const [name, planetId] of Object.entries(planets)) {
1425
+ if (!activePlanets.includes(name)) {
1426
+ continue;
1427
+ }
1355
1428
  const data = getAstrologicalData(name, transitDate);
1356
1429
  transitPlanets[name] = data;
1357
1430
  }
1358
1431
 
1359
- // Calculate birth planet positions (Radix)
1432
+ // Calculate birth planet positions (Radix) for active planets only
1360
1433
  const birthPlanets = {};
1361
1434
  for (const [name, planetId] of Object.entries(planets)) {
1435
+ if (!activePlanets.includes(name)) {
1436
+ continue;
1437
+ }
1362
1438
  const data = getAstrologicalData(name, birthData);
1363
1439
  birthPlanets[name] = data;
1364
1440
  }
@@ -1399,16 +1475,25 @@ function calculatePersonalTransitAspects(transitDate = null, birthData = null, t
1399
1475
  transitDate = getCurrentTimeInTimezone();
1400
1476
  }
1401
1477
 
1402
- // Calculate current planet positions (Transit)
1478
+ // Get active planets from configuration
1479
+ const activePlanets = getActivePlanets();
1480
+
1481
+ // Calculate current planet positions (Transit) for active planets only
1403
1482
  const transitPlanets = {};
1404
1483
  for (const [name, planetId] of Object.entries(planets)) {
1484
+ if (!activePlanets.includes(name)) {
1485
+ continue;
1486
+ }
1405
1487
  const data = getAstrologicalData(name, transitDate);
1406
1488
  transitPlanets[name] = data.longitude;
1407
1489
  }
1408
1490
 
1409
- // Calculate birth planet positions (Radix)
1491
+ // Calculate birth planet positions (Radix) for active planets only
1410
1492
  const birthPlanets = {};
1411
1493
  for (const [name, planetId] of Object.entries(planets)) {
1494
+ if (!activePlanets.includes(name)) {
1495
+ continue;
1496
+ }
1412
1497
  const data = getAstrologicalData(name, birthData);
1413
1498
  birthPlanets[name] = data.longitude;
1414
1499
  }
@@ -1606,15 +1691,23 @@ function analyzeElementDistribution(dateComponents, useBirthData = false) {
1606
1691
 
1607
1692
  const planetElements = {};
1608
1693
 
1609
- // Calculate elements of all planets
1694
+ // Get active planets from configuration
1695
+ const activePlanets = getActivePlanets();
1696
+
1697
+ // Calculate elements of active planets only
1610
1698
  for (const [name, planetId] of Object.entries(planets)) {
1699
+ // Skip planets that are not in the active list
1700
+ if (!activePlanets.includes(name)) {
1701
+ continue;
1702
+ }
1703
+
1611
1704
  const data = getAstrologicalData(name, dateComponents);
1612
1705
  elementCounts[data.element]++;
1613
1706
  planetElements[name] = data.element;
1614
1707
  }
1615
1708
 
1616
- // Calculate total number of planets
1617
- const totalPlanets = Object.keys(planets).length;
1709
+ // Calculate total number of active planets
1710
+ const totalPlanets = activePlanets.length;
1618
1711
 
1619
1712
  // Calculate percentages
1620
1713
  const elementPercentages = {};
@@ -1859,7 +1952,7 @@ function parseFilterCriteria(filterString) {
1859
1952
  const filterConditions = filterString.split(',').map(cond => cond.trim());
1860
1953
 
1861
1954
  const criteria = [];
1862
- const operatorRegex = /^(.+?)\s*(>=|<=|!=|=|>|<|\*|!)\s*(.+)$/;
1955
+ const operatorRegex = /^(.+?)\s*(>=|<=|!=|=|>|<)\s*(.+)$/;
1863
1956
 
1864
1957
  for (const condition of filterConditions) {
1865
1958
  const operatorMatch = condition.match(operatorRegex);
@@ -1872,23 +1965,33 @@ function parseFilterCriteria(filterString) {
1872
1965
  continue;
1873
1966
  }
1874
1967
 
1875
- // Check for contains (*) or excludes (!) operators in the value part
1876
- const containsMatch = condition.match(/^(.+?):\*(.+)$/);
1877
- if (containsMatch) {
1878
- criteria.push({
1879
- column: containsMatch[1].trim(),
1880
- operator: '*',
1881
- value: containsMatch[2].trim()
1882
- });
1883
- continue;
1884
- }
1885
-
1886
- const excludesMatch = condition.match(/^(.+?):!(.+)$/);
1887
- if (excludesMatch) {
1968
+ // Check for wildcard patterns in the value (new enhanced syntax)
1969
+ const wildcardMatch = condition.match(/^([^:]+):(.+)$/);
1970
+ if (wildcardMatch) {
1971
+ const column = wildcardMatch[1].trim();
1972
+ const value = wildcardMatch[2].trim();
1973
+
1974
+ // Remove * and ! from the value for contains/excludes operations
1975
+ const cleanValue = value.replace(/^[*!]+/, '').replace(/[*!]+$/, '');
1976
+
1977
+ // Check if value contains * or starts with !
1978
+ if (value.includes('*') || value.startsWith('!')) {
1979
+ // Use * operator for contains matching (remove * from value)
1980
+ // Use ! operator for excludes matching (remove ! from value)
1981
+ const operator = value.startsWith('!') ? '!' : '*';
1982
+ criteria.push({
1983
+ column: column,
1984
+ operator: operator,
1985
+ value: cleanValue
1986
+ });
1987
+ continue;
1988
+ }
1989
+
1990
+ // Regular equality match
1888
1991
  criteria.push({
1889
- column: excludesMatch[1].trim(),
1890
- operator: '!',
1891
- value: excludesMatch[2].trim()
1992
+ column: column,
1993
+ operator: '=',
1994
+ value: value
1892
1995
  });
1893
1996
  continue;
1894
1997
  }
@@ -2074,6 +2177,13 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2074
2177
  const trimmed = value != null ? value.toString().trim() : '';
2075
2178
  return /^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2076
2179
  });
2180
+
2181
+ // Second pass alternative: look for DD.MM.YYYY format (optional time)
2182
+ const ddMmYyyyDotColumns = Object.keys(data).filter(key => {
2183
+ const value = data[key];
2184
+ const trimmed = value != null ? value.toString().trim() : '';
2185
+ return /^\d{2}\.\d{2}\.\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2186
+ });
2077
2187
 
2078
2188
  // Third pass: look for Unix timestamps (10 or 13 digits)
2079
2189
  const unixTimestampColumns = Object.keys(data).filter(key => {
@@ -2093,8 +2203,8 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2093
2203
  return moment(trimmed, moment.ISO_8601, true).isValid();
2094
2204
  });
2095
2205
 
2096
- // Prioritize columns: YYYY-MM-DD, DD/MM/YYYY, Unix timestamps, then ISO dates
2097
- datetimeColumns.push(...yyyyMmDdColumns, ...ddMmYyyyColumns, ...unixTimestampColumns, ...isoDateColumns);
2206
+ // Prioritize columns: YYYY-MM-DD, DD/MM/YYYY, DD.MM.YYYY, Unix timestamps, then ISO dates
2207
+ datetimeColumns.push(...yyyyMmDdColumns, ...ddMmYyyyColumns, ...ddMmYyyyDotColumns, ...unixTimestampColumns, ...isoDateColumns);
2098
2208
 
2099
2209
  if (datetimeColumns.length > 0) {
2100
2210
  datetimeValue = data[datetimeColumns[0]];
@@ -2121,6 +2231,15 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2121
2231
  if (datetime.isValid() && !hasTime) {
2122
2232
  datetime = datetime.hour(12).minute(0);
2123
2233
  }
2234
+ } else if (/^\d{2}\.\d{2}\.\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(datetimeValueTrimmed)) {
2235
+ // Handle as DD.MM.YYYY (optional time)
2236
+ const hasTime = /\d{1,2}:\d{2}/.test(datetimeValueTrimmed);
2237
+ // Replace dots with slashes to use the same parsing logic
2238
+ const datetimeValueWithSlashes = datetimeValueTrimmed.replace(/\./g, '/');
2239
+ datetime = moment(datetimeValueWithSlashes, ['DD/MM/YYYY HH:mm:ss', 'DD/MM/YYYY HH:mm', 'DD/MM/YYYY'], true);
2240
+ if (datetime.isValid() && !hasTime) {
2241
+ datetime = datetime.hour(12).minute(0);
2242
+ }
2124
2243
  } else {
2125
2244
  // Handle as ISO-8601 date
2126
2245
  datetime = moment(datetimeValueTrimmed);
@@ -2324,9 +2443,11 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2324
2443
  const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
2325
2444
  // Check for DD/MM/YYYY date format
2326
2445
  const isDDMMYYYY = /^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2446
+ // Check for DD.MM.YYYY date format
2447
+ const isDDMMYYYYDot = /^\d{2}\.\d{2}\.\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2327
2448
  // Check for Unix-Timestamp (number with 10 or 13 digits)
2328
2449
  const isUnixTimestamp = /^\d{10,13}$/.test(trimmed);
2329
- return isISO || isYYYYMMDD || isDDMMYYYY || isUnixTimestamp;
2450
+ return isISO || isYYYYMMDD || isDDMMYYYY || isDDMMYYYYDot || isUnixTimestamp;
2330
2451
  });
2331
2452
 
2332
2453
  if (datetimeColumns.length > 0) {
@@ -2354,6 +2475,15 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2354
2475
  if (datetime.isValid() && !hasTime) {
2355
2476
  datetime = datetime.hour(12).minute(0);
2356
2477
  }
2478
+ } else if (/^\d{2}\.\d{2}\.\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(datetimeValueTrimmed)) {
2479
+ // Handle as DD.MM.YYYY (optional time)
2480
+ const hasTime = /\d{1,2}:\d{2}/.test(datetimeValueTrimmed);
2481
+ // Replace dots with slashes to use the same parsing logic
2482
+ const datetimeValueWithSlashes = datetimeValueTrimmed.replace(/\./g, '/');
2483
+ datetime = moment(datetimeValueWithSlashes, ['DD/MM/YYYY HH:mm:ss', 'DD/MM/YYYY HH:mm', 'DD/MM/YYYY'], true);
2484
+ if (datetime.isValid() && !hasTime) {
2485
+ datetime = datetime.hour(12).minute(0);
2486
+ }
2357
2487
  } else {
2358
2488
  // Handle as ISO-8601 date
2359
2489
  datetime = moment(datetimeValueTrimmed);
@@ -3053,7 +3183,10 @@ module.exports = {
3053
3183
  calculateNextPlanetIngress,
3054
3184
  calculateAstrologicalAngles,
3055
3185
  longitudeToSignDegree,
3056
- calculateTransitFrequency
3186
+ calculateTransitFrequency,
3187
+ // Filter functions for testing
3188
+ parseFilterCriteria,
3189
+ applyFilter
3057
3190
  };
3058
3191
 
3059
3192
  // Function to calculate how often a transit occurs between two planets
package/src/cli/cli.js CHANGED
@@ -393,7 +393,7 @@ program
393
393
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
394
394
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
395
395
  .option('--date-col <column>', 'Specifies the column name containing date values (e.g., --date-col "Buchungsdatum")')
396
- .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
396
+ .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee", "Item:*coffee*", "Item:!bad*")')
397
397
  .option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
398
398
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
399
399
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
@@ -401,6 +401,8 @@ program
401
401
  .option('--gui-port <port>', 'Specify custom port for GUI server')
402
402
  .option('--gui-verbose', 'Enable verbose logging for GUI server')
403
403
  .option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
404
+ .option('--planets <list>', 'Select active planets and asteroids (comma-separated, e.g., "Sun,Moon,Mercury,Venus,Hygiea")')
405
+ .option('--active-planets', 'Show the currently active planets')
404
406
  .description('Shows astrological data for a planet')
405
407
  .action(async (planetArg, planet2Arg, options) => {
406
408
  // If planet2Arg is an object, it contains the options (commander behavior)
@@ -875,46 +877,68 @@ program
875
877
  const houses = await calculateHouses(birthJulianDay, getHouseSystemCode(houseSystem), true);
876
878
  const transitDateDisplay = transitDate || getCurrentTimeInTimezone();
877
879
 
878
- // Show transit aspects for all planets
880
+ // Collect all transit aspects first
881
+ const allAspects = [];
879
882
  for (const [planetName] of Object.entries(planets)) {
880
883
  const transitPlanetData = getAstrologicalData(planetName, transitDateDisplay);
881
884
  const transitHouse = getPlanetHouse(parseFloat(transitPlanetData.degreeInSign) + (signs.indexOf(transitPlanetData.sign) * 30), houses.house);
882
885
  const transitAspectsData = calculatePersonalTransitAspects(transitDateDisplay, birthData, planetName);
883
886
  const transitAspects = transitAspectsData ? transitAspectsData.aspects : [];
884
- if (transitAspects.length > 0) {
885
- transitAspects.forEach(aspect => {
886
- const birthPlanetData = getAstrologicalData(aspect.birthPlanet, birthData);
887
- const birthHouse = getPlanetHouse(parseFloat(birthPlanetData.degreeInSign) + (signs.indexOf(birthPlanetData.sign) * 30), houses.house);
888
-
889
- // Determine aspect phase
890
- const aspectAngles = {
891
- 'Conjunction': 0,
892
- 'Opposition': 180,
893
- 'Square': 90,
894
- 'Trine': 120,
895
- 'Sextile': 60
896
- };
897
- const targetAngle = aspectAngles[aspect.type];
898
- const phase = determineAspectPhase(planetName, aspect.birthPlanet, transitDateDisplay, aspect.type, targetAngle);
899
-
900
- // Exact time calculation removed
901
- const dateTimeStr = '-';
902
-
903
- const transitPlanetFormatted = planetName.charAt(0).toUpperCase() + planetName.slice(1);
904
- const aspectTypeFormatted = aspect.type.padEnd(11);
905
- const aspectPlanetFormatted = aspect.birthPlanet.charAt(0).toUpperCase() + aspect.birthPlanet.slice(1);
906
- const orbFormatted = aspect.orb.padEnd(4);
907
- const transitPosFormatted = `${transitPlanetData.sign} ${transitPlanetData.degreeInSign}°`;
908
- const transitHouseFormatted = transitHouse.toString().padEnd(11);
909
- const birthHouseFormatted = birthHouse.toString().padEnd(11);
910
- const phaseFormatted = phase === 'separativ' ? 'separativ'.padEnd(11) : phase.padEnd(11);
911
- const dateTimeFormatted = dateTimeStr.padEnd(22);
912
-
913
- console.log(`| ${transitPlanetFormatted.padEnd(10)} | ${aspectTypeFormatted} | ${aspectPlanetFormatted.padEnd(10)} | ${orbFormatted}° | ${transitPosFormatted.padEnd(16)} | ${transitHouseFormatted} | ${birthHouseFormatted} | ${phaseFormatted} |`);
887
+
888
+ // Add planet data to each aspect for later display
889
+ transitAspects.forEach(aspect => {
890
+ allAspects.push({
891
+ planetName: planetName,
892
+ transitPlanetData: transitPlanetData,
893
+ transitHouse: transitHouse,
894
+ aspect: aspect,
895
+ birthPlanetData: getAstrologicalData(aspect.birthPlanet, birthData),
896
+ birthHouse: getPlanetHouse(parseFloat(getAstrologicalData(aspect.birthPlanet, birthData).degreeInSign) + (signs.indexOf(getAstrologicalData(aspect.birthPlanet, birthData).sign) * 30), houses.house)
914
897
  });
915
- }
898
+ });
916
899
  }
917
900
 
901
+ // Sort all aspects by orb (smallest orb first)
902
+ allAspects.sort((a, b) => {
903
+ return parseFloat(a.aspect.orb) - parseFloat(b.aspect.orb);
904
+ });
905
+
906
+ // Display sorted aspects
907
+ allAspects.forEach(aspectData => {
908
+ const planetName = aspectData.planetName;
909
+ const transitPlanetData = aspectData.transitPlanetData;
910
+ const transitHouse = aspectData.transitHouse;
911
+ const aspect = aspectData.aspect;
912
+ const birthPlanetData = aspectData.birthPlanetData;
913
+ const birthHouse = aspectData.birthHouse;
914
+
915
+ // Determine aspect phase
916
+ const aspectAngles = {
917
+ 'Conjunction': 0,
918
+ 'Opposition': 180,
919
+ 'Square': 90,
920
+ 'Trine': 120,
921
+ 'Sextile': 60
922
+ };
923
+ const targetAngle = aspectAngles[aspect.type];
924
+ const phase = determineAspectPhase(planetName, aspect.birthPlanet, transitDateDisplay, aspect.type, targetAngle);
925
+
926
+ // Exact time calculation removed
927
+ const dateTimeStr = '-';
928
+
929
+ const transitPlanetFormatted = planetName.charAt(0).toUpperCase() + planetName.slice(1);
930
+ const aspectTypeFormatted = aspect.type.padEnd(11);
931
+ const aspectPlanetFormatted = aspect.birthPlanet.charAt(0).toUpperCase() + aspect.birthPlanet.slice(1);
932
+ const orbFormatted = aspect.orb.padEnd(4);
933
+ const transitPosFormatted = `${transitPlanetData.sign} ${transitPlanetData.degreeInSign}°`;
934
+ const transitHouseFormatted = transitHouse.toString().padEnd(11);
935
+ const birthHouseFormatted = birthHouse.toString().padEnd(11);
936
+ const phaseFormatted = phase === 'separativ' ? 'separativ'.padEnd(11) : phase.padEnd(11);
937
+ const dateTimeFormatted = dateTimeStr.padEnd(22);
938
+
939
+ console.log(`| ${transitPlanetFormatted.padEnd(10)} | ${aspectTypeFormatted} | ${aspectPlanetFormatted.padEnd(10)} | ${orbFormatted}° | ${transitPosFormatted.padEnd(16)} | ${transitHouseFormatted} | ${birthHouseFormatted} | ${phaseFormatted} |`);
940
+ });
941
+
918
942
  console.log('================================================================================');
919
943
  console.log(`\nAnalyse-Datum: ${transitDateDisplay.day}.${transitDateDisplay.month}.${transitDateDisplay.year}`);
920
944
  console.log(`Geburtsdatum: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
@@ -1207,6 +1231,21 @@ program
1207
1231
  return;
1208
1232
  }
1209
1233
 
1234
+ // Set active planets if --planets option is specified (no planet required)
1235
+ if (options.planets) {
1236
+ const { setActivePlanets } = require('../config/configService');
1237
+ setActivePlanets(options.planets);
1238
+ return;
1239
+ }
1240
+
1241
+ // Show active planets if --active-planets option is specified (no planet required)
1242
+ if (options.activePlanets) {
1243
+ const { getActivePlanets } = require('../config/configService');
1244
+ const activePlanets = getActivePlanets();
1245
+ console.log('Active planets:', activePlanets.join(', '));
1246
+ return;
1247
+ }
1248
+
1210
1249
  // Set AI model if --ai option is specified (no planet required)
1211
1250
  if (options.ai) {
1212
1251
  setAIModel(options.ai);
@@ -160,13 +160,15 @@ class CLIService {
160
160
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
161
161
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
162
162
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
163
- .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
163
+ .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee", "Item:*coffee*", "Item:!bad*")')
164
164
  .option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
165
165
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
166
166
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
167
167
  .option('--gui', 'Launches the web interface for command history (port 37421)')
168
168
  .option('--gui-port <port>', 'Specify custom port for GUI server')
169
169
  .option('--o', 'Shows how often this transit occurs in the sky (frequency calculation)')
170
+ .option('--planets <list>', 'Select active planets and asteroids (comma-separated, e.g., "Sun,Moon,Mercury,Venus,Hygiea")')
171
+ .option('--active-planets', 'Show the currently active planets')
170
172
  .description('Shows astrological data for a planet')
171
173
  .action(this.handleCommand.bind(this));
172
174
  }
@@ -652,6 +654,21 @@ class CLIService {
652
654
  return { success: true, output: output.join('\n') };
653
655
  }
654
656
 
657
+ // Set active planets if --planets option is specified (no planet required)
658
+ if (actualOptions.planets) {
659
+ const { setActivePlanets } = require('../config/configService');
660
+ setActivePlanets(actualOptions.planets, userId);
661
+ return { success: true, output: output.join('\n') };
662
+ }
663
+
664
+ // Show active planets if --active-planets option is specified (no planet required)
665
+ if (actualOptions.activePlanets) {
666
+ const { getActivePlanets } = require('../config/configService');
667
+ const activePlanets = getActivePlanets(userId);
668
+ console.log('Active planets:', activePlanets.join(', '));
669
+ return { success: true, output: output.join('\n') };
670
+ }
671
+
655
672
  // Delete person if --delete-person option is specified (no planet required)
656
673
  if (actualOptions.deletePerson) {
657
674
  deletePerson(actualOptions.deletePerson, userId);
@@ -456,6 +456,52 @@ function setAIModel(modelName, userId = null) {
456
456
  return false;
457
457
  }
458
458
 
459
+ // Function to set active planets
460
+ function setActivePlanets(planetList, userId = null) {
461
+ const config = loadConfig(userId) || {
462
+ currentLocation: null,
463
+ birthData: null,
464
+ setupDate: new Date().toISOString()
465
+ };
466
+
467
+ // Get available planets for validation
468
+ const { planets } = require('../astrology/astrologyConstants');
469
+ const availablePlanets = Object.keys(planets);
470
+
471
+ // Convert comma-separated string to array and normalize planet names
472
+ const planetsArray = planetList.split(',').map(p => p.trim().toLowerCase());
473
+
474
+ // Validate planet names
475
+ const invalidPlanets = planetsArray.filter(p => !availablePlanets.includes(p));
476
+ if (invalidPlanets.length > 0) {
477
+ console.error(`Error: Invalid planet names: ${invalidPlanets.join(', ')}`);
478
+ console.error(`Available planets: ${availablePlanets.join(', ')}`);
479
+ return false;
480
+ }
481
+
482
+ config.activePlanets = planetsArray;
483
+
484
+ if (saveConfig(config, userId)) {
485
+ console.log(`Active planets set: ${planetsArray.join(', ')}`);
486
+ return true;
487
+ }
488
+ return false;
489
+ }
490
+
491
+ // Function to get active planets (with defaults)
492
+ function getActivePlanets(userId = null) {
493
+ const config = loadConfig(userId);
494
+
495
+ // Default active planets (traditional planets + Chiron, excluding other asteroids)
496
+ const defaultActivePlanets = ['sun', 'moon', 'mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune', 'pluto', 'chiron'];
497
+
498
+ if (config && config.activePlanets && Array.isArray(config.activePlanets)) {
499
+ return config.activePlanets;
500
+ }
501
+
502
+ return defaultActivePlanets;
503
+ }
504
+
459
505
  // Function to set a custom system prompt
460
506
  function setSystemPrompt(prompt, userId = null) {
461
507
  const config = loadConfig(userId) || {
@@ -903,5 +949,8 @@ module.exports = {
903
949
  showNotes,
904
950
  searchNotesBySituation,
905
951
  // Add helper functions for user-specific operations
906
- getConfigPath
952
+ getConfigPath,
953
+ // Planet selection functions
954
+ setActivePlanets,
955
+ getActivePlanets
907
956
  };