klio 1.5.5 → 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:
@@ -118,8 +137,12 @@ It's possible to analyze a csv with a column of either ISO date time or unix tim
118
137
 
119
138
  - **Show house and sign distribution of the datetime column**: `klio [planet] --csv <file-path>`
120
139
  - **Show aspect type distribution between two planets:** `klio [planet1] [planet2] --csv <file-path> --a`
140
+ - **Specify date column name:** `klio [planet] --csv <file-path> --date-col "Buchungsdatum"` (e.g., `--date-col "Date"`)
121
141
  - **Filter CSV data by column value:** `klio [planet] --csv <file-path> --filter "column:value"` (e.g., `--filter "Item:coffee"`)
142
+ - **Filter CSV data by contains operator:** `klio [planet] --csv <file-path> --filter "column*text"` (e.g., `--filter "Buchungstext*Gutschrift"`)
143
+ - **Filter CSV data by excludes operator:** `klio [planet] --csv <file-path> --filter "column!text"` (e.g., `--filter "Buchungstext!Gutschrift"`)
122
144
  - **Filter CSV data by multiple conditions:** `klio [planet] --csv <file-path> --filter "column1:value1,column2:value2"` (e.g., `--filter "FTR:H,HomeTeam:Liverpool"`)
145
+ - **Filter CSV data with comparison operators:** `klio [planet] --csv <file-path> --filter "column>value"` (e.g., `--filter "Amount>100"`)
123
146
  - **Creating a bar chart**: Create a bar chart and save the image to the downloads' folder. The image shows the aspect distribution of your csv datetime values: `klio moon sun --csv /home/user/Downloads/coffee.csv --filter "Item:cookie" --a --title "Eaten cookies during sun-moon aspects"`
124
147
 
125
148
  - The command also returns a Chi-Square.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.5.5",
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 = {};
@@ -1872,6 +1965,37 @@ function parseFilterCriteria(filterString) {
1872
1965
  continue;
1873
1966
  }
1874
1967
 
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
1991
+ criteria.push({
1992
+ column: column,
1993
+ operator: '=',
1994
+ value: value
1995
+ });
1996
+ continue;
1997
+ }
1998
+
1875
1999
  const parts = condition.split(':');
1876
2000
  if (parts.length !== 2) {
1877
2001
  console.warn(`Invalid filter format. Expected "column:value" or "column > value" but got "${condition}"`);
@@ -1919,6 +2043,12 @@ function applyFilter(records, filterCriteria) {
1919
2043
  return bothNumeric ? recordNum < valueNum : false;
1920
2044
  case '<=':
1921
2045
  return bothNumeric ? recordNum <= valueNum : false;
2046
+ case '*':
2047
+ // Contains operator - check if record value contains the search string
2048
+ return recordValueStr.includes(valueStr);
2049
+ case '!':
2050
+ // Excludes operator - check if record value does NOT contain the search string
2051
+ return !recordValueStr.includes(valueStr);
1922
2052
  default:
1923
2053
  return false;
1924
2054
  }
@@ -1941,7 +2071,7 @@ function applyFilter(records, filterCriteria) {
1941
2071
  }
1942
2072
  }
1943
2073
 
1944
- function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null) {
2074
+ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'koch', analyzeAspects = false, partnerPlanet = null, filterCriteria = null, dateColumn = null) {
1945
2075
  return new Promise(async (resolve, reject) => {
1946
2076
  const results = [];
1947
2077
  let pendingOperations = 0;
@@ -2014,8 +2144,25 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2014
2144
 
2015
2145
  // Process each row
2016
2146
  for (const data of filteredRecords) {
2017
- // Look for a column with datetime values, prioritizing specific date formats
2018
- const datetimeColumns = [];
2147
+ // Use specified date column if provided, otherwise auto-detect
2148
+ let datetimeColumns = [];
2149
+ let datetimeValue = null;
2150
+
2151
+ if (dateColumn) {
2152
+ // Use the specified date column
2153
+ if (data.hasOwnProperty(dateColumn)) {
2154
+ datetimeValue = data[dateColumn];
2155
+ datetimeColumns = [dateColumn];
2156
+ } else {
2157
+ console.warn(`⚠️ Specified date column "${dateColumn}" not found in CSV. Trying auto-detection...`);
2158
+ // Fall back to auto-detection
2159
+ }
2160
+ }
2161
+
2162
+ // If no date column was specified or it wasn't found, auto-detect
2163
+ if (datetimeColumns.length === 0) {
2164
+ // Look for a column with datetime values, prioritizing specific date formats
2165
+ datetimeColumns = [];
2019
2166
 
2020
2167
  // First pass: look for YYYY-MM-DD format (most specific)
2021
2168
  const yyyyMmDdColumns = Object.keys(data).filter(key => {
@@ -2030,6 +2177,13 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2030
2177
  const trimmed = value != null ? value.toString().trim() : '';
2031
2178
  return /^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2032
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
+ });
2033
2187
 
2034
2188
  // Third pass: look for Unix timestamps (10 or 13 digits)
2035
2189
  const unixTimestampColumns = Object.keys(data).filter(key => {
@@ -2049,11 +2203,15 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2049
2203
  return moment(trimmed, moment.ISO_8601, true).isValid();
2050
2204
  });
2051
2205
 
2052
- // Prioritize columns: YYYY-MM-DD, DD/MM/YYYY, Unix timestamps, then ISO dates
2053
- 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);
2208
+
2209
+ if (datetimeColumns.length > 0) {
2210
+ datetimeValue = data[datetimeColumns[0]];
2211
+ }
2212
+ }
2054
2213
 
2055
- if (datetimeColumns.length > 0) {
2056
- const datetimeValue = data[datetimeColumns[0]];
2214
+ if (datetimeColumns.length > 0 && datetimeValue !== null) {
2057
2215
  const datetimeValueTrimmed = datetimeValue != null ? datetimeValue.toString().trim() : '';
2058
2216
  let datetime;
2059
2217
 
@@ -2073,6 +2231,15 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2073
2231
  if (datetime.isValid() && !hasTime) {
2074
2232
  datetime = datetime.hour(12).minute(0);
2075
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
+ }
2076
2243
  } else {
2077
2244
  // Handle as ISO-8601 date
2078
2245
  datetime = moment(datetimeValueTrimmed);
@@ -2249,9 +2416,25 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2249
2416
 
2250
2417
  // Process each record
2251
2418
  for (const data of filteredRecords) {
2252
-
2253
- // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, DD/MM/YYYY dates (optional time), or Unix-Timestamps
2254
- const datetimeColumns = Object.keys(data).filter(key => {
2419
+ // Use specified date column if provided, otherwise auto-detect
2420
+ let datetimeColumns = [];
2421
+ let datetimeValue = null;
2422
+
2423
+ if (dateColumn) {
2424
+ // Use the specified date column
2425
+ if (data.hasOwnProperty(dateColumn)) {
2426
+ datetimeValue = data[dateColumn];
2427
+ datetimeColumns = [dateColumn];
2428
+ } else {
2429
+ console.warn(`⚠️ Specified date column "${dateColumn}" not found in CSV. Trying auto-detection...`);
2430
+ // Fall back to auto-detection
2431
+ }
2432
+ }
2433
+
2434
+ // If no date column was specified or it wasn't found, auto-detect
2435
+ if (datetimeColumns.length === 0) {
2436
+ // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, DD/MM/YYYY dates (optional time), or Unix-Timestamps
2437
+ datetimeColumns = Object.keys(data).filter(key => {
2255
2438
  const value = data[key];
2256
2439
  const trimmed = value != null ? value.toString().trim() : '';
2257
2440
  // Check for ISO-8601 date
@@ -2260,13 +2443,19 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2260
2443
  const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
2261
2444
  // Check for DD/MM/YYYY date format
2262
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);
2263
2448
  // Check for Unix-Timestamp (number with 10 or 13 digits)
2264
2449
  const isUnixTimestamp = /^\d{10,13}$/.test(trimmed);
2265
- return isISO || isYYYYMMDD || isDDMMYYYY || isUnixTimestamp;
2450
+ return isISO || isYYYYMMDD || isDDMMYYYY || isDDMMYYYYDot || isUnixTimestamp;
2266
2451
  });
2267
2452
 
2268
- if (datetimeColumns.length > 0) {
2269
- const datetimeValue = data[datetimeColumns[0]];
2453
+ if (datetimeColumns.length > 0) {
2454
+ datetimeValue = data[datetimeColumns[0]];
2455
+ }
2456
+ }
2457
+
2458
+ if (datetimeColumns.length > 0 && datetimeValue !== null) {
2270
2459
  const datetimeValueTrimmed = datetimeValue != null ? datetimeValue.toString().trim() : '';
2271
2460
  let datetime;
2272
2461
 
@@ -2286,6 +2475,15 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2286
2475
  if (datetime.isValid() && !hasTime) {
2287
2476
  datetime = datetime.hour(12).minute(0);
2288
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
+ }
2289
2487
  } else {
2290
2488
  // Handle as ISO-8601 date
2291
2489
  datetime = moment(datetimeValueTrimmed);
@@ -2985,7 +3183,10 @@ module.exports = {
2985
3183
  calculateNextPlanetIngress,
2986
3184
  calculateAstrologicalAngles,
2987
3185
  longitudeToSignDegree,
2988
- calculateTransitFrequency
3186
+ calculateTransitFrequency,
3187
+ // Filter functions for testing
3188
+ parseFilterCriteria,
3189
+ applyFilter
2989
3190
  };
2990
3191
 
2991
3192
  // Function to calculate how often a transit occurs between two planets
package/src/cli/cli.js CHANGED
@@ -392,7 +392,8 @@ program
392
392
  .option('--v <count>', 'Shows past aspects between two planets (Format: --v <count> planet1 aspectType planet2)')
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
- .option('--filter <column:value>', 'Filters CSV data by column:value (e.g., --filter "Item:coffee")')
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", "Item:*coffee*", "Item:!bad*")')
396
397
  .option('--title <title>', 'Title for the chart image (generates PNG image when provided)')
397
398
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
398
399
  .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
@@ -400,6 +401,8 @@ program
400
401
  .option('--gui-port <port>', 'Specify custom port for GUI server')
401
402
  .option('--gui-verbose', 'Enable verbose logging for GUI server')
402
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')
403
406
  .description('Shows astrological data for a planet')
404
407
  .action(async (planetArg, planet2Arg, options) => {
405
408
  // If planet2Arg is an object, it contains the options (commander behavior)
@@ -874,46 +877,68 @@ program
874
877
  const houses = await calculateHouses(birthJulianDay, getHouseSystemCode(houseSystem), true);
875
878
  const transitDateDisplay = transitDate || getCurrentTimeInTimezone();
876
879
 
877
- // Show transit aspects for all planets
880
+ // Collect all transit aspects first
881
+ const allAspects = [];
878
882
  for (const [planetName] of Object.entries(planets)) {
879
883
  const transitPlanetData = getAstrologicalData(planetName, transitDateDisplay);
880
884
  const transitHouse = getPlanetHouse(parseFloat(transitPlanetData.degreeInSign) + (signs.indexOf(transitPlanetData.sign) * 30), houses.house);
881
885
  const transitAspectsData = calculatePersonalTransitAspects(transitDateDisplay, birthData, planetName);
882
886
  const transitAspects = transitAspectsData ? transitAspectsData.aspects : [];
883
- if (transitAspects.length > 0) {
884
- transitAspects.forEach(aspect => {
885
- const birthPlanetData = getAstrologicalData(aspect.birthPlanet, birthData);
886
- const birthHouse = getPlanetHouse(parseFloat(birthPlanetData.degreeInSign) + (signs.indexOf(birthPlanetData.sign) * 30), houses.house);
887
-
888
- // Determine aspect phase
889
- const aspectAngles = {
890
- 'Conjunction': 0,
891
- 'Opposition': 180,
892
- 'Square': 90,
893
- 'Trine': 120,
894
- 'Sextile': 60
895
- };
896
- const targetAngle = aspectAngles[aspect.type];
897
- const phase = determineAspectPhase(planetName, aspect.birthPlanet, transitDateDisplay, aspect.type, targetAngle);
898
-
899
- // Exact time calculation removed
900
- const dateTimeStr = '-';
901
-
902
- const transitPlanetFormatted = planetName.charAt(0).toUpperCase() + planetName.slice(1);
903
- const aspectTypeFormatted = aspect.type.padEnd(11);
904
- const aspectPlanetFormatted = aspect.birthPlanet.charAt(0).toUpperCase() + aspect.birthPlanet.slice(1);
905
- const orbFormatted = aspect.orb.padEnd(4);
906
- const transitPosFormatted = `${transitPlanetData.sign} ${transitPlanetData.degreeInSign}°`;
907
- const transitHouseFormatted = transitHouse.toString().padEnd(11);
908
- const birthHouseFormatted = birthHouse.toString().padEnd(11);
909
- const phaseFormatted = phase === 'separativ' ? 'separativ'.padEnd(11) : phase.padEnd(11);
910
- const dateTimeFormatted = dateTimeStr.padEnd(22);
911
-
912
- 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)
913
897
  });
914
- }
898
+ });
915
899
  }
916
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
+
917
942
  console.log('================================================================================');
918
943
  console.log(`\nAnalyse-Datum: ${transitDateDisplay.day}.${transitDateDisplay.month}.${transitDateDisplay.year}`);
919
944
  console.log(`Geburtsdatum: ${birthData.day}.${birthData.month}.${birthData.year} ${birthData.hour}:${birthData.minute.toString().padStart(2, '0')}`);
@@ -1206,6 +1231,21 @@ program
1206
1231
  return;
1207
1232
  }
1208
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
+
1209
1249
  // Set AI model if --ai option is specified (no planet required)
1210
1250
  if (options.ai) {
1211
1251
  setAIModel(options.ai);
@@ -1742,7 +1782,9 @@ program
1742
1782
  const analyzeAspects = actualOptions.a || false;
1743
1783
  const filterCriteria = actualOptions.filter;
1744
1784
 
1745
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
1785
+ const dateColumn = actualOptions.dateCol;
1786
+
1787
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria, dateColumn)
1746
1788
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
1747
1789
  if (results.length === 0) {
1748
1790
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -2785,7 +2827,9 @@ program
2785
2827
  const analyzeAspects = actualOptions.a || false;
2786
2828
  const filterCriteria = actualOptions.filter;
2787
2829
 
2788
- analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria)
2830
+ const dateColumn = actualOptions.dateCol;
2831
+
2832
+ analyzeCSVWithDatetime(actualOptions.csv, planet, houseSystem, analyzeAspects, planet2, filterCriteria, dateColumn)
2789
2833
  .then(({ results, houseDistribution, signDistribution, aspectStatistics }) => {
2790
2834
  if (results.length === 0) {
2791
2835
  console.log('No valid ISO-Datetime values found in the CSV file.');
@@ -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
  };