@iflow-mcp/dearcloud09-logseq-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +22 -0
- package/LICENSE +135 -0
- package/README.ko.md +345 -0
- package/README.md +302 -0
- package/add-today-dairy.js +165 -0
- package/com.logseq.daily-automation.plist.example +41 -0
- package/dist/graph.d.ts +97 -0
- package/dist/graph.js +627 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +626 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +4 -0
- package/dist/weather-scraper.d.ts +22 -0
- package/dist/weather-scraper.js +202 -0
- package/language.json +1 -0
- package/package.json +1 -0
- package/package_name +1 -0
- package/push_info.json +5 -0
- package/run-daily-automation.sh +19 -0
- package/run-daily-automation.sh.example +27 -0
- package/src/graph.ts +710 -0
- package/src/index.ts +697 -0
- package/src/types.ts +66 -0
- package/src/weather-scraper.ts +249 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface WeatherData {
|
|
2
|
+
description: string;
|
|
3
|
+
temperature: number;
|
|
4
|
+
minTemperature?: number;
|
|
5
|
+
maxTemperature?: number;
|
|
6
|
+
feelsLike?: number;
|
|
7
|
+
humidity?: number;
|
|
8
|
+
windSpeed?: number;
|
|
9
|
+
pm10?: string;
|
|
10
|
+
pm25?: string;
|
|
11
|
+
uvIndex?: string;
|
|
12
|
+
sunset?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class WeatherScraper {
|
|
15
|
+
private location;
|
|
16
|
+
constructor(location?: string);
|
|
17
|
+
getWeather(): Promise<WeatherData | null>;
|
|
18
|
+
getWeatherForDongcheon(): Promise<WeatherData | null>;
|
|
19
|
+
private getWeatherFromNaver;
|
|
20
|
+
formatWeatherForLogseq(weather: WeatherData): string[];
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as cheerio from 'cheerio';
|
|
3
|
+
export class WeatherScraper {
|
|
4
|
+
location;
|
|
5
|
+
constructor(location) {
|
|
6
|
+
this.location = location || process.env.WEATHER_LOCATION || '서울';
|
|
7
|
+
}
|
|
8
|
+
async getWeather() {
|
|
9
|
+
return await this.getWeatherFromNaver();
|
|
10
|
+
}
|
|
11
|
+
// 하위 호환성 유지
|
|
12
|
+
async getWeatherForDongcheon() {
|
|
13
|
+
return await this.getWeather();
|
|
14
|
+
}
|
|
15
|
+
async getWeatherFromNaver() {
|
|
16
|
+
try {
|
|
17
|
+
const url = `https://search.naver.com/search.naver?query=${encodeURIComponent(this.location)}+날씨`;
|
|
18
|
+
const response = await axios.get(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
21
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
22
|
+
'Accept-Language': 'ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3',
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const $ = cheerio.load(response.data);
|
|
26
|
+
console.log('네이버 검색 결과에서 날씨 정보 추출 중...');
|
|
27
|
+
// 현재 온도
|
|
28
|
+
let temperature = 0;
|
|
29
|
+
const temperatureSelectors = [
|
|
30
|
+
'.temperature_text',
|
|
31
|
+
'.main_temp',
|
|
32
|
+
'.current strong',
|
|
33
|
+
'.today_area .temp'
|
|
34
|
+
];
|
|
35
|
+
for (const selector of temperatureSelectors) {
|
|
36
|
+
const tempText = $(selector).text();
|
|
37
|
+
const tempMatch = tempText.match(/-?\d+(?:\.\d+)?/);
|
|
38
|
+
if (tempMatch) {
|
|
39
|
+
temperature = Math.round(parseFloat(tempMatch[0]));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 날씨 상태
|
|
44
|
+
let description = '맑음';
|
|
45
|
+
const descSelectors = [
|
|
46
|
+
'.weather_main',
|
|
47
|
+
'.cast_txt',
|
|
48
|
+
'.summary_txt',
|
|
49
|
+
'.today_area .main .cast_txt',
|
|
50
|
+
'.current .weather',
|
|
51
|
+
'.weather_description'
|
|
52
|
+
];
|
|
53
|
+
for (const selector of descSelectors) {
|
|
54
|
+
const descText = $(selector).first().text().trim();
|
|
55
|
+
if (descText && !descText.includes('°') && !descText.includes('어제') && descText.length < 10) {
|
|
56
|
+
description = descText;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (description.length > 10) {
|
|
61
|
+
const firstWord = description.split(/\s+/)[0];
|
|
62
|
+
if (firstWord && firstWord.length < 10) {
|
|
63
|
+
description = firstWord;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 최저/최고 온도
|
|
67
|
+
let minTemperature;
|
|
68
|
+
let maxTemperature;
|
|
69
|
+
const wholeText = $('body').text();
|
|
70
|
+
const tempRangeMatch1 = wholeText.match(/(\d+)°\s*(\d+)°/);
|
|
71
|
+
if (tempRangeMatch1) {
|
|
72
|
+
minTemperature = parseInt(tempRangeMatch1[1]);
|
|
73
|
+
maxTemperature = parseInt(tempRangeMatch1[2]);
|
|
74
|
+
}
|
|
75
|
+
if (!minTemperature || !maxTemperature) {
|
|
76
|
+
const koreanTempMatch = wholeText.match(/최저[^\d]*(\d+)[^\d]*최고[^\d]*(\d+)/);
|
|
77
|
+
if (koreanTempMatch) {
|
|
78
|
+
minTemperature = parseInt(koreanTempMatch[1]);
|
|
79
|
+
maxTemperature = parseInt(koreanTempMatch[2]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 최저가 최고보다 크면 swap (네이버에서 순서가 다를 수 있음)
|
|
83
|
+
if (minTemperature !== undefined && maxTemperature !== undefined && minTemperature > maxTemperature) {
|
|
84
|
+
[minTemperature, maxTemperature] = [maxTemperature, minTemperature];
|
|
85
|
+
}
|
|
86
|
+
// 체감온도
|
|
87
|
+
let feelsLike;
|
|
88
|
+
const infoText = $('.today_area .info_list').text() || $('.detail_box').text() || wholeText;
|
|
89
|
+
if (infoText.includes('체감')) {
|
|
90
|
+
const feelsMatch = infoText.match(/체감[^\d]*(\d+(?:\.\d+)?)/);
|
|
91
|
+
if (feelsMatch) {
|
|
92
|
+
feelsLike = Math.round(parseFloat(feelsMatch[1]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 습도
|
|
96
|
+
let humidity;
|
|
97
|
+
const humidityMatch = infoText.match(/습도[^\d]*(\d+)%/);
|
|
98
|
+
if (humidityMatch) {
|
|
99
|
+
humidity = parseInt(humidityMatch[1]);
|
|
100
|
+
}
|
|
101
|
+
// 바람
|
|
102
|
+
let windSpeed;
|
|
103
|
+
const windMatch = infoText.match(/(\d+(?:\.\d+)?)m\/s/);
|
|
104
|
+
if (windMatch) {
|
|
105
|
+
windSpeed = parseFloat(windMatch[1]);
|
|
106
|
+
}
|
|
107
|
+
// 미세먼지
|
|
108
|
+
let pm10;
|
|
109
|
+
const dustMatch = wholeText.match(/미세먼지[^\w]*(좋음|보통|나쁨|매우나쁨)/);
|
|
110
|
+
if (dustMatch)
|
|
111
|
+
pm10 = dustMatch[1];
|
|
112
|
+
// 초미세먼지
|
|
113
|
+
let pm25;
|
|
114
|
+
const fineDustMatch = wholeText.match(/초미세먼지[^\w]*(좋음|보통|나쁨|매우나쁨)/);
|
|
115
|
+
if (fineDustMatch)
|
|
116
|
+
pm25 = fineDustMatch[1];
|
|
117
|
+
// 자외선
|
|
118
|
+
let uvIndex;
|
|
119
|
+
const uvMatch = wholeText.match(/자외선[^\w]*(좋음|보통|나쁨|매우나쁨)/);
|
|
120
|
+
if (uvMatch)
|
|
121
|
+
uvIndex = uvMatch[1];
|
|
122
|
+
// 일몰 시간
|
|
123
|
+
let sunset;
|
|
124
|
+
const sunsetMatch = wholeText.match(/일몰[^\d]*(\d{1,2}:\d{2})/);
|
|
125
|
+
if (sunsetMatch)
|
|
126
|
+
sunset = sunsetMatch[1];
|
|
127
|
+
console.log('수집된 날씨 정보:', {
|
|
128
|
+
temperature,
|
|
129
|
+
description,
|
|
130
|
+
minTemperature,
|
|
131
|
+
maxTemperature,
|
|
132
|
+
feelsLike,
|
|
133
|
+
humidity,
|
|
134
|
+
windSpeed,
|
|
135
|
+
pm10,
|
|
136
|
+
pm25,
|
|
137
|
+
uvIndex,
|
|
138
|
+
sunset
|
|
139
|
+
});
|
|
140
|
+
// 온도가 0이거나 description만 있어도 반환 (0도도 유효한 온도)
|
|
141
|
+
// 최소한 하나의 정보라도 있으면 반환
|
|
142
|
+
const hasAnyData = temperature !== 0 || minTemperature || humidity || pm10;
|
|
143
|
+
if (description || hasAnyData) {
|
|
144
|
+
return {
|
|
145
|
+
description: description.trim() || '정보 없음',
|
|
146
|
+
temperature: temperature || minTemperature || 0,
|
|
147
|
+
minTemperature,
|
|
148
|
+
maxTemperature,
|
|
149
|
+
feelsLike,
|
|
150
|
+
humidity,
|
|
151
|
+
windSpeed,
|
|
152
|
+
pm10,
|
|
153
|
+
pm25,
|
|
154
|
+
uvIndex,
|
|
155
|
+
sunset
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error('네이버 날씨 스크래핑 실패:', error);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
formatWeatherForLogseq(weather) {
|
|
166
|
+
const lines = [];
|
|
167
|
+
// 날씨 상태
|
|
168
|
+
lines.push(weather.description);
|
|
169
|
+
// 온도 정보
|
|
170
|
+
if (weather.minTemperature !== undefined && weather.maxTemperature !== undefined) {
|
|
171
|
+
lines.push(`최저 기온 ${weather.minTemperature}도, 최고 기온 ${weather.maxTemperature}도`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
lines.push(`현재 기온 ${weather.temperature}도`);
|
|
175
|
+
}
|
|
176
|
+
// 체감온도
|
|
177
|
+
if (weather.feelsLike !== undefined) {
|
|
178
|
+
lines.push(`체감온도 ${weather.feelsLike}도`);
|
|
179
|
+
}
|
|
180
|
+
// 습도
|
|
181
|
+
if (weather.humidity !== undefined) {
|
|
182
|
+
lines.push(`습도 ${weather.humidity}%`);
|
|
183
|
+
}
|
|
184
|
+
// 바람
|
|
185
|
+
if (weather.windSpeed !== undefined) {
|
|
186
|
+
lines.push(`바람 ${weather.windSpeed}m/s`);
|
|
187
|
+
}
|
|
188
|
+
// 미세먼지
|
|
189
|
+
if (weather.pm10) {
|
|
190
|
+
lines.push(`미세먼지 ${weather.pm10}`);
|
|
191
|
+
}
|
|
192
|
+
// 초미세먼지
|
|
193
|
+
if (weather.pm25) {
|
|
194
|
+
lines.push(`초미세먼지 ${weather.pm25}`);
|
|
195
|
+
}
|
|
196
|
+
// 자외선
|
|
197
|
+
if (weather.uvIndex) {
|
|
198
|
+
lines.push(`자외선 ${weather.uvIndex}`);
|
|
199
|
+
}
|
|
200
|
+
return lines;
|
|
201
|
+
}
|
|
202
|
+
}
|
package/language.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "@iflow-mcp/dearcloud09-logseq-mcp", "version": "1.0.0", "description": "MCP server for Logseq graph integration", "type": "module", "main": "dist/index.js", "bin": {"iflow-mcp_dearcloud09-logseq-mcp": "dist/index.js"}, "scripts": {"build": "tsc", "start": "node dist/index.js", "dev": "tsc --watch"}, "keywords": ["mcp", "logseq", "ai"], "license": "SEE LICENSE IN LICENSE", "dependencies": {"@modelcontextprotocol/sdk": "^1.0.0", "axios": "^1.13.2", "cheerio": "^1.1.2", "zod": "^3.23.8"}, "devDependencies": {"@types/cheerio": "^0.22.35", "@types/node": "^20.10.0", "typescript": "^5.3.0"}}
|
package/package_name
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@iflow-mcp/dearcloud09-logseq-mcp
|
package/push_info.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Logseq Daily Journal Automation
|
|
4
|
+
# 매일 아침 6시에 실행되어 오늘의 저널 생성 + 날씨 추가
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
export LOGSEQ_GRAPH_PATH="${LOGSEQ_GRAPH_PATH:-/Users/hbk/Documents/logseq}"
|
|
10
|
+
export WEATHER_LOCATION="${WEATHER_LOCATION:-용인시 수지구 동천동}"
|
|
11
|
+
|
|
12
|
+
echo "[$(date -Iseconds)] Logseq Daily Automation 시작"
|
|
13
|
+
echo "Graph path: $LOGSEQ_GRAPH_PATH"
|
|
14
|
+
echo "Weather location: $WEATHER_LOCATION"
|
|
15
|
+
|
|
16
|
+
cd "$SCRIPT_DIR"
|
|
17
|
+
/opt/homebrew/bin/node add-today-dairy.js
|
|
18
|
+
|
|
19
|
+
echo "[$(date -Iseconds)] 완료"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 이 파일을 run-daily-automation.sh로 복사하고 경로를 수정하세요
|
|
3
|
+
|
|
4
|
+
cd "/path/to/logseq-mcp"
|
|
5
|
+
|
|
6
|
+
# 환경 설정
|
|
7
|
+
source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null
|
|
8
|
+
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:$PATH
|
|
9
|
+
export LOGSEQ_GRAPH_PATH=/path/to/your/logseq/graph
|
|
10
|
+
export WEATHER_LOCATION=서울
|
|
11
|
+
|
|
12
|
+
# 로그 파일
|
|
13
|
+
LOG_FILE="/path/to/logseq-mcp/automation.log"
|
|
14
|
+
|
|
15
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily automation 시작" >> "$LOG_FILE"
|
|
16
|
+
|
|
17
|
+
# 5분 타임아웃으로 실행
|
|
18
|
+
timeout 300 /opt/homebrew/bin/node add-today-dairy.js >> "$LOG_FILE" 2>&1
|
|
19
|
+
|
|
20
|
+
EXIT_CODE=$?
|
|
21
|
+
if [ $EXIT_CODE -eq 124 ]; then
|
|
22
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 타임아웃: 5분 초과로 강제 종료" >> "$LOG_FILE"
|
|
23
|
+
elif [ $EXIT_CODE -eq 0 ]; then
|
|
24
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily automation 정상 완료" >> "$LOG_FILE"
|
|
25
|
+
else
|
|
26
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily automation 실패 (exit code: $EXIT_CODE)" >> "$LOG_FILE"
|
|
27
|
+
fi
|