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,27 @@
1
+ 'use client'
2
+
3
+ import { useQuery } from '@tanstack/react-query'
4
+
5
+ export interface TimingContext {
6
+ month: string
7
+ heavenly_stem: string
8
+ earthly_branch: string
9
+ pillar: string
10
+ rating: number
11
+ theme: string
12
+ insight: string
13
+ action_guide: string[]
14
+ caution?: string[]
15
+ }
16
+
17
+ export function useCurrentTiming() {
18
+ return useQuery({
19
+ queryKey: ['timing', 'current'],
20
+ queryFn: async (): Promise<TimingContext | null> => {
21
+ const res = await fetch('/api/timing/current')
22
+ if (!res.ok) return null
23
+ return res.json()
24
+ },
25
+ staleTime: 1000 * 60 * 60, // 1h — timing doesn't change often
26
+ })
27
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared API fetch with error handling.
3
+ * All hooks should use this instead of raw fetch + .json().
4
+ */
5
+ export async function apiFetch<T = void>(url: string, init?: RequestInit): Promise<T> {
6
+ const res = await fetch(url, init)
7
+ if (!res.ok) {
8
+ let detail = ''
9
+ try { const body = await res.json(); detail = body.error || body.message || '' } catch {}
10
+ throw new Error(detail ? `HTTP ${res.status}: ${detail}` : `HTTP ${res.status}`)
11
+ }
12
+ const text = await res.text()
13
+ return (text ? JSON.parse(text) : undefined) as T
14
+ }
@@ -0,0 +1,32 @@
1
+ import { SignJWT, jwtVerify } from 'jose'
2
+
3
+ const SECRET = new TextEncoder().encode(
4
+ process.env.SESSION_SECRET || (() => { throw new Error('SESSION_SECRET not set') })()
5
+ )
6
+
7
+ export const COOKIE_NAME = 'gs_auth'
8
+ export const COOKIE_MAX_AGE = 7 * 24 * 60 * 60 // 7 days
9
+
10
+ export async function signToken(payload: { username: string }) {
11
+ return new SignJWT(payload)
12
+ .setProtectedHeader({ alg: 'HS256' })
13
+ .setIssuedAt()
14
+ .setExpirationTime('7d')
15
+ .sign(SECRET)
16
+ }
17
+
18
+ export async function verifyToken(token: string) {
19
+ try {
20
+ const { payload } = await jwtVerify(token, SECRET)
21
+ return payload as { username: string }
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ export function checkCredentials(username: string, password: string) {
28
+ return (
29
+ username === process.env.AUTH_USERNAME &&
30
+ password === process.env.AUTH_PASSWORD
31
+ )
32
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Get today's date in YYYY-MM-DD format, always in Asia/Seoul timezone.
3
+ * Prevents UTC-based date shifts during 00:00-09:00 KST.
4
+ */
5
+ export function getToday(): string {
6
+ return new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' })
7
+ }
@@ -0,0 +1,35 @@
1
+ import TurndownService from 'turndown'
2
+ import { marked } from 'marked'
3
+
4
+ // ─── Markdown → HTML ───
5
+ export function markdownToHtml(md: string): string {
6
+ if (!md || !md.trim()) return ''
7
+ return marked.parse(md, { async: false }) as string
8
+ }
9
+
10
+ // ─── HTML → Markdown ───
11
+ const turndown = new TurndownService({
12
+ headingStyle: 'atx',
13
+ codeBlockStyle: 'fenced',
14
+ bulletListMarker: '-',
15
+ })
16
+
17
+ // Task list support
18
+ turndown.addRule('taskListItem', {
19
+ filter: (node) => {
20
+ return (
21
+ node.nodeName === 'LI' &&
22
+ node.getAttribute('data-type') === 'taskItem'
23
+ )
24
+ },
25
+ replacement: (content, node) => {
26
+ const checked = (node as HTMLElement).getAttribute('data-checked') === 'true'
27
+ const cleaned = content.replace(/^\n+/, '').replace(/\n+$/, '')
28
+ return `- [${checked ? 'x' : ' '}] ${cleaned}\n`
29
+ },
30
+ })
31
+
32
+ export function htmlToMarkdown(html: string): string {
33
+ if (!html || !html.trim()) return ''
34
+ return turndown.turndown(html).trim()
35
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * LLM Call Governor — Layer 5 Runtime Governance
3
+ * Based on Matthew Berman's OpenClaw security design
4
+ *
5
+ * Protection layers:
6
+ * 1. Spend limit — sliding 5-min window, hard cap $15
7
+ * 2. Volume limit — 200 calls/10min global, per-caller overrides
8
+ * 3. Lifetime counter — 500 calls/process max
9
+ * 4. Duplicate detection — hash cache, TTL 60s
10
+ */
11
+
12
+ type CallerConfig = {
13
+ maxPer10Min?: number
14
+ }
15
+
16
+ const CALLERS: Record<string, CallerConfig> = {
17
+ 'voice/chat': { maxPer10Min: 40 },
18
+ 'voice/transcribe': { maxPer10Min: 60 },
19
+ 'voice/tts': { maxPer10Min: 60 },
20
+ 'default': { maxPer10Min: 200 },
21
+ }
22
+
23
+ // Estimated cost per 1K tokens (input+output blended)
24
+ const COST_PER_CALL: Record<string, number> = {
25
+ 'gpt-4o': 0.010,
26
+ 'gpt-4o-mini': 0.0003,
27
+ 'whisper': 0.006, // per minute, approximate
28
+ 'tts': 0.015, // per 1K chars
29
+ 'default': 0.010,
30
+ }
31
+
32
+ // --- In-memory state (resets on process restart) ---
33
+ const state = {
34
+ lifetimeCount: 0,
35
+ spendWindow: [] as { ts: number; cost: number }[],
36
+ volumeWindow: [] as { ts: number; caller: string }[],
37
+ callerCounts: new Map<string, { ts: number }[]>(),
38
+ hashCache: new Map<string, { result: unknown; ts: number }>(),
39
+ }
40
+
41
+ const LIFETIME_LIMIT = 500
42
+ const SPEND_WINDOW_MS = 5 * 60 * 1000 // 5 min
43
+ const SPEND_WARN = 5 // $5
44
+ const SPEND_HARD = 15 // $15
45
+ const VOLUME_WINDOW_MS = 10 * 60 * 1000 // 10 min
46
+ const VOLUME_GLOBAL = 200
47
+ const HASH_TTL_MS = 60 * 1000 // 60s
48
+
49
+ function now() { return Date.now() }
50
+
51
+ function pruneWindow<T extends { ts: number }>(arr: T[], windowMs: number): T[] {
52
+ const cutoff = now() - windowMs
53
+ return arr.filter(e => e.ts > cutoff)
54
+ }
55
+
56
+ function hashPrompt(prompt: string): string {
57
+ // Simple djb2 hash — good enough for dedup
58
+ let h = 5381
59
+ for (let i = 0; i < Math.min(prompt.length, 2000); i++) {
60
+ h = ((h << 5) + h) ^ prompt.charCodeAt(i)
61
+ h = h >>> 0
62
+ }
63
+ return h.toString(16)
64
+ }
65
+
66
+ export type GovernorResult =
67
+ | { ok: true; cached: false }
68
+ | { ok: true; cached: true; result: unknown }
69
+ | { ok: false; reason: string; code: 'SPEND_LIMIT' | 'VOLUME_LIMIT' | 'LIFETIME_LIMIT' }
70
+
71
+ /**
72
+ * Check before making an LLM call.
73
+ * @param caller e.g. 'voice/chat', 'voice/transcribe'
74
+ * @param prompt The prompt text (for dedup hash)
75
+ * @param model Model name key (for cost estimate)
76
+ */
77
+ export function governorCheck(
78
+ caller: string,
79
+ prompt: string,
80
+ model = 'default'
81
+ ): GovernorResult {
82
+ // 1. Lifetime limit
83
+ if (state.lifetimeCount >= LIFETIME_LIMIT) {
84
+ return { ok: false, reason: `Lifetime LLM call limit reached (${LIFETIME_LIMIT})`, code: 'LIFETIME_LIMIT' }
85
+ }
86
+
87
+ // 2. Spend window
88
+ state.spendWindow = pruneWindow(state.spendWindow, SPEND_WINDOW_MS)
89
+ const totalSpend = state.spendWindow.reduce((s, e) => s + e.cost, 0)
90
+ if (totalSpend >= SPEND_HARD) {
91
+ return { ok: false, reason: `Spend hard cap hit ($${SPEND_HARD}/5min). Current: $${totalSpend.toFixed(3)}`, code: 'SPEND_LIMIT' }
92
+ }
93
+ if (totalSpend >= SPEND_WARN) {
94
+ console.warn(`[llm-governor] ⚠️ Spend warning: $${totalSpend.toFixed(3)} in last 5min (limit $${SPEND_HARD})`)
95
+ }
96
+
97
+ // 3. Global volume window
98
+ state.volumeWindow = pruneWindow(state.volumeWindow, VOLUME_WINDOW_MS)
99
+ if (state.volumeWindow.length >= VOLUME_GLOBAL) {
100
+ return { ok: false, reason: `Global volume limit hit (${VOLUME_GLOBAL}/10min)`, code: 'VOLUME_LIMIT' }
101
+ }
102
+
103
+ // 4. Per-caller volume
104
+ const callerCfg = CALLERS[caller] ?? CALLERS['default']
105
+ const callerMax = callerCfg.maxPer10Min ?? 200
106
+ const callerWindow = pruneWindow(state.callerCounts.get(caller) ?? [], VOLUME_WINDOW_MS)
107
+ if (callerWindow.length >= callerMax) {
108
+ return { ok: false, reason: `Caller ${caller} volume limit hit (${callerMax}/10min)`, code: 'VOLUME_LIMIT' }
109
+ }
110
+
111
+ // 5. Duplicate detection
112
+ const hash = hashPrompt(prompt)
113
+ const cached = state.hashCache.get(hash)
114
+ if (cached && now() - cached.ts < HASH_TTL_MS) {
115
+ console.log(`[llm-governor] Cache hit for ${caller} (hash ${hash})`)
116
+ return { ok: true, cached: true, result: cached.result }
117
+ }
118
+
119
+ return { ok: true, cached: false }
120
+ }
121
+
122
+ /**
123
+ * Record a completed LLM call (update spend + volume windows).
124
+ */
125
+ export function governorRecord(
126
+ caller: string,
127
+ estimatedCost: number,
128
+ promptHash?: string,
129
+ result?: unknown
130
+ ) {
131
+ const t = now()
132
+ state.lifetimeCount++
133
+ state.spendWindow.push({ ts: t, cost: estimatedCost })
134
+ state.volumeWindow.push({ ts: t, caller })
135
+
136
+ const callerWindow = pruneWindow(state.callerCounts.get(caller) ?? [], VOLUME_WINDOW_MS)
137
+ callerWindow.push({ ts: t })
138
+ state.callerCounts.set(caller, callerWindow)
139
+
140
+ if (promptHash && result !== undefined) {
141
+ state.hashCache.set(promptHash, { result, ts: t })
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Estimate cost for a call (rough).
147
+ */
148
+ export function estimateCost(model: string, inputChars: number): number {
149
+ const rate = COST_PER_CALL[model] ?? COST_PER_CALL['default']
150
+ const estTokens = inputChars / 4
151
+ return (estTokens / 1000) * rate
152
+ }
153
+
154
+ /**
155
+ * Get current governor stats (for /api/status or logging).
156
+ */
157
+ export function governorStats() {
158
+ const t = now()
159
+ const spendArr = pruneWindow(state.spendWindow, SPEND_WINDOW_MS)
160
+ const volArr = pruneWindow(state.volumeWindow, VOLUME_WINDOW_MS)
161
+ return {
162
+ lifetimeCount: state.lifetimeCount,
163
+ spendLast5Min: spendArr.reduce((s, e) => s + e.cost, 0).toFixed(4),
164
+ callsLast10Min: volArr.length,
165
+ cacheSize: state.hashCache.size,
166
+ }
167
+ }
@@ -0,0 +1,35 @@
1
+ export function getEnergyRecommendation(
2
+ currentHour: number,
3
+ peakHours: number[],
4
+ energyLogs: { hour: number; level: number }[]
5
+ ): {
6
+ level: 'high' | 'medium' | 'low'
7
+ recommendation: string
8
+ suggestedTaskType: string
9
+ } {
10
+ const recentLogs = energyLogs
11
+ .filter(l => l.hour === currentHour)
12
+ .slice(-7)
13
+
14
+ const avgEnergy = recentLogs.length
15
+ ? recentLogs.reduce((sum, l) => sum + l.level, 0) / recentLogs.length
16
+ : peakHours.includes(currentHour) ? 4 : 2.5
17
+
18
+ if (avgEnergy >= 3.5) return {
19
+ level: 'high',
20
+ recommendation: '지금이 딥워크 골든타임입니다',
21
+ suggestedTaskType: '창의적 작업 / 어려운 문제 해결',
22
+ }
23
+
24
+ if (avgEnergy >= 2.5) return {
25
+ level: 'medium',
26
+ recommendation: '집중력 유지 가능한 시간입니다',
27
+ suggestedTaskType: '미팅 / 이메일 / 리뷰',
28
+ }
29
+
30
+ return {
31
+ level: 'low',
32
+ recommendation: '에너지 보충이 필요한 시간입니다',
33
+ suggestedTaskType: '루틴 작업 / 행정 / 휴식',
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ import type { Routine } from '@/types'
2
+
3
+ export function buildHabitChain<T extends Routine>(routines: T[]): T[][] {
4
+ const anchors = routines.filter(r => !r.after_routine_id)
5
+ const chains: T[][] = []
6
+
7
+ for (const anchor of anchors) {
8
+ const chain: T[] = [anchor]
9
+ let current = anchor
10
+
11
+ while (true) {
12
+ const next = routines.find(r => r.after_routine_id === current.id)
13
+ if (!next) break
14
+ chain.push(next)
15
+ current = next
16
+ }
17
+
18
+ chains.push(chain)
19
+ }
20
+
21
+ return chains
22
+ }
@@ -0,0 +1,32 @@
1
+ import type { ScoreEvent, ScoreResult } from '@/types'
2
+
3
+ const BASE_SCORES: Record<ScoreEvent['type'], number> = {
4
+ routine_complete: 10,
5
+ task_done: 5,
6
+ task_done_urgent: 15,
7
+ milestone_complete: 100,
8
+ review_written: 20,
9
+ data_uploaded: 10,
10
+ goal_25pct: 30,
11
+ goal_50pct: 60,
12
+ goal_100pct: 200,
13
+ }
14
+
15
+ export function calculateScore(event: ScoreEvent): ScoreResult {
16
+ const base = BASE_SCORES[event.type] ?? 0
17
+
18
+ // streak multiplier (max 5x)
19
+ const streak = event.streakCount ?? 0
20
+ const multiplier = Math.min(1 + streak * 0.05, 5.0)
21
+ let points = Math.round(base * multiplier)
22
+
23
+ // 15% variable bonus (Skinner)
24
+ let bonus_message: string | null = null
25
+ if (Math.random() < 0.15) {
26
+ const bonus = [5, 10, 15, 25][Math.floor(Math.random() * 4)]
27
+ points += bonus
28
+ bonus_message = `보너스 +${bonus}점!`
29
+ }
30
+
31
+ return { points, bonus_message }
32
+ }
@@ -0,0 +1,15 @@
1
+ /** Mapping from entity type to URL prefix */
2
+ export const ENTITY_ROUTES: Record<string, string> = {
3
+ note: '/notes',
4
+ task: '/tasks',
5
+ goal: '/goals',
6
+ project: '/projects',
7
+ book: '/books',
8
+ }
9
+
10
+ /** Build href for an entity */
11
+ export function entityHref(type: string, id: string): string {
12
+ const prefix = ENTITY_ROUTES[type]
13
+ if (!prefix) return '/'
14
+ return `${prefix}/${id}`
15
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,29 @@
1
+ import path from 'path'
2
+
3
+ if (!process.env.VAULT_PATH) {
4
+ throw new Error('VAULT_PATH environment variable is required. Set it to your Obsidian vault GatsaengOS folder.')
5
+ }
6
+ export const VAULT_PATH = process.env.VAULT_PATH
7
+
8
+ export const FOLDERS = {
9
+ areas: path.join(VAULT_PATH, 'areas'),
10
+ goals: path.join(VAULT_PATH, 'goals'),
11
+ milestones: path.join(VAULT_PATH, 'milestones'),
12
+ projects: path.join(VAULT_PATH, 'projects'),
13
+ tasks: path.join(VAULT_PATH, 'tasks'),
14
+ routines: path.join(VAULT_PATH, 'routines'),
15
+ reviews: path.join(VAULT_PATH, 'reviews'),
16
+ sessions: path.join(VAULT_PATH, 'sessions'),
17
+ timing: path.join(VAULT_PATH, 'timing'),
18
+ books: path.join(VAULT_PATH, 'books'),
19
+ calendar: path.join(VAULT_PATH, 'calendar'),
20
+ notes: path.join(VAULT_PATH, 'notes'),
21
+ // v2 compat — kept for existing data
22
+ routineLogs: path.join(VAULT_PATH, 'logs', 'routine'),
23
+ energyLogs: path.join(VAULT_PATH, 'logs', 'energy'),
24
+ focusSessions: path.join(VAULT_PATH, 'logs', 'focus'),
25
+ } as const
26
+
27
+ export const PROFILE_PATH = path.join(VAULT_PATH, 'profile.md')
28
+
29
+ export type FolderKey = keyof typeof FOLDERS
@@ -0,0 +1,84 @@
1
+ import matter from 'gray-matter'
2
+ import type { ZodSchema } from 'zod'
3
+
4
+ export interface ParsedFile<T> {
5
+ data: T
6
+ content: string
7
+ }
8
+
9
+ function coerceDates(obj: Record<string, unknown>): Record<string, unknown> {
10
+ const result: Record<string, unknown> = {}
11
+ for (const [key, value] of Object.entries(obj)) {
12
+ if (value instanceof Date) {
13
+ result[key] = value.toISOString()
14
+ } else if (Array.isArray(value)) {
15
+ result[key] = value.map(v => v instanceof Date ? v.toISOString() : v)
16
+ } else {
17
+ result[key] = value
18
+ }
19
+ }
20
+ return result
21
+ }
22
+
23
+ export function parseMarkdown<T>(raw: string, schema?: ZodSchema<T>): ParsedFile<T> {
24
+ const { data, content } = matter(raw)
25
+ const coerced = coerceDates(data)
26
+ if (schema) {
27
+ const parsed = schema.parse(coerced)
28
+ return { data: parsed, content: content.trim() }
29
+ }
30
+ return { data: coerced as T, content: content.trim() }
31
+ }
32
+
33
+ export function safeParseMarkdown<T>(raw: string, schema: ZodSchema<T>): ParsedFile<T> | null {
34
+ const { data, content } = matter(raw)
35
+ const coerced = coerceDates(data)
36
+ const result = schema.safeParse(coerced)
37
+ if (result.success) {
38
+ return { data: result.data, content: content.trim() }
39
+ }
40
+ return null
41
+ }
42
+
43
+ function stripUndefined(obj: unknown): unknown {
44
+ if (Array.isArray(obj)) return obj.map(stripUndefined)
45
+ if (obj && typeof obj === 'object' && !(obj instanceof Date)) {
46
+ const clean: Record<string, unknown> = {}
47
+ for (const [k, v] of Object.entries(obj)) {
48
+ if (v !== undefined) clean[k] = stripUndefined(v)
49
+ }
50
+ return clean
51
+ }
52
+ return obj
53
+ }
54
+
55
+ export function stringifyMarkdown(data: object, content?: string): string {
56
+ return matter.stringify(content || '', stripUndefined(data) as Record<string, unknown>)
57
+ }
58
+
59
+ export function extractBodySections(content: string): Record<string, string> {
60
+ const sections: Record<string, string> = {}
61
+ const lines = content.split('\n')
62
+ let currentSection = ''
63
+ let currentContent: string[] = []
64
+
65
+ for (const line of lines) {
66
+ const headerMatch = line.match(/^##\s+(.+)$/)
67
+ if (headerMatch) {
68
+ if (currentSection) {
69
+ sections[currentSection] = currentContent.join('\n').trim()
70
+ }
71
+ currentSection = headerMatch[1]
72
+ currentContent = []
73
+ } else if (currentSection) {
74
+ currentContent.push(line)
75
+ }
76
+ }
77
+
78
+ if (currentSection) {
79
+ sections[currentSection] = currentContent.join('\n').trim()
80
+ }
81
+
82
+ return sections
83
+ }
84
+