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.
Files changed (180) hide show
  1. package/README.md +77 -0
  2. package/bin/arete.js +156 -0
  3. package/bin/create.js +111 -0
  4. package/lib/install-openclaw.js +50 -0
  5. package/lib/scaffold.js +213 -0
  6. package/lib/setup-wizard.js +88 -0
  7. package/lib/updater.js +130 -0
  8. package/package.json +34 -0
  9. package/packages/gatsaeng-os/README.md +36 -0
  10. package/packages/gatsaeng-os/components.json +23 -0
  11. package/packages/gatsaeng-os/eslint.config.mjs +18 -0
  12. package/packages/gatsaeng-os/next.config.ts +7 -0
  13. package/packages/gatsaeng-os/package.json +59 -0
  14. package/packages/gatsaeng-os/postcss.config.mjs +7 -0
  15. package/packages/gatsaeng-os/public/file.svg +1 -0
  16. package/packages/gatsaeng-os/public/globe.svg +1 -0
  17. package/packages/gatsaeng-os/public/next.svg +1 -0
  18. package/packages/gatsaeng-os/public/vercel.svg +1 -0
  19. package/packages/gatsaeng-os/public/window.svg +1 -0
  20. package/packages/gatsaeng-os/python/api_server.py +248 -0
  21. package/packages/gatsaeng-os/python/briefing.py +145 -0
  22. package/packages/gatsaeng-os/python/config.py +55 -0
  23. package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
  24. package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
  25. package/packages/gatsaeng-os/python/proactive.py +158 -0
  26. package/packages/gatsaeng-os/python/requirements.txt +11 -0
  27. package/packages/gatsaeng-os/python/run.py +28 -0
  28. package/packages/gatsaeng-os/python/scoring.py +44 -0
  29. package/packages/gatsaeng-os/python/streak.py +70 -0
  30. package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
  31. package/packages/gatsaeng-os/python/timing_engine.py +117 -0
  32. package/packages/gatsaeng-os/python/vault_io.py +423 -0
  33. package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
  34. package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
  35. package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
  36. package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
  37. package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
  38. package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
  39. package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
  40. package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
  41. package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
  42. package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
  43. package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
  44. package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
  45. package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
  46. package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
  47. package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
  48. package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
  49. package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
  50. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
  51. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
  52. package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
  53. package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
  54. package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
  55. package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
  56. package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
  57. package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
  58. package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
  59. package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
  60. package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
  61. package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
  62. package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
  63. package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
  64. package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
  65. package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
  66. package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
  67. package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
  68. package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
  69. package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
  70. package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
  71. package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
  72. package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
  73. package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
  74. package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
  75. package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
  76. package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
  77. package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
  78. package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
  79. package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
  80. package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
  81. package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
  82. package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
  83. package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
  84. package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
  85. package/packages/gatsaeng-os/src/app/error.tsx +30 -0
  86. package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
  87. package/packages/gatsaeng-os/src/app/globals.css +208 -0
  88. package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
  89. package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
  90. package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
  91. package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
  92. package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
  93. package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
  94. package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
  95. package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
  96. package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
  97. package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
  98. package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
  99. package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
  100. package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
  101. package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
  102. package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
  103. package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
  104. package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
  105. package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
  106. package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
  107. package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
  108. package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
  109. package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
  110. package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
  111. package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
  112. package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
  113. package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
  114. package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
  115. package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
  116. package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
  117. package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
  118. package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
  119. package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
  120. package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
  121. package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
  122. package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
  123. package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
  124. package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
  125. package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
  126. package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
  127. package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
  128. package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
  129. package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
  130. package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
  131. package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
  132. package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
  133. package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
  134. package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
  135. package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
  136. package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
  137. package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
  138. package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
  139. package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
  140. package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
  141. package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
  142. package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
  143. package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
  144. package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
  145. package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
  146. package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
  147. package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
  148. package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
  149. package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
  150. package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
  151. package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
  152. package/packages/gatsaeng-os/src/lib/date.ts +7 -0
  153. package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
  154. package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
  155. package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
  156. package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
  157. package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
  158. package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
  159. package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
  160. package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
  161. package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
  162. package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
  163. package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
  164. package/packages/gatsaeng-os/src/middleware.ts +34 -0
  165. package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
  166. package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
  167. package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
  168. package/packages/gatsaeng-os/src/types/index.ts +320 -0
  169. package/packages/gatsaeng-os/tsconfig.json +34 -0
  170. package/templates/scripts/forge_qa.sh.tmpl +237 -0
  171. package/templates/scripts/forge_ship.sh.tmpl +183 -0
  172. package/templates/scripts/session_indexer.py.tmpl +420 -0
  173. package/templates/scripts/tracer.py.tmpl +266 -0
  174. package/templates/workspace/AGENTS.md.tmpl +190 -0
  175. package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
  176. package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
  177. package/templates/workspace/MEMORY.md.tmpl +35 -0
  178. package/templates/workspace/SOUL.md.tmpl +258 -0
  179. package/templates/workspace/TOOLS.md.tmpl +28 -0
  180. package/templates/workspace/USER.md.tmpl +43 -0
@@ -0,0 +1,158 @@
1
+ """Proactive Engine β€” νŒ¨ν„΄ 감지 + 사전 μ•Œλ¦Ό μ‹œμŠ€ν…œ."""
2
+
3
+ from datetime import date, timedelta, datetime
4
+ from typing import Optional
5
+
6
+ import vault_io
7
+ import timing_engine
8
+
9
+
10
+ def check_all() -> list[dict]:
11
+ """λͺ¨λ“  proactive 체크λ₯Ό μ‹€ν–‰ν•˜κ³  μ•Œλ¦Ό λͺ©λ‘ λ°˜ν™˜."""
12
+ alerts = []
13
+ alerts.extend(check_skipped_routines())
14
+ alerts.extend(check_dday_warnings())
15
+ alerts.extend(check_streak_danger())
16
+ alerts.extend(check_reanalysis_due())
17
+ alerts.extend(check_energy_pattern())
18
+ return alerts
19
+
20
+
21
+ def check_skipped_routines() -> list[dict]:
22
+ """2일 이상 연속 μŠ€ν‚΅λœ 루틴 감지."""
23
+ alerts = []
24
+ skips = vault_io.get_consecutive_skips()
25
+
26
+ for rid, count in skips.items():
27
+ if count >= 2:
28
+ routine = vault_io.get_entity('routines', rid)
29
+ if not routine:
30
+ continue
31
+ title = routine['data'].get('title', rid)
32
+
33
+ if count >= 4:
34
+ severity = 'critical'
35
+ msg = f'"{title}" {count}일 연속 μŠ€ν‚΅. μŠ΅κ΄€ 체인 끊길 μœ„ν—˜. 루틴 μž¬μ„€κ³„ κ²€ν†  ν•„μš”.'
36
+ elif count >= 2:
37
+ severity = 'warning'
38
+ msg = f'"{title}" {count}일 연속 μŠ€ν‚΅. 였늘 λ°˜λ“œμ‹œ μ‹€ν–‰ν•˜λΌ.'
39
+
40
+ alerts.append({
41
+ 'type': 'skipped_routine',
42
+ 'severity': severity,
43
+ 'title': f'{title} β€” {count}일 연속 λ―Έμ‹€ν–‰',
44
+ 'message': msg,
45
+ 'routine_id': rid,
46
+ })
47
+
48
+ return alerts
49
+
50
+
51
+ def check_dday_warnings() -> list[dict]:
52
+ """λ§ˆμΌμŠ€ν†€ D-day κ²½κ³ ."""
53
+ alerts = []
54
+ milestones = vault_io.get_active_milestones()
55
+
56
+ for m in milestones:
57
+ d = m.get('d_day', 999)
58
+ title = m.get('title', '?')
59
+ progress = 0
60
+ target = m.get('target_value', 0)
61
+ if target > 0:
62
+ progress = round(m.get('current_value', 0) / target * 100)
63
+
64
+ if d <= 0:
65
+ alerts.append({
66
+ 'type': 'deadline',
67
+ 'severity': 'critical',
68
+ 'title': f'{title} β€” D-Day 초과!',
69
+ 'message': f'마감 {abs(d)}일 초과. μ§„ν–‰λ₯  {progress}%. μ¦‰μ‹œ μ™„λ£Œν•˜κ±°λ‚˜ 마감 μž¬μ‘°μ •.',
70
+ 'milestone_id': m.get('id'),
71
+ })
72
+ elif d <= 3:
73
+ alerts.append({
74
+ 'type': 'milestone_dday',
75
+ 'severity': 'critical',
76
+ 'title': f'{title} β€” D-{d}',
77
+ 'message': f'μ§„ν–‰λ₯  {progress}%. 남은 {m.get("target_value", 0) - m.get("current_value", 0)} {m.get("unit", "")} 집쀑.',
78
+ 'milestone_id': m.get('id'),
79
+ })
80
+ elif d <= 7:
81
+ alerts.append({
82
+ 'type': 'milestone_dday',
83
+ 'severity': 'warning',
84
+ 'title': f'{title} β€” D-{d}',
85
+ 'message': f'μ§„ν–‰λ₯  {progress}%. 마감 {d}일 μ „.',
86
+ 'milestone_id': m.get('id'),
87
+ })
88
+
89
+ return alerts
90
+
91
+
92
+ def check_streak_danger() -> list[dict]:
93
+ """슀트릭 μœ„ν—˜ 감지 β€” μ˜€ν›„ 9μ‹œ 이후 λ―Έμ™„λ£Œ 루틴 쑴재 μ‹œ."""
94
+ now = datetime.now()
95
+ if now.hour < 21:
96
+ return []
97
+
98
+ unchecked = vault_io.get_unchecked_today()
99
+ if not unchecked:
100
+ return []
101
+
102
+ streaks = vault_io.get_active_streaks()
103
+ current = streaks.get('current', 0)
104
+
105
+ if current >= 3:
106
+ return [{
107
+ 'type': 'streak_danger',
108
+ 'severity': 'warning',
109
+ 'title': f'{current}일 슀트릭 끊길 μœ„ν—˜!',
110
+ 'message': f'{len(unchecked)}개 루틴 λ―Έμ™„λ£Œ. 자기 전에 μ²˜λ¦¬ν•˜λΌ.',
111
+ }]
112
+
113
+ return []
114
+
115
+
116
+ def check_reanalysis_due() -> list[dict]:
117
+ """μž¬λΆ„μ„ ν•„μš”ν•œ λͺ©ν‘œ 감지."""
118
+ alerts = []
119
+ due = timing_engine.get_all_due_reanalyses()
120
+
121
+ for gid in due:
122
+ goal = vault_io.read_goal(gid)
123
+ if not goal:
124
+ continue
125
+ alerts.append({
126
+ 'type': 'reanalysis_due',
127
+ 'severity': 'info',
128
+ 'title': f'"{goal["data"].get("title", "?")}" μž¬λΆ„μ„ ν•„μš”',
129
+ 'message': '2μ£Ό κ²½κ³Ό. Goal Context Agent μž¬μ‹€ν–‰ μΆ”μ²œ.',
130
+ 'goal_id': gid,
131
+ })
132
+
133
+ return alerts
134
+
135
+
136
+ def check_energy_pattern() -> list[dict]:
137
+ """μ—λ„ˆμ§€ νŒ¨ν„΄ 감지 β€” 졜근 3일 연속 low energy."""
138
+ alerts = []
139
+ today = date.today()
140
+
141
+ low_days = 0
142
+ for i in range(3):
143
+ d = (today - timedelta(days=i)).isoformat()
144
+ log = vault_io.get_entity_by_date('energy_logs', d)
145
+ if log and 'entries' in log['data']:
146
+ avg = sum(e.get('level', 3) for e in log['data']['entries']) / max(len(log['data']['entries']), 1)
147
+ if avg <= 2:
148
+ low_days += 1
149
+
150
+ if low_days >= 3:
151
+ alerts.append({
152
+ 'type': 'energy_warning',
153
+ 'severity': 'warning',
154
+ 'title': '3일 연속 μ €μ—λ„ˆμ§€ 감지',
155
+ 'message': '회볡 μš°μ„ . 루틴을 절반으둜 쀄이고 수면/μš΄λ™λΆ€ν„°.',
156
+ })
157
+
158
+ return alerts
@@ -0,0 +1,11 @@
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.0
3
+ python-frontmatter==1.1.0
4
+ pyyaml==6.0.2
5
+ anthropic==0.49.0
6
+ python-telegram-bot==21.5
7
+ apscheduler==3.10.4
8
+ python-dotenv==1.0.1
9
+ python-multipart==0.0.9
10
+ aiofiles==24.1.0
11
+ pytz==2024.2
@@ -0,0 +1,28 @@
1
+ """Entry point for the gatsaeng-os Python backend (API server only).
2
+
3
+ Telegram은 OpenClaw gatewayκ°€ 처리 β€” 이 μ„œλ²„λŠ” FastAPI만 μ‹€ν–‰.
4
+ """
5
+
6
+ import logging
7
+ import sys
8
+
9
+ import uvicorn
10
+ from config import DASHBOARD_PORT
11
+
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s [%(name)s] %(levelname)s: %(message)s',
15
+ )
16
+ logger = logging.getLogger('gatsaeng')
17
+
18
+
19
+ if __name__ == '__main__':
20
+ host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
21
+ logger.info(f'Starting Gatsaeng API server on {host}:{DASHBOARD_PORT}')
22
+ uvicorn.run(
23
+ 'api_server:app',
24
+ host=host,
25
+ port=DASHBOARD_PORT,
26
+ reload='--reload' in sys.argv,
27
+ log_level='info',
28
+ )
@@ -0,0 +1,44 @@
1
+ """Gatsaeng scoring system with variable rewards (Skinner)."""
2
+
3
+ import random
4
+
5
+ BASE_SCORES = {
6
+ 'routine_complete': 10,
7
+ 'task_done': 5,
8
+ 'task_done_urgent': 15,
9
+ 'milestone_complete': 100,
10
+ 'review_written': 20,
11
+ 'data_uploaded': 10,
12
+ 'goal_25pct': 30,
13
+ 'goal_50pct': 60,
14
+ 'goal_100pct': 200,
15
+ }
16
+
17
+
18
+ def calculate(event: str, streak: int = 0) -> dict:
19
+ """Calculate score with streak multiplier and variable bonus."""
20
+ base = BASE_SCORES.get(event, 0)
21
+
22
+ # streak multiplier β€” 동일 λ‘œμ§μ„ streak.get_streak_multiplier()와 곡유
23
+ if streak >= 30:
24
+ multiplier = 5.0
25
+ elif streak >= 21:
26
+ multiplier = 4.0
27
+ elif streak >= 14:
28
+ multiplier = 3.0
29
+ elif streak >= 7:
30
+ multiplier = 2.0
31
+ elif streak >= 3:
32
+ multiplier = 1.5
33
+ else:
34
+ multiplier = 1.0
35
+ points = round(base * multiplier)
36
+
37
+ # 15% variable bonus (Skinner)
38
+ bonus_message = None
39
+ if random.random() < 0.15:
40
+ bonus = random.choice([5, 10, 15, 25])
41
+ points += bonus
42
+ bonus_message = f'πŸŽ‰ λ³΄λ„ˆμŠ€ +{bonus}점!'
43
+
44
+ return {'points': points, 'bonus_message': bonus_message}
@@ -0,0 +1,70 @@
1
+ """Streak Engine β€” 연속 싀행일 계산 + 보정."""
2
+
3
+ from datetime import date, timedelta
4
+
5
+ import vault_io
6
+
7
+
8
+ def calculate_streak() -> dict:
9
+ """ν˜„μž¬ μŠ€νŠΈλ¦­μ„ μž¬κ³„μ‚°ν•˜κ³  ν”„λ‘œν•„μ— 반영."""
10
+ today = date.today()
11
+ streak = 0
12
+
13
+ all_routines = vault_io.list_entities('routines')
14
+ active = [r for r in all_routines if r['data'].get('is_active')]
15
+
16
+ for days_ago in range(1, 365):
17
+ check_date = today - timedelta(days=days_ago)
18
+ d_str = check_date.isoformat()
19
+
20
+ log = vault_io.get_entity_by_date('routine_logs', d_str)
21
+ if not log or not log['data'].get('completions'):
22
+ break
23
+ day_of_week = check_date.isoweekday()
24
+ scheduled = [
25
+ r for r in active
26
+ if day_of_week in r['data'].get('scheduled_days', [1,2,3,4,5,6,7])
27
+ ]
28
+
29
+ if not scheduled:
30
+ continue # μŠ€μΌ€μ€„ μ—†λŠ” 날은 κ±΄λ„ˆλœ€
31
+
32
+ completed_ids = {c.get('routine_id', '') for c in log['data'].get('completions', [])}
33
+ scheduled_ids = {r['data'].get('id', '') for r in scheduled}
34
+
35
+ # 50% 이상 μ™„λ£Œ μ‹œ 슀트릭 μœ μ§€
36
+ if len(completed_ids & scheduled_ids) >= len(scheduled_ids) * 0.5:
37
+ streak += 1
38
+ else:
39
+ break
40
+
41
+ # Update profile
42
+ profile = vault_io.read_profile()
43
+ old_longest = profile.get('longest_streak', 0)
44
+ new_longest = max(old_longest, streak)
45
+
46
+ vault_io.update_profile({
47
+ 'current_streak': streak,
48
+ 'longest_streak': new_longest,
49
+ })
50
+
51
+ return {
52
+ 'current': streak,
53
+ 'longest': new_longest,
54
+ 'changed': streak != profile.get('current_streak', 0),
55
+ }
56
+
57
+
58
+ def get_streak_multiplier(streak: int) -> float:
59
+ """μŠ€νŠΈλ¦­μ— λ”°λ₯Έ 점수 배율 (μ΅œλŒ€ 5x)."""
60
+ if streak >= 30:
61
+ return 5.0
62
+ elif streak >= 21:
63
+ return 4.0
64
+ elif streak >= 14:
65
+ return 3.0
66
+ elif streak >= 7:
67
+ return 2.0
68
+ elif streak >= 3:
69
+ return 1.5
70
+ return 1.0
@@ -0,0 +1,331 @@
1
+ """Telegram Bot β€” Coach_Eve_bot 톡합 갓생 μ½”μΉ˜."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from telegram import Update
6
+ from telegram.ext import (
7
+ Application,
8
+ CommandHandler,
9
+ MessageHandler,
10
+ filters,
11
+ ContextTypes,
12
+ )
13
+
14
+ import vault_io
15
+ import briefing
16
+ import proactive
17
+ import gyeokguk
18
+ import timing_engine
19
+ import goal_context_agent
20
+ import streak
21
+ from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def is_authorized(update: Update) -> bool:
27
+ """Only allow Drake (chat_id from config)."""
28
+ if not TELEGRAM_CHAT_ID:
29
+ return True
30
+ return str(update.effective_chat.id) == TELEGRAM_CHAT_ID.replace('tg:', '')
31
+
32
+
33
+ # ─── Command Handlers ───
34
+
35
+ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
36
+ if not is_authorized(update):
37
+ return
38
+ await update.message.reply_text(
39
+ "🧭 갓생 μ½”μΉ˜ ν™œμ„±ν™”.\n"
40
+ "/morning β€” λͺ¨λ‹ λΈŒλ¦¬ν•‘\n"
41
+ "/check β€” 루틴 체크인\n"
42
+ "/score β€” 였늘 μŠ€μ½”μ–΄\n"
43
+ "/dday β€” D-day ν˜„ν™©\n"
44
+ "/timing β€” 이달 운기\n"
45
+ "/gyeokguk β€” 격ꡭ νŒμ •\n"
46
+ "/scorecard β€” μ„±μž₯ μŠ€μ½”μ–΄μΉ΄λ“œ\n"
47
+ "/analyze β€” λͺ©ν‘œ AI 뢄석"
48
+ )
49
+
50
+
51
+ async def cmd_morning(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
52
+ if not is_authorized(update):
53
+ return
54
+ text = briefing.generate_morning_briefing()
55
+ await update.message.reply_text(text, parse_mode=None)
56
+
57
+
58
+ async def cmd_evening(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
59
+ if not is_authorized(update):
60
+ return
61
+ text = briefing.generate_evening_checkin()
62
+ await update.message.reply_text(text, parse_mode=None)
63
+
64
+
65
+ async def cmd_check(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
66
+ """ν˜„μž¬ 루틴 μƒνƒœ ν‘œμ‹œ."""
67
+ if not is_authorized(update):
68
+ return
69
+ routines = vault_io.get_routines_with_status()
70
+ if not routines:
71
+ await update.message.reply_text("ν™œμ„± 루틴 μ—†μŒ.")
72
+ return
73
+
74
+ lines = ["였늘 루틴 ν˜„ν™©:"]
75
+ done = 0
76
+ for r in routines:
77
+ status = 'βœ…' if r.get('completed_today') else '⬜'
78
+ lines.append(f"{status} {r.get('title', '?')} (πŸ”₯{r.get('streak', 0)}일)")
79
+ if r.get('completed_today'):
80
+ done += 1
81
+
82
+ lines.append(f"\n{done}/{len(routines)} μ™„λ£Œ")
83
+ await update.message.reply_text('\n'.join(lines))
84
+
85
+
86
+ async def cmd_score(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
87
+ if not is_authorized(update):
88
+ return
89
+ today_score = vault_io.get_today_score()
90
+ streaks = vault_io.get_active_streaks()
91
+ profile = vault_io.read_profile()
92
+
93
+ text = (
94
+ f"였늘 μŠ€μ½”μ–΄: {today_score}점\n"
95
+ f"λˆ„μ : {profile.get('total_score', 0)}점 (Lv.{profile.get('level', 1)})\n"
96
+ f"연속: {streaks['current']}일 (졜μž₯: {streaks['longest']}일)"
97
+ )
98
+ await update.message.reply_text(text)
99
+
100
+
101
+ async def cmd_dday(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
102
+ if not is_authorized(update):
103
+ return
104
+ milestones = vault_io.get_active_milestones()
105
+ if not milestones:
106
+ await update.message.reply_text("ν™œμ„± λ§ˆμΌμŠ€ν†€ μ—†μŒ.")
107
+ return
108
+
109
+ lines = ["D-day ν˜„ν™©:"]
110
+ for m in milestones[:8]:
111
+ d = m.get('d_day', 0)
112
+ label = f"D-{d}" if d > 0 else "D-Day" if d == 0 else f"D+{abs(d)}"
113
+ progress = 0
114
+ target = m.get('target_value', 0)
115
+ if target > 0:
116
+ progress = round(m.get('current_value', 0) / target * 100)
117
+ lines.append(f" {label}: {m.get('title', '?')} ({progress}%)")
118
+
119
+ await update.message.reply_text('\n'.join(lines))
120
+
121
+
122
+ async def cmd_timing(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
123
+ if not is_authorized(update):
124
+ return
125
+ text = timing_engine.get_monthly_briefing()
126
+ if not text:
127
+ await update.message.reply_text("타이밍 데이터 μ—†μŒ.")
128
+ return
129
+ await update.message.reply_text(f"이달 운기:\n{text}")
130
+
131
+
132
+ async def cmd_gyeokguk(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
133
+ if not is_authorized(update):
134
+ return
135
+ result = gyeokguk.judge_daily_actions()
136
+
137
+ lines = [f"격ꡭ νŒμ • ({result.get('date', '?')}):", ""]
138
+ for d in result.get('details', []):
139
+ arrow = 'β†—' if d['score'] > 0 else '↙' if d['score'] < 0 else 'β†’'
140
+ lines.append(f" {arrow} {d.get('action', '?')}: {d.get('reason', '')}")
141
+
142
+ lines.append("")
143
+ lines.append(f"μ „μ§„ {result.get('forward', 0)} / 후퇴 {result.get('backward', 0)} / 쀑립 {result.get('neutral', 0)}")
144
+ lines.append(f"νŒμ •: {result.get('verdict', '')}")
145
+
146
+ await update.message.reply_text('\n'.join(lines))
147
+
148
+
149
+ async def cmd_scorecard(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
150
+ if not is_authorized(update):
151
+ return
152
+ sc = gyeokguk.get_growth_scorecard()
153
+
154
+ lines = [f"μ„±μž₯ μŠ€μ½”μ–΄μΉ΄λ“œ ({sc.get('period', '')}):", ""]
155
+ for d in sc.get('daily', []):
156
+ bar = 'β–ˆ' * max(d.get('net_score', 0), 0) + 'β–‘' * max(-d.get('net_score', 0), 0)
157
+ lines.append(f" {d['date'][-5:]}: {bar or 'Β·'} ({d.get('net_score', 0):+d})")
158
+
159
+ lines.append("")
160
+ lines.append(f"μ£Όκ°„ μˆœμ „μ§„: {sc.get('weekly_net', 0):+d}")
161
+ lines.append(f"νŒμ •: {sc.get('verdict', '')}")
162
+
163
+ await update.message.reply_text('\n'.join(lines))
164
+
165
+
166
+ async def cmd_analyze(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
167
+ if not is_authorized(update):
168
+ return
169
+ goals = vault_io.get_active_goals()
170
+ if not goals:
171
+ await update.message.reply_text("ν™œμ„± λͺ©ν‘œ μ—†μŒ.")
172
+ return
173
+
174
+ # If goal_id specified
175
+ args = ctx.args
176
+ if args:
177
+ goal_id = args[0]
178
+ else:
179
+ # Show list and analyze first one
180
+ lines = ["뢄석할 λͺ©ν‘œ:"]
181
+ for g in goals:
182
+ lines.append(f" {g.get('id', '?')}: {g.get('title', '?')}")
183
+ lines.append(f"\n첫 번째 λͺ©ν‘œ 뢄석 쀑...")
184
+ await update.message.reply_text('\n'.join(lines))
185
+ goal_id = goals[0].get('id', '')
186
+
187
+ await update.message.reply_text(f"πŸ” '{goal_id}' 뢄석 쀑...")
188
+ result = goal_context_agent.run(goal_id)
189
+
190
+ if result.get('error'):
191
+ await update.message.reply_text(f"뢄석 μ‹€νŒ¨: {result['error']}")
192
+ return
193
+
194
+ lines = [
195
+ f"AI 진단: {result.get('diagnosis', '?')}",
196
+ f"λ°©ν–₯: {result.get('direction', '?')}",
197
+ f"λ‹€μŒ 리뷰: {result.get('next_review', '?')}",
198
+ ]
199
+ if result.get('milestones_created', 0) > 0:
200
+ lines.append(f"μƒˆ λ§ˆμΌμŠ€ν†€ {result['milestones_created']}개 생성됨.")
201
+
202
+ await update.message.reply_text('\n'.join(lines))
203
+
204
+
205
+ async def cmd_alerts(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
206
+ if not is_authorized(update):
207
+ return
208
+ alerts = proactive.check_all()
209
+ if not alerts:
210
+ await update.message.reply_text("ν˜„μž¬ μ•Œλ¦Ό μ—†μŒ. 잘 ν•˜κ³  μžˆλ‹€.")
211
+ return
212
+
213
+ lines = ["🚨 μ•Œλ¦Ό:"]
214
+ for a in alerts:
215
+ severity = {'critical': 'πŸ”΄', 'warning': '🟑', 'info': 'πŸ”΅'}.get(a.get('severity', 'info'), 'βšͺ')
216
+ lines.append(f" {severity} [{a.get('type', '?')}] {a.get('title', '')}: {a.get('message', '')}")
217
+
218
+ await update.message.reply_text('\n'.join(lines))
219
+
220
+
221
+ # ─── Message Handler (Intent Classification) ───
222
+
223
+ INTENT_KEYWORDS = {
224
+ 'check': ['체크', '루틴', 'μ™„λ£Œ', 'ν–ˆμ–΄', '끝'],
225
+ 'score': ['점수', 'μŠ€μ½”μ–΄', 'λͺ‡μ '],
226
+ 'timing': ['운기', 'μš΄μ„Έ', '타이밍', '이달'],
227
+ 'dday': ['디데이', 'd-day', '마감', 'λ§ˆμΌμŠ€ν†€'],
228
+ 'gyeokguk': ['격ꡭ', 'νŒμ •', 'μ „μ§„', '후퇴'],
229
+ 'morning': ['μ•„μΉ¨', 'λͺ¨λ‹', 'λΈŒλ¦¬ν•‘', '였늘'],
230
+ 'evening': ['저녁', '체크인', 'ν•˜λ£¨', '마무리'],
231
+ 'analyze': ['뢄석', '진단', 'ai'],
232
+ 'alerts': ['μ•Œλ¦Ό', 'κ²½κ³ ', '주의'],
233
+ }
234
+
235
+ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
236
+ """Free-text message intent classification."""
237
+ if not is_authorized(update):
238
+ return
239
+
240
+ text = update.message.text.lower()
241
+
242
+ # Match intent by keywords
243
+ for intent, keywords in INTENT_KEYWORDS.items():
244
+ if any(kw in text for kw in keywords):
245
+ handler_map = {
246
+ 'check': cmd_check,
247
+ 'score': cmd_score,
248
+ 'timing': cmd_timing,
249
+ 'dday': cmd_dday,
250
+ 'gyeokguk': cmd_gyeokguk,
251
+ 'morning': cmd_morning,
252
+ 'evening': cmd_evening,
253
+ 'analyze': cmd_analyze,
254
+ 'alerts': cmd_alerts,
255
+ }
256
+ handler = handler_map.get(intent)
257
+ if handler:
258
+ await handler(update, ctx)
259
+ return
260
+
261
+ # Default: show status summary
262
+ today_score = vault_io.get_today_score()
263
+ routines = vault_io.get_routines_with_status()
264
+ done = sum(1 for r in routines if r.get('completed_today'))
265
+ streaks = vault_io.get_active_streaks()
266
+
267
+ await update.message.reply_text(
268
+ f"μŠ€μ½”μ–΄: {today_score}점 | 루틴: {done}/{len(routines)} | 연속: {streaks['current']}일\n\n"
269
+ "λͺ…λ Ήμ–΄: /morning /check /score /dday /timing /gyeokguk /scorecard /analyze /alerts"
270
+ )
271
+
272
+
273
+ # ─── Scheduled Jobs ───
274
+
275
+ async def scheduled_morning_briefing(ctx: ContextTypes.DEFAULT_TYPE):
276
+ """맀일 μ•„μΉ¨ μžλ™ λΈŒλ¦¬ν•‘."""
277
+ chat_id = TELEGRAM_CHAT_ID.replace('tg:', '')
278
+ text = briefing.generate_morning_briefing()
279
+ await ctx.bot.send_message(chat_id=chat_id, text=text)
280
+
281
+
282
+ async def scheduled_evening_checkin(ctx: ContextTypes.DEFAULT_TYPE):
283
+ """맀일 저녁 μžλ™ 체크인."""
284
+ chat_id = TELEGRAM_CHAT_ID.replace('tg:', '')
285
+
286
+ # Recalculate streak before evening check-in
287
+ streak.calculate_streak()
288
+
289
+ text = briefing.generate_evening_checkin()
290
+ await ctx.bot.send_message(chat_id=chat_id, text=text)
291
+
292
+
293
+ async def scheduled_proactive_check(ctx: ContextTypes.DEFAULT_TYPE):
294
+ """주기적 proactive μ•Œλ¦Ό."""
295
+ chat_id = TELEGRAM_CHAT_ID.replace('tg:', '')
296
+ alerts = proactive.check_all()
297
+ critical = [a for a in alerts if a.get('severity') == 'critical']
298
+
299
+ if critical:
300
+ lines = ["🚨 κΈ΄κΈ‰ μ•Œλ¦Ό:"]
301
+ for a in critical:
302
+ lines.append(f" {a.get('title', '')}: {a.get('message', '')}")
303
+ await ctx.bot.send_message(chat_id=chat_id, text='\n'.join(lines))
304
+
305
+
306
+ # ─── Bot Setup ───
307
+
308
+ def create_bot() -> Application:
309
+ """Create and configure the Telegram bot."""
310
+ if not TELEGRAM_BOT_TOKEN:
311
+ raise ValueError("TELEGRAM_BOT_TOKEN not set")
312
+
313
+ app = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
314
+
315
+ # Commands
316
+ app.add_handler(CommandHandler('start', cmd_start))
317
+ app.add_handler(CommandHandler('morning', cmd_morning))
318
+ app.add_handler(CommandHandler('evening', cmd_evening))
319
+ app.add_handler(CommandHandler('check', cmd_check))
320
+ app.add_handler(CommandHandler('score', cmd_score))
321
+ app.add_handler(CommandHandler('dday', cmd_dday))
322
+ app.add_handler(CommandHandler('timing', cmd_timing))
323
+ app.add_handler(CommandHandler('gyeokguk', cmd_gyeokguk))
324
+ app.add_handler(CommandHandler('scorecard', cmd_scorecard))
325
+ app.add_handler(CommandHandler('analyze', cmd_analyze))
326
+ app.add_handler(CommandHandler('alerts', cmd_alerts))
327
+
328
+ # Free text
329
+ app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
330
+
331
+ return app