create-arete-workspace 0.2.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/README.md +77 -0
- package/bin/arete.js +156 -0
- package/bin/create.js +111 -0
- package/lib/install-openclaw.js +50 -0
- package/lib/scaffold.js +213 -0
- package/lib/setup-wizard.js +88 -0
- package/lib/updater.js +130 -0
- package/package.json +34 -0
- package/packages/gatsaeng-os/README.md +36 -0
- package/packages/gatsaeng-os/components.json +23 -0
- package/packages/gatsaeng-os/eslint.config.mjs +18 -0
- package/packages/gatsaeng-os/next.config.ts +7 -0
- package/packages/gatsaeng-os/package.json +59 -0
- package/packages/gatsaeng-os/postcss.config.mjs +7 -0
- package/packages/gatsaeng-os/public/file.svg +1 -0
- package/packages/gatsaeng-os/public/globe.svg +1 -0
- package/packages/gatsaeng-os/public/next.svg +1 -0
- package/packages/gatsaeng-os/public/vercel.svg +1 -0
- package/packages/gatsaeng-os/public/window.svg +1 -0
- package/packages/gatsaeng-os/python/api_server.py +248 -0
- package/packages/gatsaeng-os/python/briefing.py +145 -0
- package/packages/gatsaeng-os/python/config.py +55 -0
- package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
- package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
- package/packages/gatsaeng-os/python/proactive.py +158 -0
- package/packages/gatsaeng-os/python/requirements.txt +11 -0
- package/packages/gatsaeng-os/python/run.py +28 -0
- package/packages/gatsaeng-os/python/scoring.py +44 -0
- package/packages/gatsaeng-os/python/streak.py +70 -0
- package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
- package/packages/gatsaeng-os/python/timing_engine.py +117 -0
- package/packages/gatsaeng-os/python/vault_io.py +423 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
- package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
- package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
- package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
- package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
- package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
- package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
- package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
- package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
- package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
- package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
- package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
- package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
- package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
- package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
- package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
- package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
- package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
- package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
- package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
- package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
- package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
- package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
- package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
- package/packages/gatsaeng-os/src/app/error.tsx +30 -0
- package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
- package/packages/gatsaeng-os/src/app/globals.css +208 -0
- package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
- package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
- package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
- package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
- package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
- package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
- package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
- package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
- package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
- package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
- package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
- package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
- package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
- package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
- package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
- package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
- package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
- package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
- package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
- package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
- package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
- package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
- package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
- package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
- package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
- package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
- package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
- package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
- package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
- package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
- package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
- package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
- package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
- package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
- package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
- package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
- package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
- package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
- package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
- package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
- package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
- package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
- package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
- package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
- package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
- package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
- package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
- package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
- package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
- package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
- package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
- package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
- package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
- package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
- package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
- package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
- package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
- package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
- package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
- package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
- package/packages/gatsaeng-os/src/lib/date.ts +7 -0
- package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
- package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
- package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
- package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
- package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
- package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
- package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
- package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
- package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
- package/packages/gatsaeng-os/src/middleware.ts +34 -0
- package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
- package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
- package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
- package/packages/gatsaeng-os/src/types/index.ts +320 -0
- package/packages/gatsaeng-os/tsconfig.json +34 -0
- package/templates/scripts/forge_qa.sh.tmpl +237 -0
- package/templates/scripts/forge_ship.sh.tmpl +183 -0
- package/templates/scripts/session_indexer.py.tmpl +420 -0
- package/templates/scripts/tracer.py.tmpl +266 -0
- package/templates/workspace/AGENTS.md.tmpl +190 -0
- package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
- package/templates/workspace/MEMORY.md.tmpl +35 -0
- package/templates/workspace/SOUL.md.tmpl +258 -0
- package/templates/workspace/TOOLS.md.tmpl +28 -0
- package/templates/workspace/USER.md.tmpl +43 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""사주 Timing Engine — 사전 계산된 월운 데이터 기반 타이밍 컨텍스트 제공."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, timedelta
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import vault_io
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_current_context() -> Optional[dict]:
|
|
10
|
+
"""현재 월운 컨텍스트를 반환."""
|
|
11
|
+
return vault_io.get_current_timing()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_monthly_briefing() -> str:
|
|
15
|
+
"""월운 기반 모닝 브리핑 컨텍스트 생성."""
|
|
16
|
+
timing = get_current_context()
|
|
17
|
+
if not timing:
|
|
18
|
+
return ''
|
|
19
|
+
|
|
20
|
+
rating = timing.get('rating', 3)
|
|
21
|
+
parts = []
|
|
22
|
+
|
|
23
|
+
# Rating-based frame
|
|
24
|
+
if rating >= 4:
|
|
25
|
+
parts.append(f"이번 달({timing.get('pillar', '')})은 적극적으로 움직일 타이밍이야.")
|
|
26
|
+
elif rating == 3:
|
|
27
|
+
parts.append(f"이번 달({timing.get('pillar', '')})은 기존 루틴 유지에 집중.")
|
|
28
|
+
else:
|
|
29
|
+
parts.append(f"이번 달({timing.get('pillar', '')})은 신중하게. 무리한 확장은 보류.")
|
|
30
|
+
|
|
31
|
+
# Theme
|
|
32
|
+
if timing.get('theme'):
|
|
33
|
+
parts.append(f"테마: {timing['theme']}")
|
|
34
|
+
|
|
35
|
+
# Action guide
|
|
36
|
+
guides = timing.get('action_guide', [])
|
|
37
|
+
if guides:
|
|
38
|
+
parts.append('실행 가이드:')
|
|
39
|
+
for g in guides[:3]:
|
|
40
|
+
parts.append(f' - {g}')
|
|
41
|
+
|
|
42
|
+
# Caution
|
|
43
|
+
cautions = timing.get('caution', [])
|
|
44
|
+
if cautions:
|
|
45
|
+
parts.append('주의:')
|
|
46
|
+
for c in cautions[:2]:
|
|
47
|
+
parts.append(f' - {c}')
|
|
48
|
+
|
|
49
|
+
return '\n'.join(parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_timing_score_modifier(event_type: str) -> float:
|
|
53
|
+
"""운세에 따른 점수 보정 계수 (0.8 ~ 1.2)."""
|
|
54
|
+
timing = get_current_context()
|
|
55
|
+
if not timing:
|
|
56
|
+
return 1.0
|
|
57
|
+
|
|
58
|
+
rating = timing.get('rating', 3)
|
|
59
|
+
|
|
60
|
+
# 유리한 달에는 보너스, 불리한 달에는 감소 없음 (벌점은 주지 않음)
|
|
61
|
+
if rating >= 4:
|
|
62
|
+
return 1.1 # 10% 보너스
|
|
63
|
+
elif rating == 5:
|
|
64
|
+
return 1.2 # 20% 보너스
|
|
65
|
+
|
|
66
|
+
return 1.0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_goal_timing_advice(goal_id: str) -> str:
|
|
70
|
+
"""특정 목표에 대한 타이밍 기반 조언."""
|
|
71
|
+
timing = get_current_context()
|
|
72
|
+
goal = vault_io.read_goal(goal_id)
|
|
73
|
+
|
|
74
|
+
if not timing or not goal:
|
|
75
|
+
return ''
|
|
76
|
+
|
|
77
|
+
rating = timing.get('rating', 3)
|
|
78
|
+
theme = timing.get('theme', '')
|
|
79
|
+
goal_type = goal['data'].get('type', '')
|
|
80
|
+
goal_title = goal['data'].get('title', '')
|
|
81
|
+
|
|
82
|
+
advice = []
|
|
83
|
+
|
|
84
|
+
if rating >= 4:
|
|
85
|
+
advice.append(f"'{goal_title}' — 이 달은 확장에 유리. 마일스톤 달성 속도를 높여라.")
|
|
86
|
+
if 'networking' in theme.lower() or '인맥' in theme or '관계' in theme:
|
|
87
|
+
advice.append("특히 사람을 통한 기회가 열리는 시기. 협업 제안 적극적으로.")
|
|
88
|
+
elif rating <= 2:
|
|
89
|
+
advice.append(f"'{goal_title}' — 이 달은 기초 다지기에 집중. 새로운 시작보다 완성.")
|
|
90
|
+
advice.append("무리한 목표 수정보다 기존 루틴 유지가 더 효과적.")
|
|
91
|
+
else:
|
|
92
|
+
advice.append(f"'{goal_title}' — 균형 잡힌 시기. 현재 페이스 유지하면서 미세 조정.")
|
|
93
|
+
|
|
94
|
+
return '\n'.join(advice)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def should_trigger_reanalysis(goal_id: str) -> bool:
|
|
98
|
+
"""목표 재분석이 필요한지 판단."""
|
|
99
|
+
goal = vault_io.read_goal(goal_id)
|
|
100
|
+
if not goal:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
next_review = goal['data'].get('ai_next_review')
|
|
104
|
+
if not next_review:
|
|
105
|
+
return True # 한 번도 분석 안 함
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
review_date = date.fromisoformat(str(next_review)[:10])
|
|
109
|
+
return date.today() >= review_date
|
|
110
|
+
except ValueError:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_all_due_reanalyses() -> list[str]:
|
|
115
|
+
"""재분석이 필요한 모든 목표 ID 반환."""
|
|
116
|
+
goals = vault_io.get_active_goals()
|
|
117
|
+
return [g['id'] for g in goals if should_trigger_reanalysis(g['id'])]
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Obsidian Vault I/O — Python mirror of Node.js vault/index.ts"""
|
|
2
|
+
|
|
3
|
+
import frontmatter
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
import yaml
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from config import FOLDERS, PROFILE_PATH, VAULT_PATH
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ensure_dir(folder: str) -> Path:
|
|
14
|
+
path = FOLDERS[folder]
|
|
15
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
return path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _gen_id() -> str:
|
|
20
|
+
return uuid.uuid4().hex[:10]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Read Operations ──
|
|
24
|
+
|
|
25
|
+
def list_entities(folder: str) -> list[dict[str, Any]]:
|
|
26
|
+
"""List all .md files in a folder, parse frontmatter."""
|
|
27
|
+
path = _ensure_dir(folder)
|
|
28
|
+
results = []
|
|
29
|
+
for f in sorted(path.glob('*.md')):
|
|
30
|
+
post = frontmatter.load(str(f))
|
|
31
|
+
results.append({
|
|
32
|
+
'data': dict(post.metadata),
|
|
33
|
+
'content': post.content.strip(),
|
|
34
|
+
'filename': f.name,
|
|
35
|
+
})
|
|
36
|
+
return results
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_entity(folder: str, entity_id: str) -> Optional[dict[str, Any]]:
|
|
40
|
+
"""Get a single entity by ID (searches filename)."""
|
|
41
|
+
path = FOLDERS[folder]
|
|
42
|
+
if not path.exists():
|
|
43
|
+
return None
|
|
44
|
+
for f in path.glob('*.md'):
|
|
45
|
+
if entity_id in f.name:
|
|
46
|
+
post = frontmatter.load(str(f))
|
|
47
|
+
return {
|
|
48
|
+
'data': dict(post.metadata),
|
|
49
|
+
'content': post.content.strip(),
|
|
50
|
+
}
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_entity_by_date(folder: str, date_str: str) -> Optional[dict[str, Any]]:
|
|
55
|
+
"""Get a date-based entity (e.g., tasks/2026-03-05.md)."""
|
|
56
|
+
path = FOLDERS[folder] / f'{date_str}.md'
|
|
57
|
+
if not path.exists():
|
|
58
|
+
return None
|
|
59
|
+
post = frontmatter.load(str(path))
|
|
60
|
+
return {
|
|
61
|
+
'data': dict(post.metadata),
|
|
62
|
+
'content': post.content.strip(),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_today_tasks() -> dict[str, Any]:
|
|
67
|
+
"""Read today's daily manifest."""
|
|
68
|
+
today = date.today().isoformat()
|
|
69
|
+
result = get_entity_by_date('tasks', today)
|
|
70
|
+
if result:
|
|
71
|
+
return result
|
|
72
|
+
return {
|
|
73
|
+
'data': {
|
|
74
|
+
'date': today,
|
|
75
|
+
'gatsaeng_score': 0,
|
|
76
|
+
'routines_done': 0,
|
|
77
|
+
'routines_total': 0,
|
|
78
|
+
'focus_minutes': 0,
|
|
79
|
+
},
|
|
80
|
+
'content': '',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_goal(goal_id: str) -> Optional[dict[str, Any]]:
|
|
85
|
+
return get_entity('goals', goal_id)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_routines_with_status() -> list[dict[str, Any]]:
|
|
89
|
+
"""Get all active routines with today's completion status."""
|
|
90
|
+
routines = list_entities('routines')
|
|
91
|
+
today = date.today().isoformat()
|
|
92
|
+
today_log = get_entity_by_date('routine_logs', today)
|
|
93
|
+
|
|
94
|
+
completed_ids = set()
|
|
95
|
+
if today_log and 'completions' in today_log['data']:
|
|
96
|
+
for c in today_log['data']['completions']:
|
|
97
|
+
completed_ids.add(c.get('routine_id', ''))
|
|
98
|
+
|
|
99
|
+
result = []
|
|
100
|
+
for r in routines:
|
|
101
|
+
d = r['data']
|
|
102
|
+
if d.get('is_active', True):
|
|
103
|
+
d['completed_today'] = d.get('id', '') in completed_ids
|
|
104
|
+
result.append(d)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_active_goals() -> list[dict[str, Any]]:
|
|
109
|
+
"""Get all active goals."""
|
|
110
|
+
goals = list_entities('goals')
|
|
111
|
+
return [g['data'] for g in goals if g['data'].get('status') == 'active']
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_active_milestones() -> list[dict[str, Any]]:
|
|
115
|
+
"""Get all active milestones with D-day calculation."""
|
|
116
|
+
milestones = list_entities('milestones')
|
|
117
|
+
today = date.today()
|
|
118
|
+
result = []
|
|
119
|
+
for m in milestones:
|
|
120
|
+
d = m['data']
|
|
121
|
+
if d.get('status') == 'active' and d.get('due_date'):
|
|
122
|
+
due = date.fromisoformat(str(d['due_date'])[:10])
|
|
123
|
+
d['d_day'] = (due - today).days
|
|
124
|
+
result.append(d)
|
|
125
|
+
return sorted(result, key=lambda x: x.get('d_day', 999))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_goals_with_milestones() -> list[dict[str, Any]]:
|
|
129
|
+
"""Get goals with their milestones and D-day info."""
|
|
130
|
+
goals = get_active_goals()
|
|
131
|
+
milestones = get_active_milestones()
|
|
132
|
+
|
|
133
|
+
for goal in goals:
|
|
134
|
+
goal_id = goal.get('id', '')
|
|
135
|
+
goal['milestones'] = [m for m in milestones if m.get('goal_id') == goal_id]
|
|
136
|
+
return goals
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_current_timing() -> Optional[dict[str, Any]]:
|
|
140
|
+
"""Get the current month's timing context."""
|
|
141
|
+
today = date.today()
|
|
142
|
+
timings = list_entities('timing')
|
|
143
|
+
for t in timings:
|
|
144
|
+
d = t['data']
|
|
145
|
+
start = d.get('period_start', '')
|
|
146
|
+
end = d.get('period_end', '')
|
|
147
|
+
if start and end:
|
|
148
|
+
start_date = date.fromisoformat(str(start)[:10])
|
|
149
|
+
end_date = date.fromisoformat(str(end)[:10])
|
|
150
|
+
if start_date <= today <= end_date:
|
|
151
|
+
return d
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_active_streaks() -> dict[str, Any]:
|
|
156
|
+
"""Get current and longest streaks from profile."""
|
|
157
|
+
profile = read_profile()
|
|
158
|
+
return {
|
|
159
|
+
'current': profile.get('current_streak', 0),
|
|
160
|
+
'longest': profile.get('longest_streak', 0),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_today_score() -> int:
|
|
165
|
+
"""Get today's gatsaeng score."""
|
|
166
|
+
today_data = read_today_tasks()
|
|
167
|
+
return today_data['data'].get('gatsaeng_score', 0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_yesterday_score() -> int:
|
|
171
|
+
"""Get yesterday's gatsaeng score."""
|
|
172
|
+
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
|
173
|
+
result = get_entity_by_date('tasks', yesterday)
|
|
174
|
+
if result:
|
|
175
|
+
return result['data'].get('gatsaeng_score', 0)
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_unchecked_today() -> list[dict[str, Any]]:
|
|
180
|
+
"""Get routines and tasks not yet completed today."""
|
|
181
|
+
routines = get_routines_with_status()
|
|
182
|
+
unchecked = [r for r in routines if not r.get('completed_today', False)]
|
|
183
|
+
return unchecked
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_consecutive_skips() -> dict[str, int]:
|
|
187
|
+
"""Check consecutive days routines were skipped."""
|
|
188
|
+
routines = list_entities('routines')
|
|
189
|
+
active_routines = {r['data']['id']: r['data'] for r in routines if r['data'].get('is_active')}
|
|
190
|
+
|
|
191
|
+
skips: dict[str, int] = {rid: 0 for rid in active_routines}
|
|
192
|
+
today = date.today()
|
|
193
|
+
|
|
194
|
+
for days_ago in range(1, 8): # check last 7 days
|
|
195
|
+
check_date = (today - timedelta(days=days_ago)).isoformat()
|
|
196
|
+
log = get_entity_by_date('routine_logs', check_date)
|
|
197
|
+
completed_ids = set()
|
|
198
|
+
if log and 'completions' in log['data']:
|
|
199
|
+
for c in log['data']['completions']:
|
|
200
|
+
completed_ids.add(c.get('routine_id', ''))
|
|
201
|
+
|
|
202
|
+
for rid in active_routines:
|
|
203
|
+
r = active_routines[rid]
|
|
204
|
+
day_of_week = (today - timedelta(days=days_ago)).isoweekday()
|
|
205
|
+
scheduled = r.get('scheduled_days', [1,2,3,4,5,6,7])
|
|
206
|
+
if day_of_week in scheduled:
|
|
207
|
+
if rid not in completed_ids:
|
|
208
|
+
skips[rid] += 1
|
|
209
|
+
else:
|
|
210
|
+
break # streak broken, stop counting
|
|
211
|
+
|
|
212
|
+
return {rid: count for rid, count in skips.items() if count > 0}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_upcoming_ddays(days: int = 90) -> list[dict[str, Any]]:
|
|
216
|
+
"""Get milestones with D-day within range."""
|
|
217
|
+
milestones = get_active_milestones()
|
|
218
|
+
return [m for m in milestones if 0 < m.get('d_day', 999) <= days]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def read_profile() -> dict[str, Any]:
|
|
222
|
+
"""Read user profile."""
|
|
223
|
+
if not PROFILE_PATH.exists():
|
|
224
|
+
return {
|
|
225
|
+
'display_name': 'Drake',
|
|
226
|
+
'level': 1,
|
|
227
|
+
'total_score': 0,
|
|
228
|
+
'longest_streak': 0,
|
|
229
|
+
'current_streak': 0,
|
|
230
|
+
'peak_hours': [9, 10, 11],
|
|
231
|
+
}
|
|
232
|
+
post = frontmatter.load(str(PROFILE_PATH))
|
|
233
|
+
return dict(post.metadata)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def read_last_review() -> Optional[dict[str, Any]]:
|
|
237
|
+
"""Read the most recent weekly review."""
|
|
238
|
+
reviews = list_entities('reviews')
|
|
239
|
+
weekly = [r for r in reviews if 'weekly' in r['filename'] or r['data'].get('type') == 'weekly']
|
|
240
|
+
if not weekly:
|
|
241
|
+
return None
|
|
242
|
+
weekly.sort(key=lambda x: x['data'].get('week_start', ''), reverse=True)
|
|
243
|
+
return weekly[0]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def read_context_data(goal_id: str) -> list[dict[str, Any]]:
|
|
247
|
+
"""Read context-data files for a goal."""
|
|
248
|
+
context_dir = FOLDERS['goals'] / goal_id / 'context-data'
|
|
249
|
+
if not context_dir.exists():
|
|
250
|
+
return []
|
|
251
|
+
files = []
|
|
252
|
+
for f in context_dir.iterdir():
|
|
253
|
+
files.append({
|
|
254
|
+
'name': f.name,
|
|
255
|
+
'path': str(f),
|
|
256
|
+
'size': f.stat().st_size,
|
|
257
|
+
'suffix': f.suffix,
|
|
258
|
+
})
|
|
259
|
+
return files
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def read_recent_tasks(days: int = 14) -> list[dict[str, Any]]:
|
|
263
|
+
"""Read task manifests for recent days."""
|
|
264
|
+
today = date.today()
|
|
265
|
+
results = []
|
|
266
|
+
for i in range(days):
|
|
267
|
+
d = (today - timedelta(days=i)).isoformat()
|
|
268
|
+
result = get_entity_by_date('tasks', d)
|
|
269
|
+
if result:
|
|
270
|
+
results.append(result)
|
|
271
|
+
return results
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ── Write Operations ──
|
|
275
|
+
|
|
276
|
+
def create_entity(folder: str, data: dict[str, Any], body: str = '') -> dict[str, Any]:
|
|
277
|
+
"""Create a new entity file."""
|
|
278
|
+
path = _ensure_dir(folder)
|
|
279
|
+
entity_id = data.get('id') or _gen_id()
|
|
280
|
+
data['id'] = entity_id
|
|
281
|
+
|
|
282
|
+
prefix = folder.rstrip('s')
|
|
283
|
+
filename = f'{prefix}-{entity_id}.md'
|
|
284
|
+
|
|
285
|
+
post = frontmatter.Post(body, **data)
|
|
286
|
+
filepath = path / filename
|
|
287
|
+
filepath.write_text(frontmatter.dumps(post), encoding='utf-8')
|
|
288
|
+
|
|
289
|
+
return data
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def create_date_entity(folder: str, date_str: str, data: dict[str, Any], body: str = '') -> dict[str, Any]:
|
|
293
|
+
"""Create a date-based entity file."""
|
|
294
|
+
path = _ensure_dir(folder)
|
|
295
|
+
post = frontmatter.Post(body, **data)
|
|
296
|
+
filepath = path / f'{date_str}.md'
|
|
297
|
+
filepath.write_text(frontmatter.dumps(post), encoding='utf-8')
|
|
298
|
+
return data
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def update_entity(folder: str, entity_id: str, updates: dict[str, Any], body: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
302
|
+
"""Update an entity's frontmatter."""
|
|
303
|
+
path = FOLDERS[folder]
|
|
304
|
+
for f in path.glob('*.md'):
|
|
305
|
+
if entity_id in f.name:
|
|
306
|
+
post = frontmatter.load(str(f))
|
|
307
|
+
for k, v in updates.items():
|
|
308
|
+
post.metadata[k] = v
|
|
309
|
+
if body is not None:
|
|
310
|
+
post.content = body
|
|
311
|
+
f.write_text(frontmatter.dumps(post), encoding='utf-8')
|
|
312
|
+
return dict(post.metadata)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def check_routine(routine_id: str) -> dict[str, Any]:
|
|
317
|
+
"""Mark a routine as completed today."""
|
|
318
|
+
today = date.today().isoformat()
|
|
319
|
+
now = datetime.now().isoformat()
|
|
320
|
+
|
|
321
|
+
log = get_entity_by_date('routine_logs', today)
|
|
322
|
+
|
|
323
|
+
if log:
|
|
324
|
+
completions = log['data'].get('completions', [])
|
|
325
|
+
# check if already completed
|
|
326
|
+
if any(c.get('routine_id') == routine_id for c in completions):
|
|
327
|
+
return log['data']
|
|
328
|
+
completions.append({
|
|
329
|
+
'routine_id': routine_id,
|
|
330
|
+
'completed_at': now,
|
|
331
|
+
})
|
|
332
|
+
log['data']['completions'] = completions
|
|
333
|
+
create_date_entity('routine_logs', today, log['data'])
|
|
334
|
+
else:
|
|
335
|
+
data = {
|
|
336
|
+
'date': today,
|
|
337
|
+
'completions': [{
|
|
338
|
+
'routine_id': routine_id,
|
|
339
|
+
'completed_at': now,
|
|
340
|
+
}],
|
|
341
|
+
}
|
|
342
|
+
create_date_entity('routine_logs', today, data)
|
|
343
|
+
log = {'data': data}
|
|
344
|
+
|
|
345
|
+
# update streak
|
|
346
|
+
routine = get_entity('routines', routine_id)
|
|
347
|
+
if routine:
|
|
348
|
+
streak = routine['data'].get('streak', 0) + 1
|
|
349
|
+
longest = max(routine['data'].get('longest_streak', 0), streak)
|
|
350
|
+
update_entity('routines', routine_id, {
|
|
351
|
+
'streak': streak,
|
|
352
|
+
'longest_streak': longest,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
return log['data']
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def update_today_score(delta: int) -> int:
|
|
359
|
+
"""Add points to today's score."""
|
|
360
|
+
today = date.today().isoformat()
|
|
361
|
+
manifest = read_today_tasks()
|
|
362
|
+
new_score = manifest['data'].get('gatsaeng_score', 0) + delta
|
|
363
|
+
manifest['data']['gatsaeng_score'] = new_score
|
|
364
|
+
create_date_entity('tasks', today, manifest['data'], manifest.get('content', ''))
|
|
365
|
+
|
|
366
|
+
# update profile total
|
|
367
|
+
profile = read_profile()
|
|
368
|
+
profile['total_score'] = profile.get('total_score', 0) + delta
|
|
369
|
+
update_profile(profile)
|
|
370
|
+
|
|
371
|
+
return new_score
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def update_profile(updates: dict[str, Any]) -> dict[str, Any]:
|
|
375
|
+
"""Update user profile."""
|
|
376
|
+
if PROFILE_PATH.exists():
|
|
377
|
+
post = frontmatter.load(str(PROFILE_PATH))
|
|
378
|
+
for k, v in updates.items():
|
|
379
|
+
post.metadata[k] = v
|
|
380
|
+
PROFILE_PATH.write_text(frontmatter.dumps(post), encoding='utf-8')
|
|
381
|
+
return dict(post.metadata)
|
|
382
|
+
else:
|
|
383
|
+
post = frontmatter.Post('', **updates)
|
|
384
|
+
PROFILE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
PROFILE_PATH.write_text(frontmatter.dumps(post), encoding='utf-8')
|
|
386
|
+
return updates
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def save_context_file(goal_id: str, filename: str, content: bytes) -> Path:
|
|
390
|
+
"""Save a context-data file for a goal."""
|
|
391
|
+
context_dir = FOLDERS['goals'] / goal_id / 'context-data'
|
|
392
|
+
context_dir.mkdir(parents=True, exist_ok=True)
|
|
393
|
+
filepath = context_dir / filename
|
|
394
|
+
filepath.write_bytes(content)
|
|
395
|
+
return filepath
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_heatmap(weeks: int = 52) -> list[dict[str, Any]]:
|
|
399
|
+
"""Get completion data for heatmap (last N weeks)."""
|
|
400
|
+
today = date.today()
|
|
401
|
+
data = []
|
|
402
|
+
for i in range(weeks * 7):
|
|
403
|
+
d = today - timedelta(days=i)
|
|
404
|
+
d_str = d.isoformat()
|
|
405
|
+
log = get_entity_by_date('routine_logs', d_str)
|
|
406
|
+
manifest = get_entity_by_date('tasks', d_str)
|
|
407
|
+
|
|
408
|
+
completions = 0
|
|
409
|
+
if log and 'completions' in log['data']:
|
|
410
|
+
completions = len(log['data']['completions'])
|
|
411
|
+
|
|
412
|
+
score = 0
|
|
413
|
+
if manifest:
|
|
414
|
+
score = manifest['data'].get('gatsaeng_score', 0)
|
|
415
|
+
|
|
416
|
+
data.append({
|
|
417
|
+
'date': d_str,
|
|
418
|
+
'completions': completions,
|
|
419
|
+
'score': score,
|
|
420
|
+
'day_of_week': d.isoweekday(),
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
return list(reversed(data))
|