klio 1.5.2 → 1.5.4

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
@@ -1,4 +1,4 @@
1
- # Klio - Astrological CLI Tool
1
+ # Klio
2
2
 
3
3
  A command-line tool for astrological calculations, health analysis, and personalized astrology insights.
4
4
 
@@ -144,8 +144,6 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` or
144
144
  - **Transits**: `klio --tr` - Shows personal transits based on birth data. Combine with `--wp <id or --i>` and optional `--d <"DD.MM.YYYY HH:MM>`
145
145
  - **Transit Houses**: `klio --s --tra --i`
146
146
 
147
-
148
-
149
147
  ### Past and Future Aspects
150
148
 
151
149
  - **Past aspects**: `klio --v <count> [planet1] [aspect-type] [planet2]` - Shows past aspects (Format: --v <count> planet1 aspectType planet2) Available aspect types: c, o, s, t, se (conjunction, opposition, square, trine, sextile)
@@ -187,6 +185,75 @@ When were the files created on a system folder and in which house was the planet
187
185
  - **House filters**: `--h1` through `--h12` - Filters files with planet in specific houses
188
186
 
189
187
 
188
+ ## Note Taking
189
+
190
+ Klio includes a note-taking system:
191
+
192
+ ### Basic Note Taking
193
+
194
+ - **Add a note to any astrological situation**: `klio [planet] --note "your observation"`
195
+ - Example: `klio saturn --note "Boundaries, Limitations..."`
196
+ - Example: `klio moon --note "Full moon in Scorpio"`
197
+
198
+ - **List all notes**: `klio --notes` - Shows all saved notes with timestamps and astrological context
199
+
200
+ - **Search notes**: `klio --search-notes "keyword"` - Searches notes by astrological situation
201
+ - Example: `klio --search-notes "house"` - Finds all house-related notes
202
+ - Example: `klio --search-notes "saturn"` - Finds all Saturn-related notes
203
+
204
+ ### House Notation
205
+
206
+ - **House-specific notes**: `klio h[n] --note "house observation"`
207
+ - Example: `klio h[4] --note "4th house observation - family matters"`
208
+ - Example: `klio h[10] --note "10th house - career focus"`
209
+
210
+ - **Planet in house notes**: `klio h[n] [planet] --note "planet in house observation"`
211
+ - Example: `klio h[4] moon --note "Moon in 4th house - emotional security"`
212
+ - Example: `klio h[7] venus --note "Venus in 7th house - relationship harmony"`
213
+
214
+ ### Aspect Notation
215
+
216
+ - **Aspect-specific notes**: `klio [planet1] [planet2] --note "aspect observation" --aspect [type]`
217
+ - Example: `klio saturn moon --note "Saturn conjunction Moon - emotional challenges" --aspect c`
218
+ - Example: `klio venus mars --note "Venus square Mars - relationship tension" --aspect s`
219
+ - Example: `klio jupiter uranus --note "Jupiter trine Uranus - sudden opportunities" --aspect t`
220
+
221
+ ### Aspect Type Shorthands
222
+
223
+ - `c` - Conjunction
224
+ - `s` - Square
225
+ - `t` - Trine
226
+ - `se` - Sextile
227
+ - `o` - Opposition
228
+
229
+ ### Advanced Features
230
+
231
+ - **House system integration**: Notes include house information when using `--hs` option
232
+ - **Date-specific notes**: Use `--d` option to add notes for specific dates
233
+ - **JSON storage**: All notes are stored with full astrological context in `~/.config/astrocli/notes.json`
234
+
235
+ ### Examples
236
+
237
+ ```bash
238
+ # Basic planet note
239
+ klio mars --note "Mars in Aries - high energy period"
240
+
241
+ # House note
242
+ klio h[7] --note "7th house focus - relationships this month"
243
+
244
+ # Planet in house note
245
+ klio h[10] saturn --note "Saturn in 10th house - career challenges"
246
+
247
+ # Aspect note with shorthand
248
+ klio sun moon --note "Sun trine Moon - emotional harmony" --aspect t
249
+
250
+ # List all notes
251
+ klio --notes
252
+
253
+ # Search for specific notes
254
+ klio --search-notes "saturn"
255
+ ```
256
+
190
257
  ## License
191
258
 
192
259
  ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -194,8 +194,13 @@ function getHouseSystemCode(houseSystemName) {
194
194
  }
195
195
 
196
196
  // Function to calculate houses
197
- function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false) {
197
+ function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false, locationOverride = null) {
198
198
  return new Promise((resolve, reject) => {
199
+ if (!Number.isFinite(julianDay)) {
200
+ reject('Invalid Julian Day');
201
+ return;
202
+ }
203
+
199
204
  // Load the configured location data
200
205
  const configPath = path.join(__dirname, '../../astrocli-config.json');
201
206
  let config;
@@ -210,7 +215,13 @@ function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false)
210
215
 
211
216
  let latitude, longitude, locationName;
212
217
 
213
- if (useBirthLocation && config && config.birthData && config.birthData.location) {
218
+ if (locationOverride && locationOverride.latitude != null && locationOverride.longitude != null) {
219
+ latitude = locationOverride.latitude;
220
+ longitude = locationOverride.longitude;
221
+ locationName = locationOverride.name
222
+ ? `${locationOverride.name}${locationOverride.country ? `, ${locationOverride.country}` : ''}`
223
+ : 'Custom location';
224
+ } else if (useBirthLocation && config && config.birthData && config.birthData.location) {
214
225
  // Use birth location for birth charts
215
226
  latitude = config.birthData.location.latitude;
216
227
  longitude = config.birthData.location.longitude;
@@ -222,6 +233,14 @@ function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false)
222
233
  locationName = config && config.currentLocation ? `${config.currentLocation.name}, ${config.currentLocation.country}` : 'Berlin, Germany';
223
234
  }
224
235
 
236
+ latitude = parseFloat(latitude);
237
+ longitude = parseFloat(longitude);
238
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
239
+ latitude = defaultLatitude;
240
+ longitude = defaultLongitude;
241
+ locationName = 'Berlin, Germany';
242
+ }
243
+
225
244
  swisseph.swe_houses(julianDay, latitude, longitude, houseSystem, function(result) {
226
245
  if (result.error) {
227
246
  if (result.error.includes('not found')) {
@@ -1328,7 +1347,7 @@ async function calculatePersonalTransits(transitDate = null, birthData = null) {
1328
1347
 
1329
1348
  // Calculate houses based on birth data (ASC-based)
1330
1349
  const birthJulianDay = calculateJulianDayUTC(birthData, getTimezoneOffset(birthData, birthData.location?.timezone || 'Europe/Zurich'));
1331
- const houses = await calculateHouses(birthJulianDay, 'K', true); // Always Koch house system for transits
1350
+ const houses = await calculateHouses(birthJulianDay, 'K', true, birthData.location || null); // Always Koch house system for transits
1332
1351
 
1333
1352
  // Calculate current planet positions (Transit)
1334
1353
  const transitPlanets = {};
@@ -1467,7 +1486,11 @@ function showPersonalTransitAspects(transitDate = null, birthData = null, target
1467
1486
  birthPlanetsData[name] = getAstrologicalData(name, birthData);
1468
1487
  }
1469
1488
 
1470
- aspectData.aspects.forEach(aspect => {
1489
+ const sortedAspects = [...aspectData.aspects].sort((a, b) => {
1490
+ return parseFloat(a.orb) - parseFloat(b.orb);
1491
+ });
1492
+
1493
+ sortedAspects.forEach(aspect => {
1471
1494
  const transitPlanetData = transitPlanetsData[aspect.transitPlanet];
1472
1495
  const birthPlanetData = birthPlanetsData[aspect.birthPlanet];
1473
1496
 
@@ -1504,7 +1527,7 @@ async function showCombinedAnalysis(planetName, transitDate = null, birthData =
1504
1527
 
1505
1528
  // Calculate houses based on birth data
1506
1529
  const birthJulianDay = calculateJulianDayUTC(birthData, getTimezoneOffset(birthData, birthData.location?.timezone || 'Europe/Zurich'));
1507
- const houses = await calculateHouses(birthJulianDay, getHouseSystemCode(houseSystem), true);
1530
+ const houses = await calculateHouses(birthJulianDay, getHouseSystemCode(houseSystem), true, birthData.location || null);
1508
1531
 
1509
1532
  // Calculate current planet positions (Transit)
1510
1533
  const transitPlanetData = getAstrologicalData(planetName, transitDate);
@@ -1836,16 +1859,28 @@ function parseFilterCriteria(filterString) {
1836
1859
  const filterConditions = filterString.split(',').map(cond => cond.trim());
1837
1860
 
1838
1861
  const criteria = [];
1862
+ const operatorRegex = /^(.+?)\s*(>=|<=|!=|=|>|<)\s*(.+)$/;
1839
1863
 
1840
1864
  for (const condition of filterConditions) {
1865
+ const operatorMatch = condition.match(operatorRegex);
1866
+ if (operatorMatch) {
1867
+ criteria.push({
1868
+ column: operatorMatch[1].trim(),
1869
+ operator: operatorMatch[2],
1870
+ value: operatorMatch[3].trim()
1871
+ });
1872
+ continue;
1873
+ }
1874
+
1841
1875
  const parts = condition.split(':');
1842
1876
  if (parts.length !== 2) {
1843
- console.warn(`Invalid filter format. Expected "column:value" but got "${condition}"`);
1877
+ console.warn(`Invalid filter format. Expected "column:value" or "column > value" but got "${condition}"`);
1844
1878
  return null;
1845
1879
  }
1846
-
1880
+
1847
1881
  criteria.push({
1848
1882
  column: parts[0].trim(),
1883
+ operator: '=',
1849
1884
  value: parts[1].trim()
1850
1885
  });
1851
1886
  }
@@ -1863,19 +1898,45 @@ function applyFilter(records, filterCriteria) {
1863
1898
 
1864
1899
  if (!parsedCriteria) return records;
1865
1900
 
1901
+ const matchesCriterion = (recordValue, criterion) => {
1902
+ if (recordValue === undefined || recordValue === null) return false;
1903
+ const recordValueStr = recordValue.toString().trim();
1904
+ const valueStr = criterion.value.toString().trim();
1905
+ const recordNum = parseFloat(recordValueStr);
1906
+ const valueNum = parseFloat(valueStr);
1907
+ const bothNumeric = Number.isFinite(recordNum) && Number.isFinite(valueNum);
1908
+
1909
+ switch (criterion.operator || '=') {
1910
+ case '=':
1911
+ return recordValueStr === valueStr;
1912
+ case '!=':
1913
+ return recordValueStr !== valueStr;
1914
+ case '>':
1915
+ return bothNumeric ? recordNum > valueNum : false;
1916
+ case '>=':
1917
+ return bothNumeric ? recordNum >= valueNum : false;
1918
+ case '<':
1919
+ return bothNumeric ? recordNum < valueNum : false;
1920
+ case '<=':
1921
+ return bothNumeric ? recordNum <= valueNum : false;
1922
+ default:
1923
+ return false;
1924
+ }
1925
+ };
1926
+
1866
1927
  // Handle both single criteria and multiple criteria
1867
1928
  if (Array.isArray(parsedCriteria)) {
1868
1929
  return records.filter(record => {
1869
1930
  return parsedCriteria.every(criterion => {
1870
1931
  const recordValue = record[criterion.column];
1871
- return recordValue && recordValue.toString() === criterion.value;
1932
+ return matchesCriterion(recordValue, criterion);
1872
1933
  });
1873
1934
  });
1874
1935
  } else {
1875
1936
  // Single criteria (backward compatibility)
1876
1937
  return records.filter(record => {
1877
1938
  const recordValue = record[parsedCriteria.column];
1878
- return recordValue && recordValue.toString() === parsedCriteria.value;
1939
+ return matchesCriterion(recordValue, parsedCriteria);
1879
1940
  });
1880
1941
  }
1881
1942
  }
@@ -1959,44 +2020,66 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
1959
2020
  // First pass: look for YYYY-MM-DD format (most specific)
1960
2021
  const yyyyMmDdColumns = Object.keys(data).filter(key => {
1961
2022
  const value = data[key];
1962
- return /^\d{4}-\d{2}-\d{2}$/.test(value);
2023
+ const trimmed = value != null ? value.toString().trim() : '';
2024
+ return /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
2025
+ });
2026
+
2027
+ // Second pass: look for DD/MM/YYYY format (optional time)
2028
+ const ddMmYyyyColumns = Object.keys(data).filter(key => {
2029
+ const value = data[key];
2030
+ const trimmed = value != null ? value.toString().trim() : '';
2031
+ return /^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
1963
2032
  });
1964
2033
 
1965
- // Second pass: look for Unix timestamps (10 or 13 digits)
2034
+ // Third pass: look for Unix timestamps (10 or 13 digits)
1966
2035
  const unixTimestampColumns = Object.keys(data).filter(key => {
1967
2036
  const value = data[key];
1968
- return /^\d{10,13}$/.test(value);
2037
+ const trimmed = value != null ? value.toString().trim() : '';
2038
+ return /^\d{10,13}$/.test(trimmed);
1969
2039
  });
1970
2040
 
1971
- // Third pass: look for ISO-8601 dates (least specific, as it can match many formats)
2041
+ // Fourth pass: look for ISO-8601 dates (least specific, as it can match many formats)
1972
2042
  const isoDateColumns = Object.keys(data).filter(key => {
1973
2043
  const value = data[key];
2044
+ const trimmed = value != null ? value.toString().trim() : '';
1974
2045
  // Only consider it an ISO date if it's not already matched by more specific patterns
1975
- if (yyyyMmDdColumns.includes(key) || unixTimestampColumns.includes(key)) {
2046
+ if (yyyyMmDdColumns.includes(key) || ddMmYyyyColumns.includes(key) || unixTimestampColumns.includes(key)) {
1976
2047
  return false;
1977
2048
  }
1978
- return moment(value, moment.ISO_8601, true).isValid();
2049
+ return moment(trimmed, moment.ISO_8601, true).isValid();
1979
2050
  });
1980
2051
 
1981
- // Prioritize columns: YYYY-MM-DD first, then Unix timestamps, then ISO dates
1982
- datetimeColumns.push(...yyyyMmDdColumns, ...unixTimestampColumns, ...isoDateColumns);
2052
+ // Prioritize columns: YYYY-MM-DD, DD/MM/YYYY, Unix timestamps, then ISO dates
2053
+ datetimeColumns.push(...yyyyMmDdColumns, ...ddMmYyyyColumns, ...unixTimestampColumns, ...isoDateColumns);
1983
2054
 
1984
2055
  if (datetimeColumns.length > 0) {
1985
2056
  const datetimeValue = data[datetimeColumns[0]];
2057
+ const datetimeValueTrimmed = datetimeValue != null ? datetimeValue.toString().trim() : '';
1986
2058
  let datetime;
1987
2059
 
1988
2060
  // Check if it's a Unix-Timestamp
1989
- if (/^\d{10,13}$/.test(datetimeValue)) {
2061
+ if (/^\d{10,13}$/.test(datetimeValueTrimmed)) {
1990
2062
  // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
1991
- const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2063
+ const timestamp = datetimeValueTrimmed.length === 10 ? parseInt(datetimeValueTrimmed) * 1000 : parseInt(datetimeValueTrimmed);
1992
2064
  datetime = moment(timestamp);
1993
- } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2065
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValueTrimmed)) {
1994
2066
  // Handle as YYYY-MM-DD date format
1995
2067
  // Parse as date only, set time to 12:00 (noon) as default
1996
- datetime = moment(datetimeValue + 'T12:00:00');
2068
+ datetime = moment(datetimeValueTrimmed + 'T12:00:00');
2069
+ } else if (/^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(datetimeValueTrimmed)) {
2070
+ // Handle as DD/MM/YYYY (optional time)
2071
+ const hasTime = /\d{1,2}:\d{2}/.test(datetimeValueTrimmed);
2072
+ datetime = moment(datetimeValueTrimmed, ['DD/MM/YYYY HH:mm:ss', 'DD/MM/YYYY HH:mm', 'DD/MM/YYYY'], true);
2073
+ if (datetime.isValid() && !hasTime) {
2074
+ datetime = datetime.hour(12).minute(0);
2075
+ }
1997
2076
  } else {
1998
2077
  // Handle as ISO-8601 date
1999
- datetime = moment(datetimeValue);
2078
+ datetime = moment(datetimeValueTrimmed);
2079
+ }
2080
+
2081
+ if (!datetime.isValid()) {
2082
+ continue;
2000
2083
  }
2001
2084
 
2002
2085
  // Convert the date to the format needed for astrological calculations
@@ -2017,11 +2100,39 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2017
2100
  continue;
2018
2101
  }
2019
2102
 
2103
+ if (!Number.isFinite(astroData.longitude)) {
2104
+ continue;
2105
+ }
2106
+
2020
2107
  // Increase the counter for pending operations
2021
2108
  pendingOperations++;
2022
2109
 
2023
2110
  // Calculate the houses
2024
2111
  const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2112
+ if (!Number.isFinite(julianDay)) {
2113
+ let aspects = [];
2114
+ if (analyzeAspects) {
2115
+ try {
2116
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2117
+
2118
+ if (partnerPlanet) {
2119
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2120
+ }
2121
+ } catch (error) {}
2122
+ }
2123
+
2124
+ results.push({
2125
+ datetime: datetimeValue,
2126
+ planet: planetName,
2127
+ sign: astroData.sign,
2128
+ degreeInSign: astroData.degreeInSign,
2129
+ house: 'N/A',
2130
+ ...(analyzeAspects ? { aspects } : {})
2131
+ });
2132
+ pendingOperations--;
2133
+ continue;
2134
+ }
2135
+
2025
2136
  calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2026
2137
  .then(houses => {
2027
2138
  const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
@@ -2134,63 +2245,54 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2134
2245
  delimiter: delimiter
2135
2246
  });
2136
2247
 
2248
+ const filteredRecords = filterCriteria ? applyFilter(records, filterCriteria) : records;
2249
+
2137
2250
  // Process each record
2138
- for (const data of records) {
2139
- // Apply filter if specified
2140
- if (filterCriteria) {
2141
- const parsedCriteria = typeof filterCriteria === 'string'
2142
- ? parseFilterCriteria(filterCriteria)
2143
- : filterCriteria;
2144
-
2145
- if (parsedCriteria) {
2146
- let shouldSkip = false;
2147
-
2148
- if (Array.isArray(parsedCriteria)) {
2149
- // Multiple criteria - all must match
2150
- shouldSkip = !parsedCriteria.every(criterion => {
2151
- const recordValue = data[criterion.column];
2152
- return recordValue && recordValue.toString() === criterion.value;
2153
- });
2154
- } else {
2155
- // Single criteria
2156
- const recordValue = data[parsedCriteria.column];
2157
- shouldSkip = !recordValue || recordValue.toString() !== parsedCriteria.value;
2158
- }
2159
-
2160
- if (shouldSkip) {
2161
- continue; // Skip this record
2162
- }
2163
- }
2164
- }
2251
+ for (const data of filteredRecords) {
2165
2252
 
2166
- // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, or Unix-Timestamps
2253
+ // Look for a column with ISO-Datetime values, YYYY-MM-DD dates, DD/MM/YYYY dates (optional time), or Unix-Timestamps
2167
2254
  const datetimeColumns = Object.keys(data).filter(key => {
2168
2255
  const value = data[key];
2256
+ const trimmed = value != null ? value.toString().trim() : '';
2169
2257
  // Check for ISO-8601 date
2170
- const isISO = moment(value, moment.ISO_8601, true).isValid();
2258
+ const isISO = moment(trimmed, moment.ISO_8601, true).isValid();
2171
2259
  // Check for YYYY-MM-DD date format
2172
- const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(value);
2260
+ const isYYYYMMDD = /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
2261
+ // Check for DD/MM/YYYY date format
2262
+ const isDDMMYYYY = /^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(trimmed);
2173
2263
  // Check for Unix-Timestamp (number with 10 or 13 digits)
2174
- const isUnixTimestamp = /^\d{10,13}$/.test(value);
2175
- return isISO || isYYYYMMDD || isUnixTimestamp;
2264
+ const isUnixTimestamp = /^\d{10,13}$/.test(trimmed);
2265
+ return isISO || isYYYYMMDD || isDDMMYYYY || isUnixTimestamp;
2176
2266
  });
2177
2267
 
2178
2268
  if (datetimeColumns.length > 0) {
2179
2269
  const datetimeValue = data[datetimeColumns[0]];
2270
+ const datetimeValueTrimmed = datetimeValue != null ? datetimeValue.toString().trim() : '';
2180
2271
  let datetime;
2181
2272
 
2182
2273
  // Check if it's a Unix-Timestamp
2183
- if (/^\d{10,13}$/.test(datetimeValue)) {
2274
+ if (/^\d{10,13}$/.test(datetimeValueTrimmed)) {
2184
2275
  // Convert Unix-Timestamp to milliseconds (if 10 digits, multiply by 1000)
2185
- const timestamp = datetimeValue.length === 10 ? parseInt(datetimeValue) * 1000 : parseInt(datetimeValue);
2276
+ const timestamp = datetimeValueTrimmed.length === 10 ? parseInt(datetimeValueTrimmed) * 1000 : parseInt(datetimeValueTrimmed);
2186
2277
  datetime = moment(timestamp);
2187
- } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValue)) {
2278
+ } else if (/^\d{4}-\d{2}-\d{2}$/.test(datetimeValueTrimmed)) {
2188
2279
  // Handle as YYYY-MM-DD date format
2189
2280
  // Parse as date only, set time to 12:00 (noon) as default
2190
- datetime = moment(datetimeValue + 'T12:00:00');
2281
+ datetime = moment(datetimeValueTrimmed + 'T12:00:00');
2282
+ } else if (/^\d{2}\/\d{2}\/\d{4}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(datetimeValueTrimmed)) {
2283
+ // Handle as DD/MM/YYYY (optional time)
2284
+ const hasTime = /\d{1,2}:\d{2}/.test(datetimeValueTrimmed);
2285
+ datetime = moment(datetimeValueTrimmed, ['DD/MM/YYYY HH:mm:ss', 'DD/MM/YYYY HH:mm', 'DD/MM/YYYY'], true);
2286
+ if (datetime.isValid() && !hasTime) {
2287
+ datetime = datetime.hour(12).minute(0);
2288
+ }
2191
2289
  } else {
2192
2290
  // Handle as ISO-8601 date
2193
- datetime = moment(datetimeValue);
2291
+ datetime = moment(datetimeValueTrimmed);
2292
+ }
2293
+
2294
+ if (!datetime.isValid()) {
2295
+ continue;
2194
2296
  }
2195
2297
 
2196
2298
  // Convert the date to the format needed for astrological calculations
@@ -2211,11 +2313,39 @@ function analyzeCSVWithDatetime(filePath, planetName = 'moon', houseSystem = 'ko
2211
2313
  continue;
2212
2314
  }
2213
2315
 
2316
+ if (!Number.isFinite(astroData.longitude)) {
2317
+ continue;
2318
+ }
2319
+
2214
2320
  // Increase the counter for pending operations
2215
2321
  pendingOperations++;
2216
2322
 
2217
2323
  // Calculate the houses
2218
2324
  const julianDay = calculateJulianDayUTC(dateComponents, -datetime.utcOffset());
2325
+ if (!Number.isFinite(julianDay)) {
2326
+ let aspects = [];
2327
+ if (analyzeAspects) {
2328
+ try {
2329
+ aspects = calculatePlanetAspects(planetName, dateComponents, true);
2330
+
2331
+ if (partnerPlanet) {
2332
+ aspects = aspects.filter(a => a.planet.toLowerCase() === partnerPlanet.toLowerCase());
2333
+ }
2334
+ } catch (error) {}
2335
+ }
2336
+
2337
+ results.push({
2338
+ datetime: datetimeValue,
2339
+ planet: planetName,
2340
+ sign: astroData.sign,
2341
+ degreeInSign: astroData.degreeInSign,
2342
+ house: 'N/A',
2343
+ ...(analyzeAspects ? { aspects } : {})
2344
+ });
2345
+ pendingOperations--;
2346
+ continue;
2347
+ }
2348
+
2219
2349
  calculateHouses(julianDay, getHouseSystemCode(houseSystem), false)
2220
2350
  .then(houses => {
2221
2351
  const planetLongitude = parseFloat(astroData.degreeInSign) + (signs.indexOf(astroData.sign) * 30);
package/src/cli/cli.js CHANGED
@@ -2,7 +2,7 @@ const { Command } = require('commander');
2
2
  const { planets, signs } = require('../astrology/astrologyConstants');
3
3
  const { showRetrogradePlanets } = require('../astrology/retrogradeService');
4
4
  const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree, calculateTransitFrequency } = require('../astrology/astrologyService');
5
- const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
5
+ const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson, saveNote, showNotes, searchNotesBySituation } = require('../config/configService');
6
6
  const { parseAppleHealthXML } = require('../health/healthService');
7
7
  const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep, analyzeLateNightAspects, analyzeAllNighterAspects } = require('../health/healthAnalysis');
8
8
  const { getFileCreationDate, parseDateToComponents } = require('../utils/fileUtils');
@@ -159,6 +159,136 @@ const shouldUseBirthData = (options) => {
159
159
  return options.i;
160
160
  };
161
161
 
162
+ // Helper function to parse aspect type shorthands
163
+ const parseAspectType = (aspectCode) => {
164
+ const aspectMap = {
165
+ 'c': 'Conjunction',
166
+ 'con': 'Conjunction',
167
+ 'conjunction': 'Conjunction',
168
+ 'o': 'Opposition',
169
+ 'opp': 'Opposition',
170
+ 'opposition': 'Opposition',
171
+ 's': 'Square',
172
+ 'sq': 'Square',
173
+ 'square': 'Square',
174
+ 't': 'Trine',
175
+ 'tri': 'Trine',
176
+ 'trine': 'Trine',
177
+ 'se': 'Sextile',
178
+ 'sex': 'Sextile',
179
+ 'sextile': 'Sextile'
180
+ };
181
+
182
+ return aspectMap[aspectCode.toLowerCase()] || aspectCode;
183
+ };
184
+
185
+ // Helper function to parse house notation h[n]
186
+ const parseHouseNotation = (arg) => {
187
+ const houseMatch = arg.match(/^h\[(\d+)\]$/);
188
+ if (houseMatch) {
189
+ const houseNumber = parseInt(houseMatch[1]);
190
+ if (houseNumber >= 1 && houseNumber <= 12) {
191
+ return { isHouse: true, houseNumber: houseNumber };
192
+ }
193
+ }
194
+ return { isHouse: false };
195
+ };
196
+
197
+ // Helper function to create a note for the current astrological situation
198
+ const createAstrologicalNote = async (planetArg, planet2Arg, options, customDate = null, aspectType = null) => {
199
+ try {
200
+ // Get current date if not provided
201
+ const currentDate = customDate || getCurrentTimeInTimezone();
202
+
203
+ // Parse planet arguments for special notations
204
+ const planet1Parse = parseHouseNotation(planetArg);
205
+ const planet2Parse = planet2Arg ? parseHouseNotation(planet2Arg) : { isHouse: false };
206
+
207
+ // Create the note data structure
208
+ const noteData = {
209
+ content: options.note,
210
+ timestamp: new Date().toISOString()
211
+ };
212
+
213
+ // Handle different astrological notations
214
+ if (planet1Parse.isHouse) {
215
+ // House notation: h[n]
216
+ noteData.house = planet1Parse.houseNumber;
217
+ noteData.astrologicalSituation = `House ${planet1Parse.houseNumber}`;
218
+
219
+ if (planet2Arg && !planet2Parse.isHouse) {
220
+ // House with planet: h[n] planet
221
+ const planetData = getAstrologicalData(planet2Arg, currentDate);
222
+ noteData.planet = planet2Arg;
223
+ noteData.astrologicalSituation = `${planet2Arg} in House ${planet1Parse.houseNumber} (${planetData.sign} ${planetData.degreeInSign}°)`;
224
+ }
225
+ } else if (aspectType) {
226
+ // Explicit aspect notation: planet1 aspectType planet2 (e.g., saturn c moon)
227
+ const planet1Data = getAstrologicalData(planetArg, currentDate);
228
+ let planet2Data = null;
229
+
230
+ if (planet2Arg) {
231
+ planet2Data = getAstrologicalData(planet2Arg, currentDate);
232
+ }
233
+
234
+ noteData.planet1 = planetArg;
235
+ noteData.aspectType = aspectType;
236
+ noteData.astrologicalSituation = `${planetArg} ${aspectType}`;
237
+
238
+ if (planet2Arg) {
239
+ noteData.planet2 = planet2Arg;
240
+ noteData.astrologicalSituation += ` ${planet2Arg} (${planet1Data.sign} ${planet1Data.degreeInSign}° ${aspectType} ${planet2Data.sign} ${planet2Data.degreeInSign}°)`;
241
+ } else {
242
+ noteData.astrologicalSituation += ` (${planet1Data.sign} ${planet1Data.degreeInSign}°)`;
243
+ }
244
+ } else if (planetArg && planet2Arg) {
245
+ // Two planets - check if there's an aspect between them
246
+ const planet1Data = getAstrologicalData(planetArg, currentDate);
247
+ const aspects = calculatePlanetAspects(planetArg, currentDate);
248
+ const aspectWithPlanet2 = aspects.find(a => a.planet === planet2Arg);
249
+
250
+ if (aspectWithPlanet2) {
251
+ noteData.planet1 = planetArg;
252
+ noteData.planet2 = planet2Arg;
253
+ noteData.aspectType = aspectWithPlanet2.type;
254
+ noteData.astrologicalSituation = `${planetArg} ${aspectWithPlanet2.type} ${planet2Arg} (${planet1Data.sign} ${planet1Data.degreeInSign}°)`;
255
+ } else {
256
+ noteData.planet1 = planetArg;
257
+ noteData.planet2 = planet2Arg;
258
+ noteData.astrologicalSituation = `${planetArg} and ${planet2Arg} (no current aspect)`;
259
+ }
260
+ } else if (planetArg) {
261
+ // Single planet
262
+ const planetData = getAstrologicalData(planetArg, currentDate);
263
+ noteData.planet = planetArg;
264
+ noteData.astrologicalSituation = `${planetArg} in ${planetData.sign} (${planetData.degreeInSign}°)`;
265
+
266
+ // Add house information if available
267
+ if (options.hs) {
268
+ const houseSystem = options.hs.toLowerCase();
269
+ const julianDay = getJulianDay(currentDate, shouldUseBirthData(options));
270
+ const houses = await calculateHouses(julianDay, getHouseSystemCode(houseSystem), shouldUseBirthData(options));
271
+ const planetLongitude = parseFloat(planetData.degreeInSign) + (signs.indexOf(planetData.sign) * 30);
272
+ const house = getPlanetHouse(planetLongitude, houses.house);
273
+ noteData.house = house;
274
+ noteData.astrologicalSituation += ` in house ${house}`;
275
+ }
276
+ } else {
277
+ // General situation
278
+ noteData.astrologicalSituation = 'General astrological situation';
279
+ }
280
+
281
+ // Add date information
282
+ noteData.date = currentDate;
283
+
284
+ // Save the note
285
+ saveNote(noteData);
286
+
287
+ } catch (error) {
288
+ console.error('Error creating note:', error.message);
289
+ }
290
+ };
291
+
162
292
  // Function to calculate the Julian Day in UTC
163
293
  // This function is defined before use to avoid initialization issues
164
294
  const getJulianDay = (customDate = null, useBirthData = false) => {
@@ -251,6 +381,10 @@ program
251
381
  .option('--delete-person <id>', 'Deletes a person')
252
382
  .option('--t <days>', 'Time limit (e.g. 7d, 30d, 90d, 14d)')
253
383
  .option('--p <prompt>', 'Asks a question to the AI model (e.g. "Which careers suit this position?")')
384
+ .option('--note <content>', 'Adds a note to the current astrological situation')
385
+ .option('--notes', 'Shows all saved notes')
386
+ .option('--search-notes <situation>', 'Searches notes by astrological situation')
387
+ .option('--aspect <type>', 'Specifies aspect type for note (c, s, t, se, o)')
254
388
  .option('--el', 'Shows the element distribution of planets in a horizontal chart')
255
389
  .option('--af', 'Shows active aspect figures like T-squares, Grand Trines, etc.')
256
390
  .option('--tra', 'Shows personal transits based on birth data')
@@ -987,6 +1121,32 @@ program
987
1121
  return;
988
1122
  }
989
1123
 
1124
+ // Show all notes if --notes option is specified (no planet required)
1125
+ if (options.notes) {
1126
+ showNotes();
1127
+ return;
1128
+ }
1129
+
1130
+ // Search notes if --search-notes option is specified (no planet required)
1131
+ if (options.searchNotes) {
1132
+ const results = searchNotesBySituation(options.searchNotes);
1133
+ if (results.length === 0) {
1134
+ console.log(`No notes found for situation: "${options.searchNotes}"`);
1135
+ } else {
1136
+ console.log(`Found ${results.length} notes for situation: "${options.searchNotes}"`);
1137
+ console.log('================================================================================');
1138
+ results.forEach((note, index) => {
1139
+ console.log(`Note ${index + 1}:`);
1140
+ console.log(`Date: ${new Date(note.timestamp).toLocaleString()}`);
1141
+ if (note.content) {
1142
+ console.log(`Content: ${note.content}`);
1143
+ }
1144
+ console.log('');
1145
+ });
1146
+ }
1147
+ return;
1148
+ }
1149
+
990
1150
  // Create/update person 1 if --person1 option is specified (no planet required)
991
1151
  if (options.person1) {
992
1152
  await setPerson1(options.person1);
@@ -1269,6 +1429,56 @@ program
1269
1429
  process.exit(1);
1270
1430
  }
1271
1431
 
1432
+ // Handle note creation if --note option is specified
1433
+ if (options.note) {
1434
+ // Determine the custom date if specified
1435
+ let customDate = null;
1436
+ if (options.d) {
1437
+ // Try to parse the date as DD.MM.YYYY or DD.MM.YYYY HH:MM
1438
+ const dateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
1439
+ const match = options.d.match(dateRegex);
1440
+
1441
+ if (match) {
1442
+ const day = parseInt(match[1], 10);
1443
+ const month = parseInt(match[2], 10);
1444
+ const year = parseInt(match[3], 10);
1445
+ const hour = match[4] ? parseInt(match[4], 10) : 12; // Default: 12 o'clock
1446
+ const minute = match[5] ? parseInt(match[5], 10) : 0; // Default: 0 minutes
1447
+
1448
+ // Check if the date is valid
1449
+ const date = new Date(year, month - 1, day, hour, minute);
1450
+ if (date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day) {
1451
+ customDate = {
1452
+ day: day,
1453
+ month: month,
1454
+ year: year,
1455
+ hour: hour,
1456
+ minute: minute
1457
+ };
1458
+ }
1459
+ }
1460
+ }
1461
+
1462
+ // Check if we have an explicit aspect option
1463
+ let planet1 = planetArg;
1464
+ let aspectType = null;
1465
+ let planet2 = planet2Arg;
1466
+
1467
+ // Handle explicit aspect notation via --aspect option
1468
+ if (options.aspect) {
1469
+ aspectType = parseAspectType(options.aspect);
1470
+ }
1471
+
1472
+ // Create the note with the parsed arguments
1473
+ await createAstrologicalNote(planet1, planet2, options, customDate, aspectType);
1474
+
1475
+ // If only --note is specified, don't continue with other processing
1476
+ if (!options.a && !options.s && !options.hs && !options.k && !options.c && !options.rx && !options.el && !options.af && !options.tra && !options.tr && !options.in && !options.o && !options.p) {
1477
+ console.log('✓ Note saved successfully!');
1478
+ return;
1479
+ }
1480
+ }
1481
+
1272
1482
  // Show table view of all planet positions if --s option is specified
1273
1483
  if (options.s) {
1274
1484
  // Use person data for house calculation if --p1, --p2 or --i option is specified
@@ -889,7 +889,7 @@ class CLIService {
889
889
  const julianDayForHouses = getJulianDayUTCForHouses();
890
890
  const useBirthLocation = birthData !== null;
891
891
 
892
- houses = await calculateHouses(julianDayForHouses, getHouseSystemCode(houseSystem), useBirthLocation);
892
+ houses = await calculateHouses(julianDayForHouses, getHouseSystemCode(houseSystem), useBirthLocation, birthData ? birthData.location : null);
893
893
  } catch (error) {
894
894
  console.error('Error calculating houses:', error);
895
895
  }
@@ -264,6 +264,134 @@ function deletePerson(personId, userId = null) {
264
264
  return saveConfig(config, userId);
265
265
  }
266
266
 
267
+ // Function to get the notes file path
268
+ function getNotesPath(userId = null) {
269
+ if (process.env.KLIO_NOTES_PATH) {
270
+ return process.env.KLIO_NOTES_PATH;
271
+ }
272
+ let configDir;
273
+
274
+ // Determine configuration directory based on operating system
275
+ if (process.platform === 'win32') {
276
+ // Windows: Use %APPDATA%\astrocli
277
+ configDir = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'astrocli');
278
+ } else if (process.platform === 'darwin') {
279
+ // macOS: Use ~/Library/Application Support/astrocli
280
+ configDir = path.join(os.homedir(), 'Library', 'Application Support', 'astrocli');
281
+ } else {
282
+ // Linux and other Unix systems: Use ~/.config/astrocli
283
+ configDir = path.join(os.homedir(), '.config', 'astrocli');
284
+ }
285
+
286
+ // Create directory if it doesn't exist
287
+ if (!fs.existsSync(configDir)) {
288
+ fs.mkdirSync(configDir, { recursive: true });
289
+ }
290
+
291
+ // For user-specific notes, create user directory
292
+ if (userId) {
293
+ const userDir = path.join(configDir, 'users', userId);
294
+ if (!fs.existsSync(userDir)) {
295
+ fs.mkdirSync(userDir, { recursive: true });
296
+ }
297
+ return path.join(userDir, 'notes.json');
298
+ }
299
+
300
+ return path.join(configDir, 'notes.json');
301
+ }
302
+
303
+ // Function to save a note
304
+ function saveNote(noteData, userId = null) {
305
+ try {
306
+ const notesPath = getNotesPath(userId);
307
+ let notes = [];
308
+
309
+ // Load existing notes if file exists
310
+ if (fs.existsSync(notesPath)) {
311
+ const notesData = fs.readFileSync(notesPath, 'utf8');
312
+ notes = JSON.parse(notesData);
313
+ }
314
+
315
+ // Add timestamp if not provided
316
+ if (!noteData.timestamp) {
317
+ noteData.timestamp = new Date().toISOString();
318
+ }
319
+
320
+ // Add the new note
321
+ notes.push(noteData);
322
+
323
+ // Sort notes by timestamp (newest first)
324
+ notes.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
325
+
326
+ // Save the notes
327
+ fs.writeFileSync(notesPath, JSON.stringify(notes, null, 2));
328
+ console.log('✓ Note successfully saved!');
329
+ console.log(`📁 Location: ${notesPath}`);
330
+ return true;
331
+ } catch (error) {
332
+ console.error('Error saving note:', error.message);
333
+ return false;
334
+ }
335
+ }
336
+
337
+ // Function to load all notes
338
+ function loadNotes(userId = null) {
339
+ try {
340
+ const notesPath = getNotesPath(userId);
341
+ if (fs.existsSync(notesPath)) {
342
+ const notesData = fs.readFileSync(notesPath, 'utf8');
343
+ return JSON.parse(notesData);
344
+ }
345
+ } catch (error) {
346
+ console.error('Error loading notes:', error.message);
347
+ }
348
+ return [];
349
+ }
350
+
351
+ // Function to show all notes
352
+ function showNotes(userId = null) {
353
+ const notes = loadNotes(userId);
354
+
355
+ if (notes.length === 0) {
356
+ console.log('No notes found.');
357
+ return;
358
+ }
359
+
360
+ console.log('Your astrological notes:');
361
+ console.log('========================');
362
+
363
+ notes.forEach((note, index) => {
364
+ console.log(`Note ${index + 1}:`);
365
+ console.log(`Date: ${new Date(note.timestamp).toLocaleString()}`);
366
+
367
+ if (note.astrologicalSituation) {
368
+ console.log(`Astrological Situation: ${note.astrologicalSituation}`);
369
+ }
370
+
371
+ if (note.planet1 && note.aspectType && note.planet2) {
372
+ console.log(`Aspect: ${note.planet1} ${note.aspectType} ${note.planet2}`);
373
+ }
374
+
375
+ if (note.moonHouse) {
376
+ console.log(`Moon House: ${note.moonHouse}`);
377
+ }
378
+
379
+ if (note.content) {
380
+ console.log(`Content: ${note.content}`);
381
+ }
382
+
383
+ console.log('');
384
+ });
385
+ }
386
+
387
+ // Function to search notes by astrological situation
388
+ function searchNotesBySituation(situation, userId = null) {
389
+ const notes = loadNotes(userId);
390
+ return notes.filter(note =>
391
+ note.astrologicalSituation && note.astrologicalSituation.toLowerCase().includes(situation.toLowerCase())
392
+ );
393
+ }
394
+
267
395
  // Function to create/set person 1 (for backward compatibility)
268
396
  async function setPerson1(personString, userId = null) {
269
397
  return await setPerson('p1', personString, userId);
@@ -769,6 +897,11 @@ module.exports = {
769
897
  getPersonData,
770
898
  listPeople,
771
899
  deletePerson,
900
+ // Note functions
901
+ saveNote,
902
+ loadNotes,
903
+ showNotes,
904
+ searchNotesBySituation,
772
905
  // Add helper functions for user-specific operations
773
906
  getConfigPath
774
907
  };