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,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,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,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
|
+
|