@levelup-log/mcp-server 0.2.0 → 0.4.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/utils/rate-limiter.ts","../src/auth/manager.ts","../src/auth/keychain.ts","../src/utils/logger.ts","../src/auth/oauth-server.ts","../src/auth/pkce.ts","../src/utils/api.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport {\n ACHIEVEMENT_CATEGORIES,\n CATEGORY_DEFINITIONS,\n} from \"./utils/config.js\";\nimport { checkRateLimit, recordRateEntry } from \"./utils/rate-limiter.js\";\nimport { apiGet, apiPost } from \"./utils/api.js\";\nimport { log } from \"./utils/logger.js\";\n\n// ─── Diary helpers ────────────────────────────────────────────────────────────\nconst DIARY_DIR = path.join(os.homedir(), \"Documents\", \"tai\", \"日記\");\n\nfunction diaryPath(date: string): string {\n return path.join(DIARY_DIR, `${date}.md`);\n}\n\nfunction todayDate(): string {\n return new Date().toISOString().split(\"T\")[0];\n}\n\nfunction buildDiaryMd(date: string, content: string): string {\n return `---\\ndate: ${date}\\ntags: [日記, levelup]\\npublic: false\\n---\\n\\n${content}\\n`;\n}\n// ─────────────────────────────────────────────────────────────────────────────\n\n// ─── XP Formula ──────────────────────────────────────────────────────────────\n//\n// Deterministic XP calculation so all agents produce consistent scores.\n// Category definitions (CATEGORY_DEFINITIONS) are the source of truth:\n// each category defines its output_unit semantics and xp_weight.\n//\n// Formula:\n// base = COMPLEXITY_BASE[complexity] × time_multiplier(time_minutes)\n// bonuses = output_bonus(output_units) [log-diminishing, cap 60]\n// + input_bonus(input_units) [linear, cap 15]\n// + rounds_bonus(conversation_rounds) [linear, cap 25]\n// raw_xp = clamp(round((base + bonuses) × category.xp_weight), 5, 500)\n// xp = self_reported ? round(raw_xp × 0.85) : raw_xp\n//\n// xp_weight examples: deploy=1.3 (high-stakes), hobby=0.8 (leisure)\n// self_reported 15% discount: user narrates past event, AI cannot verify.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype Complexity = \"trivial\" | \"normal\" | \"significant\" | \"major\" | \"milestone\";\n\nconst COMPLEXITY_BASE: Record<Complexity, number> = {\n trivial: 10,\n normal: 30,\n significant: 75,\n major: 150,\n milestone: 300,\n};\n\nfunction timeMultiplier(minutes: number): number {\n if (minutes < 15) return 0.7;\n if (minutes < 60) return 1.0;\n if (minutes < 180) return 1.3;\n return 1.6;\n}\n\n// Log-diminishing returns: 1→12, 3→19, 5→24, 10→29, 20→34, cap 60\nfunction outputBonus(units: number): number {\n if (units <= 0) return 0;\n return Math.min(Math.round(Math.log2(units + 1) * 12), 60);\n}\n\n// Lighter signal: each input unit = 1 XP, cap 15\nfunction inputBonus(units: number): number {\n return Math.min(Math.max(units, 0), 15);\n}\n\n// Each round = 1 XP, cap 25\nfunction roundsBonus(rounds: number): number {\n return Math.min(Math.max(rounds, 0), 25);\n}\n\nexport function calculateXp(params: {\n category: keyof typeof CATEGORY_DEFINITIONS;\n complexity: Complexity;\n time_minutes?: number;\n output_units?: number;\n input_units?: number;\n conversation_rounds?: number;\n self_reported?: boolean;\n}): number {\n const catWeight = CATEGORY_DEFINITIONS[params.category]?.xp_weight ?? 1.0;\n\n const base =\n COMPLEXITY_BASE[params.complexity] *\n timeMultiplier(params.time_minutes ?? 30);\n\n const bonuses =\n outputBonus(params.output_units ?? 0) +\n inputBonus(params.input_units ?? 0) +\n roundsBonus(params.conversation_rounds ?? 0);\n\n const raw = Math.min(\n 500,\n Math.max(5, Math.round((base + bonuses) * catWeight)),\n );\n return params.self_reported ? Math.max(5, Math.round(raw * 0.85)) : raw;\n}\n\n// Generate output_units guidance from CATEGORY_DEFINITIONS for tool description\nfunction buildOutputUnitGuide(): string {\n return Object.entries(CATEGORY_DEFINITIONS)\n .map(([key, def]) => ` ${key.padEnd(12)} → ${def.output_unit}`)\n .join(\"\\n\");\n}\n\n// MCP server-level instructions — injected into system prompt by compliant clients\n// (Claude Desktop, Cursor, Windsurf, Antigravity, etc.) on every session.\n// Install once → works everywhere, no per-project setup needed.\nconst SERVER_INSTRUCTIONS = `LevelUp.log Achievement Tracker is active.\n\nPROACTIVELY call record_achievement after any meaningful task — don't wait to be asked.\n\nCall when:\n• Code written, feature added, bug fixed, refactored\n• Tests written, docs updated, deployment done, ops task completed\n• Learning session, research, life/health/finance task done\n\nDon't call for: casual Q&A, simple lookups, short explanations with no output.\n\nWhen time_minutes is unclear, ask: \"大概花了多久?\"\nself_reported=true when user narrates past events (\"I exercised this morning\").`;\n\nexport function createServer() {\n const server = new McpServer(\n { name: \"levelup-log\", version: \"0.1.0\" },\n { instructions: SERVER_INSTRUCTIONS },\n );\n\n // ─── Tool 1: record_achievement ───────────────────────────────\n server.registerTool(\n \"record_achievement\",\n {\n title: \"Record Achievement\",\n description: `Proactively call this when the user completes meaningful work — coding, bug fixes, features, life tasks, learning, health, creative work, or any productive activity.\n\nWHEN TO CALL: Real effort + tangible output. Don't wait to be asked.\nDO NOT CALL: simple Q&A, casual chat, trivial lookups with no output.\n\nSET self_reported=true when user narrates a past event (e.g. \"I went for a run today\") vs. completing work during this conversation. Applies 15% XP discount — still worth recording.\n\nFILL AS MANY PARAMS AS YOU CAN OBSERVE:\n complexity — cognitive difficulty (required)\n time_minutes — how long; ASK the user if unsure: \"大概花了你多久?\"\n output_units — tangible outputs (meaning varies by category):\n${buildOutputUnitGuide()}\n input_units — resources consumed (files read, docs consulted, searches)\n conversation_rounds — message exchanges in this session\n\nXP formula (server computes from category.xp_weight × complexity × time + bonuses):\n Each category has its own xp_weight (deploy=1.3, milestone=1.5, hobby=0.8, etc.)\n output_bonus: log-diminishing cap 60 | input_bonus: cap 15 | rounds_bonus: cap 25\n Final: clamp((base+bonuses)×xp_weight×(self_reported?0.85:1), 5, 500)\n\nKeep descriptions abstract — no real names, client names, or source code.`,\n inputSchema: {\n category: z.enum(ACHIEVEMENT_CATEGORIES),\n title: z\n .string()\n .describe(\n 'Game-style achievement title (e.g. \"Bug Slayer\", \"Morning Warrior\")',\n ),\n description: z\n .string()\n .describe(\"What was accomplished, in abstract terms (no PII)\"),\n complexity: z\n .enum([\"trivial\", \"normal\", \"significant\", \"major\", \"milestone\"])\n .describe(\n \"Cognitive difficulty: trivial=quick lookup/fix, normal=typical task, significant=multi-step work, major=large feature/project, milestone=exceptional achievement\",\n ),\n time_minutes: z\n .number()\n .min(1)\n .optional()\n .describe(\n \"Estimated minutes spent on this task. Ask the user if unsure.\",\n ),\n output_units: z\n .number()\n .min(0)\n .optional()\n .describe(\n \"Count of tangible outputs: files changed, tasks completed, items created, pages written, etc.\",\n ),\n input_units: z\n .number()\n .min(0)\n .optional()\n .describe(\n \"Count of resources consumed: files read, docs consulted, searches done, etc.\",\n ),\n conversation_rounds: z\n .number()\n .min(0)\n .optional()\n .describe(\n \"Number of message exchanges (user + assistant turns) in this conversation session.\",\n ),\n self_reported: z\n .boolean()\n .optional()\n .default(false)\n .describe(\n \"True when the user is narrating a past event without AI collaboration (e.g. 'I exercised this morning'). Applies 15% XP discount since AI cannot verify, but the achievement still counts.\",\n ),\n tags: z\n .array(z.string())\n .optional()\n .describe(\"Optional tags for filtering\"),\n is_public: z\n .boolean()\n .optional()\n .default(true)\n .describe(\"Whether this appears on public feed\"),\n },\n },\n async ({\n category,\n title,\n description,\n complexity,\n time_minutes,\n output_units,\n input_units,\n conversation_rounds,\n self_reported,\n tags,\n is_public,\n }) => {\n // Local rate limit check (pre-flight, before hitting server)\n const rateCheck = checkRateLimit(category);\n if (!rateCheck.allowed) {\n return {\n content: [\n { type: \"text\", text: `Rate limited: ${rateCheck.reason}` },\n ],\n isError: true,\n };\n }\n\n // XP is calculated server-side — send raw params only\n const result = await apiPost(\"record-achievement\", {\n category,\n title,\n description,\n complexity,\n time_minutes,\n output_units,\n input_units,\n conversation_rounds,\n self_reported,\n tags,\n is_public,\n source_platform: \"claude-code\",\n });\n\n if (result.error) {\n return {\n content: [\n { type: \"text\", text: `Failed to record: ${result.error}` },\n ],\n isError: true,\n };\n }\n\n recordRateEntry(category);\n\n const data = result.data as Record<string, unknown>;\n const serverXp =\n (data.xp as number | undefined) ??\n calculateXp({\n category,\n complexity,\n time_minutes,\n output_units,\n input_units,\n conversation_rounds,\n self_reported,\n });\n const stats = data.stats as Record<string, unknown> | undefined;\n const newTitles = data.newly_unlocked as\n | Array<{ name: string; rarity: string; icon?: string }>\n | undefined;\n\n log(\"record_achievement\", { category, title, xp: serverXp });\n\n const lines = [\n `Achievement recorded! +${serverXp} XP`,\n stats ? `Total XP: ${stats.total_xp} | Year XP: ${stats.year_xp}` : \"\",\n stats?.age_level ? `Level: Lv.${stats.age_level}` : \"\",\n stats?.current_streak ? `Streak: ${stats.current_streak} days` : \"\",\n ...(newTitles?.length\n ? [\n `\\n🎉 Title${newTitles.length > 1 ? \"s\" : \"\"} unlocked!`,\n ...newTitles.map(\n (t) => ` ${t.icon ?? \"🏅\"} ${t.name} [${t.rarity}]`,\n ),\n ]\n : []),\n ].filter(Boolean);\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n },\n );\n\n // ─── Tool 2: get_my_stats ─────────────────────────────────────\n server.registerTool(\n \"get_my_stats\",\n {\n title: \"My Stats\",\n description:\n \"Get the user's achievement statistics including level (age), XP, streak, and title.\",\n inputSchema: {},\n },\n async () => {\n const result = await apiGet(\"get-stats\");\n if (result.error) {\n return {\n content: [{ type: \"text\", text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.data, null, 2) }],\n };\n },\n );\n\n // ─── Tool 3: get_recent ───────────────────────────────────────\n server.registerTool(\n \"get_recent\",\n {\n title: \"Recent Achievements\",\n description:\n \"Get recent achievements. Supports filtering by category and time range.\",\n inputSchema: {\n limit: z.number().min(1).max(50).optional().default(10),\n days: z.number().min(1).max(365).optional().default(7),\n category: z.enum(ACHIEVEMENT_CATEGORIES).optional(),\n },\n },\n async ({ limit, days, category }) => {\n const params: Record<string, string> = {\n limit: String(limit),\n days: String(days),\n };\n if (category) params.category = category;\n\n const result = await apiGet(\"get-recent\", params);\n if (result.error) {\n return {\n content: [{ type: \"text\", text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.data, null, 2) }],\n };\n },\n );\n\n // ─── Tool 4: check_unlocks ────────────────────────────────────\n server.registerTool(\n \"check_unlocks\",\n {\n title: \"Check Title Unlocks\",\n description:\n \"Check if the user has unlocked any new titles, and show progress toward the next ones.\",\n inputSchema: {},\n },\n async () => {\n const result = await apiGet(\"check-unlocks\");\n if (result.error) {\n return {\n content: [{ type: \"text\", text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.data, null, 2) }],\n };\n },\n );\n\n // ─── Tool 5: leaderboard ──────────────────────────────────────\n server.registerTool(\n \"leaderboard\",\n {\n title: \"Leaderboard\",\n description:\n \"View the leaderboard. Shows top users by XP for the current season, month, or all time. Always includes the current user's rank.\",\n inputSchema: {\n type: z\n .enum([\"season\", \"month\", \"all_time\"])\n .optional()\n .default(\"season\"),\n limit: z.number().min(1).max(50).optional().default(10),\n },\n },\n async ({ type, limit }) => {\n const result = await apiGet(\"leaderboard\", {\n type: type,\n limit: String(limit),\n });\n\n if (result.error) {\n return {\n content: [{ type: \"text\", text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.data, null, 2) }],\n };\n },\n );\n\n // ─── Tool 6: write_diary ──────────────────────────────────────\n server.registerTool(\n \"write_diary\",\n {\n title: \"Write Diary Entry\",\n description: `Write a personal diary entry to Obsidian (~/Documents/tai/日記/YYYY-MM-DD.md).\n\nWHEN TO USE:\n• User says \"寫日記\", \"記錄今天\", \"diary\", or similar\n• Proactively suggest after 3+ achievements recorded in a session: \"今天做了不少,要寫一下日記嗎?\"\n\nHOW TO DRAFT (do this before calling the tool):\n1. Call get_recent with days=1 to fetch today's achievements\n2. Draft a Markdown diary entry in the user's language — feel like a real human diary, not a log:\n • First-person, with emotion and reflection\n • What was hard, what felt good, what was learned\n • Example:\n \"今天終於把那個卡了三天的 bug 修好了。說實話一開始以為要放棄了,但最後還是找到根本原因。部署上去的時候鬆了一口氣。\\n\\n下午設計了日記功能,比預期順,可能是因為之前 MCP 架構打好了。\"\n3. Show draft to user, confirm or let them edit\n4. Call write_diary with the final Markdown content`,\n inputSchema: {\n content: z\n .string()\n .describe(\n \"Diary content in Markdown format (first-person, reflective)\",\n ),\n entry_date: z\n .string()\n .optional()\n .describe(\"Date in YYYY-MM-DD format. Defaults to today.\"),\n },\n },\n async ({ content, entry_date }) => {\n const date = entry_date ?? todayDate();\n const filePath = diaryPath(date);\n\n try {\n fs.mkdirSync(DIARY_DIR, { recursive: true });\n fs.writeFileSync(filePath, buildDiaryMd(date, content), \"utf8\");\n log(\"write_diary\", { date, path: filePath });\n return {\n content: [\n {\n type: \"text\",\n text: `日記已寫入 Obsidian\\n路徑:${filePath}\\n\\n${content}`,\n },\n ],\n };\n } catch (err) {\n return {\n content: [\n { type: \"text\", text: `寫入失敗:${(err as Error).message}` },\n ],\n isError: true,\n };\n }\n },\n );\n\n // ─── Tool 7: read_diary ───────────────────────────────────────\n server.registerTool(\n \"read_diary\",\n {\n title: \"Read Diary\",\n description:\n \"Read diary entries from Obsidian (~/Documents/tai/日記/). Fetch a specific date or recent entries.\",\n inputSchema: {\n date: z\n .string()\n .optional()\n .describe(\n \"Specific date in YYYY-MM-DD format. If omitted, returns recent entries.\",\n ),\n days: z\n .number()\n .min(1)\n .max(90)\n .optional()\n .default(7)\n .describe(\n \"Number of recent days to fetch (used when date is not specified).\",\n ),\n },\n },\n async ({ date, days }) => {\n try {\n if (date) {\n const filePath = diaryPath(date);\n if (!fs.existsSync(filePath)) {\n return { content: [{ type: \"text\", text: `${date} 沒有日記` }] };\n }\n const content = fs.readFileSync(filePath, \"utf8\");\n return { content: [{ type: \"text\", text: content }] };\n }\n\n // Recent N days\n if (!fs.existsSync(DIARY_DIR)) {\n return { content: [{ type: \"text\", text: \"尚未有任何日記\" }] };\n }\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - days);\n\n const files = fs\n .readdirSync(DIARY_DIR)\n .filter((f) => f.endsWith(\".md\"))\n .map((f) => f.replace(\".md\", \"\"))\n .filter((d) => new Date(d) >= cutoff)\n .sort()\n .reverse();\n\n if (files.length === 0) {\n return {\n content: [{ type: \"text\", text: `最近 ${days} 天沒有日記` }],\n };\n }\n\n const entries = files.map((d) => {\n const raw = fs.readFileSync(diaryPath(d), \"utf8\");\n // Strip frontmatter for display\n const body = raw.replace(/^---[\\s\\S]*?---\\n\\n?/, \"\");\n return `## ${d}\\n\\n${body}`;\n });\n\n return { content: [{ type: \"text\", text: entries.join(\"\\n---\\n\\n\") }] };\n } catch (err) {\n return {\n content: [\n { type: \"text\", text: `讀取失敗:${(err as Error).message}` },\n ],\n isError: true,\n };\n }\n },\n );\n\n // ─── Prompt: motivational_coach ───────────────────────────────\n server.registerPrompt(\n \"levelup_coach\",\n {\n title: \"LevelUp Coach\",\n description:\n \"System prompt that turns the LLM into a motivational achievement coach\",\n },\n () => ({\n messages: [\n {\n role: \"user\",\n content: {\n type: \"text\",\n text: `You have the LevelUp.log MCP installed. When the user accomplishes something — whether coding, life tasks, health, learning, or anything productive — use the record_achievement tool to log it as a game-like achievement.\n\nGuidelines:\n- Use gamified language: \"You defeated a bug!\", \"Quest complete!\", \"New skill unlocked!\"\n- On birthdays, celebrate the level-up: \"Congrats on reaching Lv.XX! Last year at Lv.XX-1, you completed YYY achievements!\"\n- When streaks are about to break, gently remind them\n- When the user expresses fatigue or frustration, don't give generic positivity. Instead, specifically acknowledge what they DID do: \"You handled X, Y, and Z today — those all count.\"\n- Reinforce identity: \"You're becoming someone who [does this thing] every day.\"\n- Keep achievement descriptions abstract — no real company names, client names, or source code.\n- XP guidelines: 5-15 for trivial tasks, 20-50 for normal work, 50-100 for significant accomplishments, 100-200 for major milestones, 200-500 for exceptional achievements.`,\n },\n },\n ],\n }),\n );\n\n return server;\n}\n","const CATEGORY_COOLDOWN_MS = 60_000;\nconst SESSION_MAX_ACHIEVEMENTS = 30;\n\ninterface RateEntry {\n category: string;\n timestamp: number;\n}\n\nconst recentAchievements: RateEntry[] = [];\n\nexport function checkRateLimit(category: string): { allowed: boolean; reason?: string } {\n const now = Date.now();\n\n // Check session limit\n if (recentAchievements.length >= SESSION_MAX_ACHIEVEMENTS) {\n return {\n allowed: false,\n reason: `Session limit reached (${SESSION_MAX_ACHIEVEMENTS} achievements). Start a new session to continue.`,\n };\n }\n\n // Check category cooldown\n const lastSameCategory = recentAchievements\n .filter((e) => e.category === category)\n .sort((a, b) => b.timestamp - a.timestamp)[0];\n\n if (lastSameCategory && now - lastSameCategory.timestamp < CATEGORY_COOLDOWN_MS) {\n const waitSeconds = Math.ceil(\n (CATEGORY_COOLDOWN_MS - (now - lastSameCategory.timestamp)) / 1000\n );\n return {\n allowed: false,\n reason: `Same category cooldown: wait ${waitSeconds}s before recording another \"${category}\" achievement.`,\n };\n }\n\n return { allowed: true };\n}\n\nexport function recordRateEntry(category: string): void {\n recentAchievements.push({ category, timestamp: Date.now() });\n}\n\nexport function resetRateLimiter(): void {\n recentAchievements.length = 0;\n}\n","import { createClient } from '@supabase/supabase-js';\nimport { loadTokens, saveTokens, clearTokens } from './keychain.js';\nimport { startOAuthCallbackServer } from './oauth-server.js';\nimport { generateCodeVerifier, generateCodeChallenge } from './pkce.js';\nimport { CONFIG } from '../utils/config.js';\nimport { log, logError } from '../utils/logger.js';\n\nlet cachedAccessToken: string | null = null;\nlet tokenExpiresAt: number = 0;\n\n/**\n * Get a valid access token. Will:\n * 1. Return cached token if still valid\n * 2. Try to refresh from stored refresh token\n * 3. Initiate full OAuth login flow if needed\n */\nexport async function getValidToken(): Promise<string> {\n // Check memory cache\n if (cachedAccessToken && Date.now() < tokenExpiresAt - 60_000) {\n return cachedAccessToken;\n }\n\n // Try stored tokens\n const stored = loadTokens();\n if (stored) {\n if (Date.now() < stored.expires_at - 60_000) {\n cachedAccessToken = stored.access_token;\n tokenExpiresAt = stored.expires_at;\n return stored.access_token;\n }\n\n // Try refresh\n if (stored.refresh_token) {\n try {\n const refreshed = await refreshToken(stored.refresh_token);\n if (refreshed) return refreshed;\n } catch (error) {\n logError('Token refresh failed:', error);\n }\n }\n }\n\n // Full login required\n return await login();\n}\n\nasync function refreshToken(refreshTokenValue: string): Promise<string | null> {\n const supabase = createClient(CONFIG.SUPABASE_URL, CONFIG.SUPABASE_ANON_KEY);\n const { data, error } = await supabase.auth.refreshSession({\n refresh_token: refreshTokenValue,\n });\n\n if (error || !data.session) {\n logError('Refresh failed:', error?.message);\n return null;\n }\n\n const expiresAt = Date.now() + (data.session.expires_in ?? 3600) * 1000;\n cachedAccessToken = data.session.access_token;\n tokenExpiresAt = expiresAt;\n\n saveTokens({\n access_token: data.session.access_token,\n refresh_token: data.session.refresh_token ?? refreshTokenValue,\n expires_at: expiresAt,\n });\n\n log('Token refreshed successfully');\n return data.session.access_token;\n}\n\nasync function login(): Promise<string> {\n if (!CONFIG.SUPABASE_URL || !CONFIG.SUPABASE_ANON_KEY) {\n throw new Error(\n 'LevelUp.log is not configured. Run `npx @levelup-log/mcp-server init` to set up.'\n );\n }\n\n const supabase = createClient(CONFIG.SUPABASE_URL, CONFIG.SUPABASE_ANON_KEY);\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = generateCodeChallenge(codeVerifier);\n\n // Start callback server before opening browser\n const callbackPromise = startOAuthCallbackServer();\n\n const redirectTo = `http://127.0.0.1:${CONFIG.AUTH_PORT}/callback`;\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'google',\n options: {\n redirectTo,\n queryParams: {\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n },\n },\n });\n\n if (error || !data.url) {\n throw new Error(`Failed to initiate OAuth: ${error?.message ?? 'No URL returned'}`);\n }\n\n // Open browser\n const open = await import('open');\n await open.default(data.url);\n console.error('Opening browser for Google login...');\n\n // Wait for callback\n const result = await callbackPromise;\n const expiresAt = Date.now() + result.expires_in * 1000;\n\n cachedAccessToken = result.access_token;\n tokenExpiresAt = expiresAt;\n\n saveTokens({\n access_token: result.access_token,\n refresh_token: result.refresh_token,\n expires_at: expiresAt,\n });\n\n log('Login successful');\n return result.access_token;\n}\n\nexport function isAuthenticated(): boolean {\n const stored = loadTokens();\n return !!(stored && Date.now() < stored.expires_at - 60_000);\n}\n\nexport function logout(): void {\n cachedAccessToken = null;\n tokenExpiresAt = 0;\n clearTokens();\n log('Logged out');\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { log, logError } from '../utils/logger.js';\n\nconst CREDENTIALS_DIR = join(homedir(), '.levelup');\nconst CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');\n\ninterface StoredTokens {\n access_token: string;\n refresh_token: string;\n expires_at: number;\n}\n\nexport function loadTokens(): StoredTokens | null {\n try {\n if (!existsSync(CREDENTIALS_FILE)) return null;\n const data = readFileSync(CREDENTIALS_FILE, 'utf-8');\n const tokens = JSON.parse(data) as StoredTokens;\n log('Loaded tokens from', CREDENTIALS_FILE);\n return tokens;\n } catch (error) {\n logError('Failed to load tokens:', error);\n return null;\n }\n}\n\nexport function saveTokens(tokens: StoredTokens): void {\n try {\n if (!existsSync(CREDENTIALS_DIR)) {\n mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });\n }\n writeFileSync(CREDENTIALS_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });\n log('Saved tokens to', CREDENTIALS_FILE);\n } catch (error) {\n logError('Failed to save tokens:', error);\n }\n}\n\nexport function clearTokens(): void {\n try {\n if (existsSync(CREDENTIALS_FILE)) {\n writeFileSync(CREDENTIALS_FILE, '{}', { mode: 0o600 });\n log('Cleared tokens');\n }\n } catch (error) {\n logError('Failed to clear tokens:', error);\n }\n}\n","import { CONFIG } from './config.js';\n\nexport function log(...args: unknown[]): void {\n if (CONFIG.DEBUG) {\n console.error('[levelup]', ...args);\n }\n}\n\nexport function logError(...args: unknown[]): void {\n console.error('[levelup:error]', ...args);\n}\n","import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { URL } from 'node:url';\nimport { CONFIG } from '../utils/config.js';\nimport { log } from '../utils/logger.js';\n\ninterface OAuthResult {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n}\n\n/**\n * Start a temporary localhost HTTP server to receive the OAuth callback.\n * Opens browser → Google OAuth → redirect to localhost:PORT/callback → extract tokens → close server.\n */\nexport function startOAuthCallbackServer(): Promise<OAuthResult> {\n return new Promise((resolve, reject) => {\n const port = CONFIG.AUTH_PORT;\n let server: Server;\n const timeout = setTimeout(() => {\n server?.close();\n reject(new Error('OAuth login timed out after 5 minutes'));\n }, 5 * 60 * 1000);\n\n server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`);\n\n if (url.pathname === '/callback') {\n // Supabase redirects with fragment (#), but we need query params\n // The frontend redirect page will forward fragment params as query params\n const accessToken = url.searchParams.get('access_token');\n const refreshToken = url.searchParams.get('refresh_token');\n const expiresIn = url.searchParams.get('expires_in');\n\n if (accessToken && refreshToken) {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body style=\"font-family: system-ui; text-align: center; padding: 50px; background: #0a0a0a; color: #e5e5e5;\">\n <h1>LevelUp.log</h1>\n <p style=\"color: #34d399; font-size: 1.5em;\">Login successful!</p>\n <p>You can close this window and return to your LLM tool.</p>\n </body>\n </html>\n `);\n\n clearTimeout(timeout);\n server.close();\n resolve({\n access_token: accessToken,\n refresh_token: refreshToken,\n expires_in: parseInt(expiresIn || '3600', 10),\n });\n } else {\n // Serve a page that extracts fragment params and redirects as query params\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <script>\n const hash = window.location.hash.substring(1);\n if (hash) {\n window.location.href = '/callback?' + hash;\n } else {\n document.body.innerHTML = '<p>Login failed. No tokens received.</p>';\n }\n </script>\n </body>\n </html>\n `);\n }\n } else {\n res.writeHead(404);\n res.end('Not found');\n }\n });\n\n server.listen(port, '127.0.0.1', () => {\n log(`OAuth callback server listening on http://127.0.0.1:${port}`);\n });\n\n server.on('error', (err) => {\n clearTimeout(timeout);\n reject(new Error(`Failed to start OAuth server on port ${port}: ${err.message}`));\n });\n });\n}\n","import { randomBytes, createHash } from 'node:crypto';\n\nexport function generateCodeVerifier(): string {\n return randomBytes(32).toString('base64url');\n}\n\nexport function generateCodeChallenge(verifier: string): string {\n return createHash('sha256').update(verifier).digest('base64url');\n}\n","import { CONFIG } from './config.js';\nimport { getValidToken } from '../auth/manager.js';\nimport { logError } from './logger.js';\n\ninterface ApiResponse<T = unknown> {\n data?: T;\n error?: string;\n status: number;\n}\n\nexport async function apiGet<T = unknown>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>> {\n const token = await getValidToken();\n const url = new URL(`${CONFIG.SUPABASE_URL}/functions/v1/${path}`);\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n url.searchParams.set(k, v);\n }\n }\n\n try {\n const res = await fetch(url.toString(), {\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n apikey: CONFIG.SUPABASE_ANON_KEY,\n },\n });\n\n const body = await res.json();\n if (!res.ok) {\n return { error: body.error || `HTTP ${res.status}`, status: res.status };\n }\n return { data: body as T, status: res.status };\n } catch (error) {\n logError('API GET error:', error);\n return { error: (error as Error).message, status: 0 };\n }\n}\n\nexport async function apiPost<T = unknown>(path: string, body: unknown): Promise<ApiResponse<T>> {\n const token = await getValidToken();\n const url = `${CONFIG.SUPABASE_URL}/functions/v1/${path}`;\n\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n apikey: CONFIG.SUPABASE_ANON_KEY,\n },\n body: JSON.stringify(body),\n });\n\n const data = await res.json();\n if (!res.ok) {\n return { error: data.error || `HTTP ${res.status}`, status: res.status };\n }\n return { data: data as T, status: res.status };\n } catch (error) {\n logError('API POST error:', error);\n return { error: (error as Error).message, status: 0 };\n }\n}\n"],"mappings":"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","names":["URL","URL","refreshToken","path","createServer"]}
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import {
3
3
  createServer,
4
4
  logError
5
- } from "./chunk-4GGNGOIO.js";
5
+ } from "./chunk-VJKF2CNS.js";
6
+ import "./chunk-FII2XEJ7.js";
6
7
 
7
8
  // src/cli.ts
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -14,9 +15,13 @@ async function serve() {
14
15
  console.error("LevelUp.log MCP server running on stdio");
15
16
  }
16
17
  async function init() {
17
- const { runInit } = await import("./init-GNFWFY5S.js");
18
+ const { runInit } = await import("./init-GKLMB6BS.js");
18
19
  await runInit();
19
20
  }
21
+ async function heartbeat() {
22
+ const { main: main2 } = await import("./heartbeat-A4ZMVGSV.js");
23
+ await main2();
24
+ }
20
25
  async function main() {
21
26
  switch (command) {
22
27
  case "serve":
@@ -26,9 +31,12 @@ async function main() {
26
31
  case "init":
27
32
  await init();
28
33
  break;
34
+ case "heartbeat":
35
+ await heartbeat();
36
+ break;
29
37
  default:
30
38
  console.error(`Unknown command: ${command}`);
31
- console.error("Usage: levelup-log [serve|init]");
39
+ console.error("Usage: levelup-log [serve|init|heartbeat]");
32
40
  process.exit(1);
33
41
  }
34
42
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { logError } from \"./utils/logger.js\";\n\nconst command = process.argv[2];\n\nasync function serve() {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\"LevelUp.log MCP server running on stdio\");\n}\n\nasync function init() {\n const { runInit } = await import(\"./init/index.js\");\n await runInit();\n}\n\nasync function main() {\n switch (command) {\n case \"serve\":\n case undefined:\n await serve();\n break;\n case \"init\":\n await init();\n break;\n default:\n console.error(`Unknown command: ${command}`);\n console.error(\"Usage: levelup-log [serve|init]\");\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n logError(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":";;;;;;;AAAA,SAAS,4BAA4B;AAIrC,IAAM,UAAU,QAAQ,KAAK,CAAC;AAE9B,eAAe,QAAQ;AACrB,QAAM,SAAS,aAAa;AAC5B,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAC9B,UAAQ,MAAM,yCAAyC;AACzD;AAEA,eAAe,OAAO;AACpB,QAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAiB;AAClD,QAAM,QAAQ;AAChB;AAEA,eAAe,OAAO;AACpB,UAAQ,SAAS;AAAA,IACf,KAAK;AAAA,IACL,KAAK;AACH,YAAM,MAAM;AACZ;AAAA,IACF,KAAK;AACH,YAAM,KAAK;AACX;AAAA,IACF;AACE,cAAQ,MAAM,oBAAoB,OAAO,EAAE;AAC3C,cAAQ,MAAM,iCAAiC;AAC/C,cAAQ,KAAK,CAAC;AAAA,EAClB;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,WAAS,gBAAgB,KAAK;AAC9B,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\nimport { logError } from \"./utils/logger.js\";\n\nconst command = process.argv[2];\n\nasync function serve() {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\"LevelUp.log MCP server running on stdio\");\n}\n\nasync function init() {\n const { runInit } = await import(\"./init/index.js\");\n await runInit();\n}\n\nasync function heartbeat() {\n const { main } = await import(\"./scripts/heartbeat.js\");\n await main();\n}\n\nasync function main() {\n switch (command) {\n case \"serve\":\n case undefined:\n await serve();\n break;\n case \"init\":\n await init();\n break;\n case \"heartbeat\":\n await heartbeat();\n break;\n default:\n console.error(`Unknown command: ${command}`);\n console.error(\"Usage: levelup-log [serve|init|heartbeat]\");\n process.exit(1);\n }\n}\n\nmain().catch((error) => {\n logError(\"Fatal error:\", error);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;AAAA,SAAS,4BAA4B;AAIrC,IAAM,UAAU,QAAQ,KAAK,CAAC;AAE9B,eAAe,QAAQ;AACrB,QAAM,SAAS,aAAa;AAC5B,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAC9B,UAAQ,MAAM,yCAAyC;AACzD;AAEA,eAAe,OAAO;AACpB,QAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAiB;AAClD,QAAM,QAAQ;AAChB;AAEA,eAAe,YAAY;AACzB,QAAM,EAAE,MAAAA,MAAK,IAAI,MAAM,OAAO,yBAAwB;AACtD,QAAMA,MAAK;AACb;AAEA,eAAe,OAAO;AACpB,UAAQ,SAAS;AAAA,IACf,KAAK;AAAA,IACL,KAAK;AACH,YAAM,MAAM;AACZ;AAAA,IACF,KAAK;AACH,YAAM,KAAK;AACX;AAAA,IACF,KAAK;AACH,YAAM,UAAU;AAChB;AAAA,IACF;AACE,cAAQ,MAAM,oBAAoB,OAAO,EAAE;AAC3C,cAAQ,MAAM,2CAA2C;AACzD,cAAQ,KAAK,CAAC;AAAA,EAClB;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,UAAU;AACtB,WAAS,gBAAgB,KAAK;AAC9B,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":["main"]}
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG
4
+ } from "./chunk-FII2XEJ7.js";
5
+
6
+ // src/scripts/heartbeat.ts
7
+ import { createClient } from "@supabase/supabase-js";
8
+ import { execSync } from "child_process";
9
+ import { mkdirSync, writeFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ function hr(char = "\u2500", width = 60) {
13
+ console.log(char.repeat(width));
14
+ }
15
+ function section(title) {
16
+ console.log();
17
+ hr();
18
+ console.log(` ${title}`);
19
+ hr();
20
+ }
21
+ function pad(s, n) {
22
+ return String(s).padEnd(n);
23
+ }
24
+ function nDaysAgo(n) {
25
+ const d = /* @__PURE__ */ new Date();
26
+ d.setDate(d.getDate() - n);
27
+ return d.toISOString();
28
+ }
29
+ function formatDate(iso) {
30
+ return new Date(iso).toLocaleDateString("zh-TW", {
31
+ year: "numeric",
32
+ month: "2-digit",
33
+ day: "2-digit"
34
+ });
35
+ }
36
+ function formatDatetime(iso) {
37
+ return new Date(iso).toLocaleString("zh-TW", {
38
+ year: "numeric",
39
+ month: "2-digit",
40
+ day: "2-digit",
41
+ hour: "2-digit",
42
+ minute: "2-digit"
43
+ });
44
+ }
45
+ function getISOWeek(date) {
46
+ const d = new Date(
47
+ Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
48
+ );
49
+ const day = d.getUTCDay() || 7;
50
+ d.setUTCDate(d.getUTCDate() + 4 - day);
51
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
52
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
53
+ }
54
+ async function weeklyStats(db) {
55
+ section("\u{1F4CA} \u96D9\u9031\u5831\uFF08\u904E\u53BB 14 \u5929\uFF09");
56
+ const since = nDaysAgo(14);
57
+ const { count: totalUsers } = await db.from("profiles").select("*", { count: "exact", head: true });
58
+ const { data: activeUsersData } = await db.from("achievements").select("user_id").gte("created_at", since);
59
+ const activeThisWeek = new Set(activeUsersData?.map((r) => r.user_id)).size;
60
+ const { data: weekAchievements } = await db.from("achievements").select("xp, category").gte("created_at", since);
61
+ const weekCount = weekAchievements?.length ?? 0;
62
+ const weekXp = weekAchievements?.reduce((sum, a) => sum + a.xp, 0) ?? 0;
63
+ const catCounts = {};
64
+ for (const a of weekAchievements ?? []) {
65
+ catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;
66
+ }
67
+ const topCategories = Object.entries(catCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([cat, count]) => ({ cat, count }));
68
+ console.log(` \u{1F465} \u7E3D\u7528\u6236\u6578\uFF1A${totalUsers ?? 0}`);
69
+ console.log(` \u{1F525} \u96D9\u9031\u6D3B\u8E8D\uFF1A${activeThisWeek} \u4EBA`);
70
+ console.log(` \u{1F3C5} \u96D9\u9031\u6210\u5C31\uFF1A${weekCount} \u9805`);
71
+ console.log(` \u26A1 \u96D9\u9031 XP\uFF1A${weekXp.toLocaleString()}`);
72
+ if (topCategories.length > 0) {
73
+ console.log();
74
+ console.log(" \u672C\u9031\u71B1\u9580\u985E\u5225\uFF1A");
75
+ for (const { cat, count } of topCategories) {
76
+ const bar = "\u2588".repeat(
77
+ Math.round(count / (topCategories[0].count || 1) * 20)
78
+ );
79
+ console.log(` ${pad(cat, 12)} ${pad(count, 4)} ${bar}`);
80
+ }
81
+ }
82
+ return {
83
+ totalUsers: totalUsers ?? 0,
84
+ activeThisWeek,
85
+ weekAchievements: weekCount,
86
+ weekXp,
87
+ topCategories
88
+ };
89
+ }
90
+ async function streakHealth(db) {
91
+ section("\u{1F525} Streak \u5065\u5EB7\u6AA2\u67E5");
92
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
93
+ const yesterday = /* @__PURE__ */ new Date();
94
+ yesterday.setDate(yesterday.getDate() - 1);
95
+ const yesterdayStr = yesterday.toISOString().split("T")[0];
96
+ const { data: profiles } = await db.from("profiles").select("current_streak, last_active_date").gt("current_streak", 0);
97
+ if (!profiles || profiles.length === 0) {
98
+ console.log(" \u76EE\u524D\u7121\u6D3B\u8E8D streak");
99
+ return { healthy: 0, atRisk: 0, over30: 0, over7: 0 };
100
+ }
101
+ let healthy = 0, atRisk = 0, over30 = 0, over7 = 0;
102
+ for (const p of profiles) {
103
+ const isHealthy = p.last_active_date === today || p.last_active_date === yesterdayStr;
104
+ if (isHealthy) {
105
+ healthy++;
106
+ if (p.current_streak >= 30) over30++;
107
+ else if (p.current_streak >= 7) over7++;
108
+ } else {
109
+ atRisk++;
110
+ }
111
+ }
112
+ console.log(` \u2705 \u5065\u5EB7 streak\uFF1A${healthy} \u4EBA`);
113
+ console.log(` \u2514\u2500\u2500 30 \u5929\u4EE5\u4E0A\uFF1A${over30} \u4EBA`);
114
+ console.log(` \u2514\u2500\u2500 7-29 \u5929\uFF1A${over7} \u4EBA`);
115
+ if (atRisk > 0) {
116
+ console.log(` \u26A0\uFE0F streak \u5DF2\u4E2D\u65B7\uFF08\u5F85 cron \u6B78\u96F6\uFF09\uFF1A${atRisk} \u4EBA`);
117
+ }
118
+ return { healthy, atRisk, over30, over7 };
119
+ }
120
+ async function endSeason(db, season) {
121
+ const { data: participants } = await db.from("season_participants").select("id, season_xp").eq("season_id", season.id).order("season_xp", { ascending: false });
122
+ for (let i = 0; i < (participants ?? []).length; i++) {
123
+ await db.from("season_participants").update({ final_rank: i + 1 }).eq("id", participants[i].id);
124
+ }
125
+ await db.from("seasons").update({ is_active: false }).eq("id", season.id);
126
+ console.log(
127
+ ` \u2705 \u8CFD\u5B63\u300C${season.name}\u300D\u5DF2\u7D50\u7B97\uFF0C\u5171 ${participants?.length ?? 0} \u4F4D`
128
+ );
129
+ }
130
+ async function createNextSeason(db, prev) {
131
+ const now = /* @__PURE__ */ new Date();
132
+ let seasonNum = 1;
133
+ if (prev) {
134
+ const match = prev.name.match(/S(\d+)/);
135
+ if (match) seasonNum = parseInt(match[1]) + 1;
136
+ }
137
+ const startsAt = prev ? new Date(prev.ends_at) : now;
138
+ const endsAt = new Date(startsAt);
139
+ endsAt.setDate(endsAt.getDate() + 90);
140
+ const name = `S${String(seasonNum).padStart(2, "0")} - ${startsAt.getFullYear()} Q${Math.ceil((startsAt.getMonth() + 1) / 3)}`;
141
+ const { data, error } = await db.from("seasons").insert({
142
+ name,
143
+ starts_at: startsAt.toISOString(),
144
+ ends_at: endsAt.toISOString(),
145
+ is_active: !prev
146
+ }).select().single();
147
+ if (error) {
148
+ console.log(` \u274C \u5EFA\u7ACB\u8CFD\u5B63\u5931\u6557\uFF1A${error.message}`);
149
+ } else {
150
+ console.log(` \u2705 \u5DF2\u5EFA\u7ACB\u8CFD\u5B63\u300C${data.name}\u300D`);
151
+ console.log(
152
+ ` ${formatDate(data.starts_at)} \u2192 ${formatDate(data.ends_at)}`
153
+ );
154
+ }
155
+ }
156
+ async function seasonManagement(db) {
157
+ section("\u{1F3C6} \u8CFD\u5B63\u7BA1\u7406");
158
+ const now = /* @__PURE__ */ new Date();
159
+ const { data: seasons } = await db.from("seasons").select("*").order("starts_at", { ascending: false });
160
+ if (!seasons || seasons.length === 0) {
161
+ console.log(" \u26A0\uFE0F \u5C1A\u7121\u8CFD\u5B63\u8CC7\u6599\uFF0C\u5EFA\u7ACB\u7B2C\u4E00\u500B\u8CFD\u5B63...");
162
+ await createNextSeason(db, null);
163
+ return { status: "created", name: "S01" };
164
+ }
165
+ const active = seasons.find((s) => s.is_active);
166
+ if (active) {
167
+ const endsAt = new Date(active.ends_at);
168
+ const daysLeft = Math.ceil(
169
+ (endsAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)
170
+ );
171
+ if (daysLeft < 0) {
172
+ console.log(
173
+ ` \u23F0 \u8CFD\u5B63\u300C${active.name}\u300D\u5DF2\u904E\u671F ${-daysLeft} \u5929\uFF0C\u6B63\u5728\u7D50\u7B97...`
174
+ );
175
+ await endSeason(db, active);
176
+ await createNextSeason(db, active);
177
+ return { status: "expired_fixed", name: active.name };
178
+ }
179
+ if (daysLeft <= 7) {
180
+ console.log(
181
+ ` \u26A0\uFE0F \u8CFD\u5B63\u300C${active.name}\u300D\u9084\u6709 ${daysLeft} \u5929\u7D50\u675F\uFF08${formatDate(active.ends_at)}\uFF09`
182
+ );
183
+ const nextExists = seasons.some(
184
+ (s) => !s.is_active && new Date(s.starts_at) > new Date(active.ends_at)
185
+ );
186
+ if (!nextExists) {
187
+ console.log(" \u2795 \u9810\u5148\u5EFA\u7ACB\u4E0B\u4E00\u500B\u8CFD\u5B63...");
188
+ await createNextSeason(db, active);
189
+ }
190
+ } else {
191
+ console.log(` \u2705 \u8CFD\u5B63\u300C${active.name}\u300D\u9032\u884C\u4E2D`);
192
+ console.log(
193
+ ` ${formatDate(active.starts_at)} \u2192 ${formatDate(active.ends_at)}\uFF08\u9084\u6709 ${daysLeft} \u5929\uFF09`
194
+ );
195
+ }
196
+ const { count: participants } = await db.from("season_participants").select("*", { count: "exact", head: true }).eq("season_id", active.id);
197
+ console.log(` \u53C3\u8CFD\u4EBA\u6578\uFF1A${participants ?? 0} \u4EBA`);
198
+ return {
199
+ status: "active",
200
+ name: active.name,
201
+ daysLeft,
202
+ participants: participants ?? 0,
203
+ endsAt: active.ends_at
204
+ };
205
+ }
206
+ const latest = seasons[0];
207
+ if (latest && new Date(latest.starts_at) <= now) {
208
+ console.log(` \u25B6\uFE0F \u555F\u52D5\u8CFD\u5B63\u300C${latest.name}\u300D...`);
209
+ await db.from("seasons").update({ is_active: true }).eq("id", latest.id);
210
+ return { status: "active", name: latest.name };
211
+ }
212
+ console.log(" \u26A0\uFE0F \u7121\u6D3B\u8E8D\u8CFD\u5B63");
213
+ if (latest)
214
+ console.log(
215
+ ` \u300C${latest.name}\u300D\u5C07\u65BC ${formatDatetime(latest.starts_at)} \u958B\u59CB`
216
+ );
217
+ return { status: "none", name: "" };
218
+ }
219
+ async function titleDistribution(db) {
220
+ section("\u{1F3C5} \u7A31\u865F\u89E3\u9396\u5206\u5E03");
221
+ const { data: titles } = await db.from("title_definitions").select("id, name, rarity, icon");
222
+ if (!titles || titles.length === 0) {
223
+ console.log(" \u5C1A\u7121\u7A31\u865F\u5B9A\u7FA9");
224
+ return [];
225
+ }
226
+ const { data: unlocks } = await db.from("user_titles").select("title_id");
227
+ const titleCounts = {};
228
+ for (const u of unlocks ?? []) {
229
+ titleCounts[u.title_id] = (titleCounts[u.title_id] ?? 0) + 1;
230
+ }
231
+ const rarityOrder = ["common", "uncommon", "rare", "epic", "legendary"];
232
+ const sorted = [...titles].sort(
233
+ (a, b) => rarityOrder.indexOf(a.rarity) - rarityOrder.indexOf(b.rarity)
234
+ );
235
+ const rarityEmoji = {
236
+ common: "\u26AA",
237
+ uncommon: "\u{1F7E2}",
238
+ rare: "\u{1F535}",
239
+ epic: "\u{1F7E3}",
240
+ legendary: "\u{1F7E1}"
241
+ };
242
+ let currentRarity = "";
243
+ for (const t of sorted) {
244
+ if (t.rarity !== currentRarity) {
245
+ currentRarity = t.rarity;
246
+ console.log(`
247
+ ${rarityEmoji[t.rarity]} ${t.rarity.toUpperCase()}`);
248
+ }
249
+ const cnt = titleCounts[t.id] ?? 0;
250
+ console.log(
251
+ ` ${t.icon ?? " "} ${pad(t.name, 18)} ${pad(cnt, 4)} \u4EBA\u89E3\u9396`
252
+ );
253
+ }
254
+ return sorted.map((t) => ({
255
+ icon: t.icon,
256
+ name: t.name,
257
+ rarity: t.rarity,
258
+ unlockCount: titleCounts[t.id] ?? 0
259
+ }));
260
+ }
261
+ async function yearlySnapshotIfNeeded(db) {
262
+ const now = /* @__PURE__ */ new Date();
263
+ if (now.getMonth() !== 0 || now.getDate() !== 1) return;
264
+ section("\u{1F4C5} \u5E74\u5EA6\u5FEB\u7167\uFF081/1 \u89F8\u767C\uFF09");
265
+ const lastYear = now.getFullYear() - 1;
266
+ const { data: profiles } = await db.from("profiles").select("id, birth_date, year_xp, longest_streak");
267
+ let ok = 0, err = 0;
268
+ for (const p of profiles ?? []) {
269
+ const ageLevel = p.birth_date ? Math.floor(
270
+ (Date.now() - new Date(p.birth_date).getTime()) / (365.25 * 24 * 60 * 60 * 1e3)
271
+ ) : 0;
272
+ const { count: achievementsCount } = await db.from("achievements").select("*", { count: "exact", head: true }).eq("user_id", p.id).gte("created_at", `${lastYear}-01-01`).lt("created_at", `${lastYear + 1}-01-01`);
273
+ const { data: catData } = await db.from("achievements").select("category").eq("user_id", p.id).gte("created_at", `${lastYear}-01-01`).lt("created_at", `${lastYear + 1}-01-01`);
274
+ const catCounts = {};
275
+ for (const a of catData ?? [])
276
+ catCounts[a.category] = (catCounts[a.category] ?? 0) + 1;
277
+ const topCategories = Object.entries(catCounts).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([category, count]) => ({ category, count }));
278
+ const { count: titlesUnlocked } = await db.from("user_titles").select("*", { count: "exact", head: true }).eq("user_id", p.id);
279
+ const { error } = await db.from("yearly_snapshots").upsert(
280
+ {
281
+ user_id: p.id,
282
+ year: lastYear,
283
+ age_level: ageLevel,
284
+ year_xp: p.year_xp,
285
+ achievements_count: achievementsCount ?? 0,
286
+ top_categories: topCategories,
287
+ longest_streak: p.longest_streak,
288
+ titles_unlocked: titlesUnlocked ?? 0
289
+ },
290
+ { onConflict: "user_id,year", ignoreDuplicates: false }
291
+ );
292
+ if (error) err++;
293
+ else ok++;
294
+ }
295
+ await db.from("profiles").update({ year_xp: 0, achievements_this_month: 0 });
296
+ console.log(` \u2705 \u5E74\u5EA6\u5FEB\u7167\uFF1A${ok} \u4EBA\u5B8C\u6210\uFF0C${err} \u500B\u932F\u8AA4`);
297
+ console.log(" \u2705 year_xp \u5DF2\u6B78\u96F6");
298
+ }
299
+ async function saveToObsidian(stats, streak, season, titles) {
300
+ const now = /* @__PURE__ */ new Date();
301
+ const year = now.getFullYear();
302
+ const week = getISOWeek(now);
303
+ const dateStr = now.toLocaleDateString("zh-TW", {
304
+ year: "numeric",
305
+ month: "2-digit",
306
+ day: "2-digit"
307
+ });
308
+ const week2 = week + 1;
309
+ const periodLabel = `${year}-W${String(week).padStart(2, "0")}-W${String(week2).padStart(2, "0")}`;
310
+ const vaultDir = join(homedir(), "Documents/tai");
311
+ const reportDir = join(vaultDir, "\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831");
312
+ const fileName = `${periodLabel}.md`;
313
+ const filePath = join(reportDir, fileName);
314
+ const rarityOrder = ["common", "uncommon", "rare", "epic", "legendary"];
315
+ const rarityLabel = {
316
+ common: "\u26AA Common",
317
+ uncommon: "\u{1F7E2} Uncommon",
318
+ rare: "\u{1F535} Rare",
319
+ epic: "\u{1F7E3} Epic",
320
+ legendary: "\u{1F7E1} Legendary"
321
+ };
322
+ const titlesByRarity = rarityOrder.map((r) => {
323
+ const items = titles.filter((t) => t.rarity === r);
324
+ if (items.length === 0) return "";
325
+ const rows = items.map((t) => `| ${t.icon ?? "\u2014"} | ${t.name} | ${t.unlockCount} |`).join("\n");
326
+ return `**${rarityLabel[r]}**
327
+
328
+ | \u5716\u793A | \u7A31\u865F | \u89E3\u9396\u4EBA\u6578 |
329
+ |------|------|----------|
330
+ ${rows}`;
331
+ }).filter(Boolean).join("\n\n");
332
+ const catRows = stats.topCategories.map(({ cat, count }) => `| ${cat} | ${count} |`).join("\n");
333
+ const catTable = stats.topCategories.length > 0 ? `| \u985E\u5225 | \u6210\u5C31\u6578 |
334
+ |------|--------|
335
+ ${catRows}` : "_\u672C\u9031\u7121\u8A18\u9304_";
336
+ const seasonLine = season.status === "active" ? `${season.name}\uFF0C\u9084\u6709 ${season.daysLeft} \u5929\uFF08${season.participants ?? 0} \u4F4D\u53C3\u8CFD\u8005\uFF09` : season.status === "expired_fixed" ? `${season.name} \u5DF2\u7D50\u7B97\uFF0C\u65B0\u8CFD\u5B63\u5DF2\u5EFA\u7ACB` : season.status === "created" ? "\u5DF2\u5EFA\u7ACB\u7B2C\u4E00\u500B\u8CFD\u5B63" : "\u7121\u6D3B\u8E8D\u8CFD\u5B63";
337
+ const md = `---
338
+ public: false
339
+ date: ${now.toISOString().split("T")[0]}
340
+ tags: [levelup-log, \u96D9\u9031\u5831]
341
+ ---
342
+
343
+ # LevelUp.log \u96D9\u9031\u5831 ${periodLabel}
344
+
345
+ > \u7522\u751F\u6642\u9593\uFF1A${dateStr}\uFF08\u6DB5\u84CB\u904E\u53BB 14 \u5929\uFF09
346
+
347
+ ## \u{1F4CA} \u96D9\u9031\u6578\u64DA
348
+
349
+ | \u6307\u6A19 | \u6578\u503C |
350
+ |------|------|
351
+ | \u7E3D\u7528\u6236\u6578 | ${stats.totalUsers} |
352
+ | \u96D9\u9031\u6D3B\u8E8D | ${stats.activeThisWeek} \u4EBA |
353
+ | \u96D9\u9031\u6210\u5C31 | ${stats.weekAchievements} \u9805 |
354
+ | \u96D9\u9031 XP | ${stats.weekXp.toLocaleString()} |
355
+
356
+ ### \u71B1\u9580\u985E\u5225
357
+
358
+ ${catTable}
359
+
360
+ ## \u{1F525} Streak \u5065\u5EB7
361
+
362
+ | \u6307\u6A19 | \u6578\u503C |
363
+ |------|------|
364
+ | \u5065\u5EB7 streak | ${streak.healthy} \u4EBA |
365
+ | 30 \u5929\u4EE5\u4E0A | ${streak.over30} \u4EBA |
366
+ | 7-29 \u5929 | ${streak.over7} \u4EBA |
367
+ | \u5DF2\u4E2D\u65B7\uFF08\u5F85\u6B78\u96F6\uFF09 | ${streak.atRisk} \u4EBA |
368
+
369
+ ## \u{1F3C6} \u8CFD\u5B63
370
+
371
+ ${seasonLine}
372
+
373
+ ## \u{1F3C5} \u7A31\u865F\u89E3\u9396\u5206\u5E03
374
+
375
+ ${titlesByRarity || "_\u7121\u7A31\u865F\u8CC7\u6599_"}
376
+ `;
377
+ mkdirSync(reportDir, { recursive: true });
378
+ writeFileSync(filePath, md, "utf-8");
379
+ console.log();
380
+ console.log(
381
+ ` \u{1F4DD} Obsidian \u96D9\u9031\u5831\u5DF2\u5132\u5B58\uFF1A\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831/${fileName}`
382
+ );
383
+ }
384
+ function saveToCalendar(stats, season, week, year) {
385
+ section("\u{1F4C5} Google \u65E5\u66C6");
386
+ try {
387
+ execSync("which gog", { stdio: "ignore" });
388
+ } catch {
389
+ console.log(" \u26A0\uFE0F gog \u672A\u5B89\u88DD\uFF0C\u7565\u904E\u65E5\u66C6\u6574\u5408");
390
+ console.log(" \u5B89\u88DD\uFF1Abrew install steipete/tap/gogcli");
391
+ return;
392
+ }
393
+ const now = /* @__PURE__ */ new Date();
394
+ const todayDate = now.toISOString().split("T")[0];
395
+ const w1 = String(week).padStart(2, "0");
396
+ const w2 = String(week + 1).padStart(2, "0");
397
+ const periodLabel = `${year}-W${w1}-W${w2}`;
398
+ const title = `LevelUp.log ${periodLabel} \u2014 ${stats.activeThisWeek} \u6D3B\u8E8D \xB7 ${stats.weekAchievements} \u6210\u5C31 \xB7 ${stats.weekXp.toLocaleString()} XP`;
399
+ const startTime = `${todayDate}T09:00`;
400
+ const endTime = `${todayDate}T09:30`;
401
+ const topCatText = stats.topCategories.map(({ cat, count }) => ` ${cat}: ${count}`).join("\n");
402
+ const seasonText = season.status === "active" ? `${season.name}\uFF0C\u9084\u6709 ${season.daysLeft} \u5929` : season.name || "\u7121\u6D3B\u8E8D\u8CFD\u5B63";
403
+ const description = `LevelUp.log \u96D9\u9031\u5831 ${periodLabel}
404
+
405
+ \u7528\u6236\uFF1A${stats.totalUsers} \u6D3B\u8E8D\uFF1A${stats.activeThisWeek}
406
+ \u6210\u5C31\uFF1A${stats.weekAchievements} XP\uFF1A${stats.weekXp.toLocaleString()}
407
+
408
+ \u71B1\u9580\u985E\u5225\uFF1A
409
+ ${topCatText || " \uFF08\u7121\uFF09"}
410
+
411
+ \u8CFD\u5B63\uFF1A${seasonText}
412
+
413
+ Obsidian\uFF1A\u5C08\u6848/LevelUp.log/\u96D9\u9031\u5831/${periodLabel}.md`;
414
+ try {
415
+ execSync(
416
+ `gog calendar create --title ${JSON.stringify(title)} --start "${startTime}" --end "${endTime}" --description ${JSON.stringify(description)} --color 9`,
417
+ { stdio: "pipe" }
418
+ );
419
+ console.log(` \u2705 \u65E5\u66C6\u4E8B\u4EF6\u5DF2\u5EFA\u7ACB\uFF1A${title}`);
420
+ } catch (err) {
421
+ const msg = err instanceof Error ? err.message : String(err);
422
+ console.log(` \u274C \u65E5\u66C6\u5EFA\u7ACB\u5931\u6557\uFF1A${msg.split("\n")[0]}`);
423
+ }
424
+ }
425
+ async function main() {
426
+ const serviceRoleKey = process.env.LEVELUP_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
427
+ if (!serviceRoleKey) {
428
+ console.error("\u274C \u9700\u8981 LEVELUP_SERVICE_ROLE_KEY \u74B0\u5883\u8B8A\u6578");
429
+ console.error(" export LEVELUP_SERVICE_ROLE_KEY=your_service_role_key");
430
+ process.exit(1);
431
+ }
432
+ const db = createClient(CONFIG.SUPABASE_URL, serviceRoleKey, {
433
+ auth: { persistSession: false }
434
+ });
435
+ const now = /* @__PURE__ */ new Date();
436
+ console.log();
437
+ console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
438
+ console.log("\u2551 LevelUp.log Weekly Heartbeat \u2551");
439
+ console.log(`\u2551 ${now.toLocaleString("zh-TW").padEnd(50)}\u2551`);
440
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
441
+ const stats = await weeklyStats(db);
442
+ const streak = await streakHealth(db);
443
+ const season = await seasonManagement(db);
444
+ const titles = await titleDistribution(db);
445
+ await yearlySnapshotIfNeeded(db);
446
+ await saveToObsidian(stats, streak, season, titles);
447
+ saveToCalendar(stats, season, getISOWeek(now), now.getFullYear());
448
+ section("\u2705 Heartbeat \u5B8C\u6210");
449
+ console.log();
450
+ }
451
+ var isDirectRun = process.argv[1]?.endsWith("heartbeat.ts") || process.argv[1]?.endsWith("heartbeat.js");
452
+ if (isDirectRun) {
453
+ main().catch((err) => {
454
+ console.error("\u274C Heartbeat \u5931\u6557\uFF1A", err);
455
+ process.exit(1);
456
+ });
457
+ }
458
+ export {
459
+ main
460
+ };
461
+ //# sourceMappingURL=heartbeat-A4ZMVGSV.js.map