klio 1.4.4 → 1.4.6

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
@@ -129,6 +129,15 @@ Then, instead using `--i` for the commands from above you can use `--wp <id>` i.
129
129
  - Example: `klio saturn --in 2` - Shows next 2 Saturn ingresses (note: slow planets may have limited future data)
130
130
  - Example: `klio mercury --in --d "15.03.2026"` - Shows next Mercury ingress from a specific date
131
131
 
132
+ ### Wikidata Integration
133
+
134
+ - **Search for people with specific aspects**: `[planet1] [aspect-type] [planet2] --wiki <occupation> [limit]` - Searches Wikidata for people with specific astrological aspects
135
+ - **Available occupations**: authors, scientists, artists, musicians, politicians, actors, philosophers
136
+ - **Aspect types**: c (conjunction), o (opposition), s (square), t (trine), se (sextile), q (quincunx)
137
+ - **Example**: `klio moon c neptune --wiki authors 50` - Tries to find authors with Moon conjunct Neptune aspect with a limit of 50. Is faster but less common aspects are maybe not found with 50
138
+ - **Example**: `klio saturn s pluto --wiki scientists 100` - Finds scientists with Saturn square Pluto aspect
139
+ - **Example**: `klio sun t moon --wiki artists 200` - Finds artists with Sun trine Moon aspect
140
+
132
141
  ### AI Integration
133
142
 
134
143
  - **AI model selection**: `--ai <model>` - Sets a specific AI model (e.g., "google/gemma-3n-e4b") for LM Studio ([lmstudio.ai](https://lmstudio.ai))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klio",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "A CLI for astrological calculations",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -281,6 +281,39 @@ function calculateHouses(julianDay, houseSystem = 'K', useBirthLocation = false)
281
281
  });
282
282
  }
283
283
 
284
+ // Function to calculate astrological angles from house cusps
285
+ function calculateAstrologicalAngles(houseCusps) {
286
+ // House cusps are in houseCusps[0] to houseCusps[11]
287
+ // In most house systems:
288
+ // ASC (Ascendant) = House 1 cusp (index 0)
289
+ // MC (Medium Coeli/Midheaven) = House 10 cusp (index 9)
290
+ // DESC (Descendant) = House 7 cusp (index 6)
291
+ // IC (Imum Coeli) = House 4 cusp (index 3)
292
+
293
+ return {
294
+ asc: houseCusps[0], // Ascendant
295
+ mc: houseCusps[9], // Medium Coeli (Midheaven)
296
+ desc: houseCusps[6], // Descendant
297
+ ic: houseCusps[3] // Imum Coeli
298
+ };
299
+ }
300
+
301
+ // Function to convert longitude to sign and degree
302
+ function longitudeToSignDegree(longitude) {
303
+ // Normalize longitude to 0-360 range
304
+ longitude = longitude % 360;
305
+ if (longitude < 0) longitude += 360;
306
+
307
+ // Calculate sign (0-11) and degree within sign (0-30)
308
+ const signIndex = Math.floor(longitude / 30);
309
+ const degreeInSign = longitude % 30;
310
+
311
+ return {
312
+ sign: signs[signIndex],
313
+ degree: degreeInSign.toFixed(2)
314
+ };
315
+ }
316
+
284
317
  // Function to determine the house for a planet
285
318
  function getPlanetHouse(planetLongitude, houseCusps) {
286
319
  // Normalize planet longitude to 0-360 range
@@ -2745,5 +2778,7 @@ module.exports = {
2745
2778
  calculateAspectStatistics,
2746
2779
  calculatePlanetComboAspects,
2747
2780
  showPlanetComboAspects,
2748
- calculateNextPlanetIngress
2781
+ calculateNextPlanetIngress,
2782
+ calculateAstrologicalAngles,
2783
+ longitudeToSignDegree
2749
2784
  };
package/src/cli/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { Command } = require('commander');
2
2
  const { planets, signs } = require('../astrology/astrologyConstants');
3
3
  const { showRetrogradePlanets } = require('../astrology/retrogradeService');
4
- const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress } = require('../astrology/astrologyService');
4
+ const { getCurrentTimeInTimezone, showAspectFigures, analyzeElementDistribution, getTimezoneOffset, calculateJulianDayUTC, calculateHouses, getAstrologicalData, getPlanetHouse, showPlanetAspects, calculatePlanetAspects, getAllActiveAspects, showAllActiveAspects, getBirthDataFromConfig, getPersonDataFromConfig, detectAspectFigures, calculatePersonalTransits, showPersonalTransitAspects, showCombinedAnalysis, calculatePersonalTransitAspects, determineAspectPhase, findExactAspectTime, findLastExactAspectTime, getAspectAngle, getFutureAspects, getPastAspects, analyzeCSVWithDatetime, analyzeHouseDistributionSignificance, analyzeAspectDistributionSignificance, analyzeSignDistributionSignificance, calculateAspectStatistics, calculatePlanetComboAspects, showPlanetComboAspects, getCriticalPlanets, getHouseSystemCode, calculateNextPlanetIngress, calculateAstrologicalAngles, longitudeToSignDegree } = require('../astrology/astrologyService');
5
5
  const { performSetup, showConfigStatus, loadConfig, setAIModel, askAIModel, setPerson1, setPerson2, setPerson, getPersonData, listPeople, deletePerson } = require('../config/configService');
6
6
  const { parseAppleHealthXML } = require('../health/healthService');
7
7
  const { analyzeStepsByPlanetSign, analyzeStressByPlanetAspects, analyzePlanetAspectsForSleep } = require('../health/healthAnalysis');
@@ -11,6 +11,15 @@ const swisseph = require('swisseph');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
 
14
+ // Wikidata service import
15
+ let wikidataService = null;
16
+ try {
17
+ wikidataService = require('../wikidata/wikidataService');
18
+ } catch (error) {
19
+ // Wikidata service not available
20
+ console.debug('Wikidata service not available');
21
+ }
22
+
14
23
  // GUI Server import
15
24
  let guiServer = null;
16
25
  try {
@@ -255,6 +264,7 @@ program
255
264
  .option('--z <count>', 'Shows future aspects between two planets (Format: --z <count> planet1 aspectType planet2)')
256
265
  .option('--csv <filepath>', 'Analyzes a CSV file with ISO-Datetime values or Unix timestamps')
257
266
  .option('--in [count]', 'Shows next planet ingress (entering new sign). Optional count for multiple ingresses')
267
+ .option('--wiki <occupation>', 'Fetches people from Wikidata by occupation and checks for specific aspects (Format: planet1 aspectType planet2 --wiki <occupation> [limit])')
258
268
  .option('--gui', 'Launches the web interface for command history (port 37421)')
259
269
  .option('--gui-port <port>', 'Specify custom port for GUI server')
260
270
  .description('Shows astrological data for a planet')
@@ -1112,6 +1122,111 @@ program
1112
1122
  return;
1113
1123
  }
1114
1124
 
1125
+ // Handle Wikidata people search if --wiki option is specified
1126
+ if (options.wiki) {
1127
+ // For the wiki command, the format is: planet1 aspectType planet2 --wiki <occupation> [limit]
1128
+ // But the argument parsing is: planetArg=planet1, planet2=aspectType, and options.wiki=planet2
1129
+ // We need to extract the occupation and planet2 from the remaining arguments
1130
+
1131
+ if (!planetArg || !planet2 || !options.wiki) {
1132
+ console.error('Error: Wikidata search requires format: planet1 aspectType planet2 --wiki <occupation> [limit]');
1133
+ console.error('Example: klio saturn conjunction neptune --wiki authors');
1134
+ console.error('Available occupations:', Object.keys(wikidataService.PREDEFINED_OCCUPATIONS || {}).join(', '));
1135
+ process.exit(1);
1136
+ }
1137
+
1138
+ const planet1 = planetArg.toLowerCase();
1139
+ const aspectType = planet2.toLowerCase();
1140
+
1141
+ // The occupation is in options.wiki, but we need to find the actual planet2
1142
+ // Let's look at the original arguments to parse this correctly
1143
+ const args = process.argv.slice(2); // Skip 'node' and script name
1144
+
1145
+ // Find the position of --wiki
1146
+ const wikiIndex = args.findIndex(arg => arg === '--wiki');
1147
+ if (wikiIndex === -1 || wikiIndex + 1 >= args.length) {
1148
+ console.error('Error: Missing occupation after --wiki flag');
1149
+ process.exit(1);
1150
+ }
1151
+
1152
+ const occupation = args[wikiIndex + 1].toLowerCase();
1153
+
1154
+ // The planet2 should be the argument before --wiki
1155
+ const planet2Name = args[wikiIndex - 1].toLowerCase();
1156
+
1157
+ // Validate planets
1158
+ if (planets[planet1] === undefined) {
1159
+ console.error(`Error: Invalid planet '${planet1}'. Available planets: ${Object.keys(planets).join(', ')}`);
1160
+ process.exit(1);
1161
+ }
1162
+
1163
+ if (planets[planet2Name] === undefined) {
1164
+ console.error(`Error: Invalid planet '${planet2Name}'. Available planets: ${Object.keys(planets).join(', ')}`);
1165
+ process.exit(1);
1166
+ }
1167
+
1168
+ // Validate aspect type (including shorthands)
1169
+ const validAspects = ['conjunction', 'opposition', 'square', 'trine', 'sextile', 'quincunx', 'c', 'con', 'opp', 'sq', 'tri', 'sex', 'qui'];
1170
+ if (!validAspects.includes(aspectType)) {
1171
+ console.error(`Error: Invalid aspect type '${aspectType}'. Available aspects: ${['conjunction', 'opposition', 'square', 'trine', 'sextile', 'quincunx'].join(', ')}`);
1172
+ console.error('Shorthands: c/con, opp, sq, tri, sex, qui');
1173
+ process.exit(1);
1174
+ }
1175
+
1176
+ // Parse limit if provided (look for arguments after occupation)
1177
+ let limit = 500;
1178
+ if (wikiIndex + 2 < args.length) {
1179
+ const limitArg = args[wikiIndex + 2];
1180
+ if (!isNaN(limitArg) && parseInt(limitArg) > 0) {
1181
+ limit = parseInt(limitArg);
1182
+ }
1183
+ }
1184
+
1185
+ console.log(`🔍 Searching Wikidata for ${occupation} with ${planet1} ${aspectType} ${planet2Name} aspect...`);
1186
+ console.log(`📊 Limit: ${limit} people`);
1187
+
1188
+ if (!wikidataService) {
1189
+ console.error('❌ Wikidata service is not available.');
1190
+ process.exit(1);
1191
+ }
1192
+
1193
+ try {
1194
+ const results = await wikidataService.findPeopleWithAspect(occupation, planet1, aspectType, planet2Name, limit);
1195
+
1196
+ if (results.length === 0) {
1197
+ console.log('No results found.');
1198
+ return;
1199
+ }
1200
+
1201
+ // Display results in a table
1202
+ console.log('\n📋 Results (sorted by orb, closest aspects first):');
1203
+ console.log('================================================================================');
1204
+ console.log('| # | Name | Birth Date | Aspect Type | Orb | Wikidata Link |');
1205
+ console.log('================================================================================');
1206
+
1207
+ results.forEach((result, index) => {
1208
+ const name = result.name.padEnd(28, ' ').substring(0, 28);
1209
+ const birthDate = result.birthDate ? result.birthDate.substring(0, 10) : 'Unknown';
1210
+ const aspectTypeDisplay = result.aspect.type.padEnd(11, ' ');
1211
+ const orb = result.aspect.orb.padEnd(4, ' ');
1212
+ const linkUrl = result.linkUrl || 'N/A';
1213
+
1214
+ console.log(`| ${(index + 1).toString().padEnd(2)} | ${name} | ${birthDate} | ${aspectTypeDisplay} | ${orb}° | ${linkUrl} |`);
1215
+ });
1216
+
1217
+ console.log('================================================================================');
1218
+ console.log(`\n📊 Found ${results.length} ${occupation} with ${planet1} ${aspectType} ${planet2Name} aspect.`);
1219
+ console.log('🔗 Wikipedia search links are provided for each person.');
1220
+ console.log('🎯 Results are sorted by orb (closest aspects first).');
1221
+
1222
+ } catch (error) {
1223
+ console.error('❌ Error searching Wikidata:', error.message);
1224
+ process.exit(1);
1225
+ }
1226
+
1227
+ return;
1228
+ }
1229
+
1115
1230
  // For other options, a planet is required (except for --a or --s without planet)
1116
1231
  if (!planetArg && !options.a && !options.s) {
1117
1232
  console.error('Error: Planet is required for this operation.');
@@ -1279,6 +1394,34 @@ program
1279
1394
 
1280
1395
  console.log('================================================================================');
1281
1396
 
1397
+ // Show astrological angles if houses were calculated
1398
+ if (houses) {
1399
+ console.log('\nAstrological Angles:');
1400
+ console.log('================================================================================');
1401
+ console.log('| Angle | Sign | Degree |');
1402
+ console.log('================================================================================');
1403
+
1404
+ const angles = calculateAstrologicalAngles(houses.house);
1405
+
1406
+ // ASC (Ascendant)
1407
+ const ascData = longitudeToSignDegree(angles.asc);
1408
+ console.log(`| ASC | ${ascData.sign.padEnd(10)} | ${ascData.degree.padEnd(6)}° |`);
1409
+
1410
+ // MC (Medium Coeli/Midheaven)
1411
+ const mcData = longitudeToSignDegree(angles.mc);
1412
+ console.log(`| MC | ${mcData.sign.padEnd(10)} | ${mcData.degree.padEnd(6)}° |`);
1413
+
1414
+ // DESC (Descendant)
1415
+ const descData = longitudeToSignDegree(angles.desc);
1416
+ console.log(`| DESC | ${descData.sign.padEnd(10)} | ${descData.degree.padEnd(6)}° |`);
1417
+
1418
+ // IC (Imum Coeli)
1419
+ const icData = longitudeToSignDegree(angles.ic);
1420
+ console.log(`| IC | ${icData.sign.padEnd(10)} | ${icData.degree.padEnd(6)}° |`);
1421
+
1422
+ console.log('================================================================================');
1423
+ }
1424
+
1282
1425
  if (birthData) {
1283
1426
  if (useNatalPositions) {
1284
1427
  console.log('\nThis analysis shows your natal planet positions.');
@@ -1315,6 +1458,18 @@ program
1315
1458
  };
1316
1459
  }
1317
1460
 
1461
+ // Add astrological angles to AI data if houses were calculated
1462
+ let anglesData = null;
1463
+ if (houses) {
1464
+ const angles = calculateAstrologicalAngles(houses.house);
1465
+ anglesData = {
1466
+ asc: longitudeToSignDegree(angles.asc),
1467
+ mc: longitudeToSignDegree(angles.mc),
1468
+ desc: longitudeToSignDegree(angles.desc),
1469
+ ic: longitudeToSignDegree(angles.ic)
1470
+ };
1471
+ }
1472
+
1318
1473
  const aiData = {
1319
1474
  planet: 'alle',
1320
1475
  sign: 'Multiple',
@@ -1323,7 +1478,8 @@ program
1323
1478
  element: 'Multiple',
1324
1479
  decan: 'Multiple',
1325
1480
  allPlanetData: allPlanetData,
1326
- houseSystem: houseSystem
1481
+ houseSystem: houseSystem,
1482
+ astrologicalAngles: anglesData
1327
1483
  };
1328
1484
 
1329
1485
  await askAIModel(actualOptions.p, aiData);
@@ -0,0 +1,228 @@
1
+ // Wikidata Service for fetching author data and checking astrological aspects
2
+ const axios = require('axios');
3
+ const { calculatePlanetComboAspects } = require('../astrology/astrologyService');
4
+
5
+ // Wikidata SPARQL endpoint
6
+ const WIKIDATA_SPARQL_ENDPOINT = 'https://query.wikidata.org/sparql';
7
+
8
+ // Predefined occupations with their Wikidata Q IDs
9
+ const PREDEFINED_OCCUPATIONS = {
10
+ 'authors': 'wd:Q36180', // Writer
11
+ 'scientists': 'wd:Q901', // Scientist
12
+ 'artists': 'wd:Q1028181', // Visual artist
13
+ 'musicians': 'wd:Q639669', // Musician
14
+ 'politicians': 'wd:Q82955', // Politician
15
+ 'actors': 'wd:Q33999', // Actor
16
+ 'philosophers': 'wd:Q4964182' // Philosopher
17
+ };
18
+
19
+ // Function to fetch people by occupation from Wikidata
20
+ async function fetchPeopleByOccupation(occupation, limit = 500) {
21
+ try {
22
+ // Get the Wikidata Q ID for the occupation
23
+ const occupationQid = PREDEFINED_OCCUPATIONS[occupation.toLowerCase()];
24
+
25
+ if (!occupationQid) {
26
+ console.error(`Error: Unknown occupation '${occupation}'. Available occupations: ${Object.keys(PREDEFINED_OCCUPATIONS).join(', ')}`);
27
+ return [];
28
+ }
29
+
30
+ const query = `
31
+ SELECT DISTINCT ?person ?personLabel ?birthDate ?wikipediaLink WHERE {
32
+ ?person wdt:P31 wd:Q5; # Instance of human
33
+ wdt:P106 ${occupationQid}. # Specific occupation
34
+
35
+ # Make birth date optional
36
+ OPTIONAL { ?person wdt:P569 ?birthDate. }
37
+
38
+ # Get English Wikipedia article link
39
+ OPTIONAL { ?wikipediaArticle schema:about ?person ;
40
+ schema:isPartOf <https://en.wikipedia.org/> ;
41
+ schema:url ?wikipediaLink . }
42
+
43
+ # Get label
44
+ SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
45
+ }
46
+ LIMIT ${limit}
47
+ `;
48
+
49
+ const response = await axios.get(WIKIDATA_SPARQL_ENDPOINT, {
50
+ params: {
51
+ query: query,
52
+ format: 'json'
53
+ },
54
+ headers: {
55
+ 'Accept': 'application/sparql-results+json'
56
+ }
57
+ });
58
+
59
+ if (response.data && response.data.results && response.data.results.bindings) {
60
+ return response.data.results.bindings.map(binding => ({
61
+ name: binding.personLabel?.value || 'Unknown',
62
+ birthDate: binding.birthDate?.value || null,
63
+ wikidataId: binding.person.value.split('/').pop()
64
+ }));
65
+ }
66
+
67
+ return [];
68
+ } catch (error) {
69
+ console.error('Error fetching people from Wikidata:', error.message);
70
+ return [];
71
+ }
72
+ }
73
+
74
+ // Backward compatibility function
75
+ async function fetchAuthorsFromWikidata(limit = 500) {
76
+ return fetchPeopleByOccupation('authors', limit);
77
+ }
78
+
79
+ // Function to parse birth date and extract astrological data
80
+ function parseBirthDate(birthDateString) {
81
+ if (!birthDateString) return null;
82
+
83
+ try {
84
+ // Parse ISO date string (format: +YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ssZ or +YYYY-MM-DD or YYYY-MM-DD or +YYYY-MM or YYYY-MM or +YYYY or YYYY)
85
+ const dateMatch = birthDateString.match(/^\+?(\d{4})(?:-(\d{1,2})(?:-(\d{1,2}))?)?/);
86
+ if (!dateMatch) return null;
87
+
88
+ const year = parseInt(dateMatch[1]);
89
+ const month = dateMatch[2] ? parseInt(dateMatch[2]) : 1; // Default to January if month not specified
90
+ const day = dateMatch[3] ? parseInt(dateMatch[3]) : 1; // Default to 1st if day not specified
91
+
92
+ // Handle invalid month/day values (like 00 for century-level dates)
93
+ const validMonth = month === 0 ? 1 : month;
94
+ const validDay = day === 0 ? 1 : day;
95
+
96
+ // Default time to noon if not specified
97
+ return {
98
+ year: year,
99
+ month: validMonth,
100
+ day: validDay,
101
+ hour: 12,
102
+ minute: 0
103
+ };
104
+ } catch (error) {
105
+ console.error('Error parsing birth date:', error.message);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // Aspect type mapping for shorthand support
111
+ const ASPECT_SHORTHAND = {
112
+ 'c': 'conjunction',
113
+ 'o': 'opposition',
114
+ 's': 'square',
115
+ 't': 'trine',
116
+ 'se': 'sextile',
117
+ 'q': 'quincunx'
118
+ };
119
+
120
+ // Function to create a Wikipedia search URL from a person's name
121
+ function createWikipediaSearchUrl(personName) {
122
+ // Create a Wikipedia search URL using the person's name
123
+ // This will take users to a search page where they can find the correct article
124
+ const encodedName = encodeURIComponent(personName);
125
+ return `https://en.wikipedia.org/wiki/Special:Search?search=${encodedName}`;
126
+ }
127
+
128
+ // Function to normalize aspect type (support shorthands)
129
+ function normalizeAspectType(aspectType) {
130
+ const normalized = aspectType.toLowerCase();
131
+ return ASPECT_SHORTHAND[normalized] || normalized;
132
+ }
133
+
134
+ // Function to check if an author has a specific aspect in their natal chart
135
+ function checkAuthorAspect(authorData, planet1, aspectType, planet2) {
136
+ if (!authorData.birthDateData) return null;
137
+
138
+ try {
139
+ // Normalize aspect type to handle shorthands
140
+ const normalizedAspectType = normalizeAspectType(aspectType);
141
+
142
+ // Calculate aspects for this author's birth chart
143
+ const aspects = calculatePlanetComboAspects([planet1, planet2], authorData.birthDateData, true);
144
+
145
+ // Look for the specific aspect type
146
+ const matchingAspect = aspects.find(aspect =>
147
+ ((aspect.planet1 === planet1 && aspect.planet2 === planet2) ||
148
+ (aspect.planet1 === planet2 && aspect.planet2 === planet1)) &&
149
+ aspect.type.toLowerCase() === normalizedAspectType.toLowerCase()
150
+ );
151
+
152
+ return matchingAspect || null;
153
+ } catch (error) {
154
+ console.error('Error checking aspect for author:', authorData.name, error.message);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ // Main function to find people with specific aspects by occupation
160
+ async function findPeopleWithAspect(occupation, planet1, aspectType, planet2, limit = 500) {
161
+ // Fetch people by occupation from Wikidata
162
+ const people = await fetchPeopleByOccupation(occupation, limit);
163
+
164
+ if (people.length === 0) {
165
+ console.log(`No ${occupation} found in Wikidata.`);
166
+ return [];
167
+ }
168
+
169
+ console.log(`Fetched ${people.length} ${occupation} from Wikidata. Checking aspects...`);
170
+
171
+ // Process each person to check for the specified aspect
172
+ const results = [];
173
+ let processedCount = 0;
174
+
175
+ for (const person of people) {
176
+ // Parse birth date
177
+ const birthDateData = parseBirthDate(person.birthDate);
178
+
179
+
180
+ // Add birth date data to person object
181
+ person.birthDateData = birthDateData;
182
+
183
+ // Check for the specific aspect
184
+ const aspect = checkAuthorAspect(person, planet1, aspectType, planet2);
185
+
186
+ if (aspect) {
187
+ // Create Wikipedia search URL using the person's name
188
+ const linkUrl = createWikipediaSearchUrl(person.name);
189
+
190
+ results.push({
191
+ name: person.name,
192
+ birthDate: person.birthDate,
193
+ linkUrl: linkUrl,
194
+ aspect: aspect,
195
+ birthDateData: person.birthDateData
196
+ });
197
+ }
198
+
199
+ processedCount++;
200
+ if (processedCount % 50 === 0) {
201
+ console.log(`Processed ${processedCount}/${people.length} ${occupation}...`);
202
+ }
203
+ }
204
+
205
+ console.log(`Found ${results.length} ${occupation} with ${planet1} ${aspectType} ${planet2} aspect.`);
206
+
207
+ // Sort by orb (closest aspects first)
208
+ results.sort((a, b) => parseFloat(a.aspect.orb) - parseFloat(b.aspect.orb));
209
+
210
+ return results;
211
+ }
212
+
213
+ // Backward compatibility function
214
+ async function findAuthorsWithAspect(planet1, aspectType, planet2, limit = 500) {
215
+ return findPeopleWithAspect('authors', planet1, aspectType, planet2, limit);
216
+ }
217
+
218
+ module.exports = {
219
+ fetchAuthorsFromWikidata,
220
+ findAuthorsWithAspect,
221
+ fetchPeopleByOccupation,
222
+ findPeopleWithAspect,
223
+ parseBirthDate,
224
+ checkAuthorAspect,
225
+ normalizeAspectType,
226
+ ASPECT_SHORTHAND,
227
+ PREDEFINED_OCCUPATIONS
228
+ };