@jefuriiij/synthra 0.1.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/dashboard/server.ts","../../src/shared/logger.ts","../../src/server/port.ts","../../src/dashboard/delta.ts","../../src/shared/paths.ts","../../src/shared/pricing.ts","../../src/shared/project-registry.ts","../../src/dashboard/public/index.html","../../src/dashboard/public/style.css"],"sourcesContent":["// Standalone dashboard server. Default port 8901 (override via\n// SYN_DASHBOARD_PORT); falls back through a small range 8901–8910 if the\n// preferred port is busy (so we can coexist with other dashboards like\n// GrapeRoot's on the same machine).\n// Reads .synthra-graph/token_log.jsonl + .synthra-graph/gate_log.jsonl for the\n// given project and renders a live SPA backed by GET /data polled every 2s.\n\nimport { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\n\nimport { log } from \"../shared/logger.js\";\nimport type { SynthraPaths } from \"../shared/paths.js\";\nimport { findFreePort } from \"../server/port.js\";\nimport { computeDashboardData } from \"./delta.js\";\n\nimport indexHtml from \"./public/index.html\";\nimport styleCss from \"./public/style.css\";\n\nconst FALLBACK_RANGE = 9; // try preferredPort + [0..9]\n\nexport interface DashboardServerHandle {\n port: number;\n url: string;\n stop(): Promise<void>;\n}\n\nexport async function startDashboard(\n paths: SynthraPaths,\n preferredPort = 8901,\n): Promise<DashboardServerHandle> {\n const port = await findFreePort(preferredPort, preferredPort + FALLBACK_RANGE);\n if (port !== preferredPort) {\n log.info(\n `dashboard port ${preferredPort} was busy — bound to ${port} instead (likely another dashboard from a coexisting tool).`,\n );\n }\n const app = new Hono();\n\n app.get(\"/\", (c) => c.html(indexHtml));\n\n app.get(\"/style.css\", (c) => {\n c.header(\"Content-Type\", \"text/css; charset=utf-8\");\n c.header(\"Cache-Control\", \"no-cache\");\n return c.body(styleCss);\n });\n\n app.get(\"/health\", (c) => c.json({ ok: true }));\n\n app.get(\"/data\", async (c) => {\n const data = await computeDashboardData(paths);\n return c.json(data);\n });\n\n const nodeServer = serve({ fetch: app.fetch, port, hostname: \"127.0.0.1\" });\n\n return {\n port,\n url: `http://127.0.0.1:${port}`,\n async stop() {\n await new Promise<void>((resolve, reject) => {\n nodeServer.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n","// Minimal logger. Prefixes Synthra output with [syn].\n\ntype Level = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LEVEL_PRIORITY: Record<Level, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nlet activeLevel: Level = (process.env.SYN_LOG_LEVEL as Level) ?? \"info\";\n\nexport function setLevel(level: Level): void {\n activeLevel = level;\n}\n\nfunction shouldLog(level: Level): boolean {\n return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[activeLevel];\n}\n\nfunction emit(level: Level, msg: string, ...args: unknown[]): void {\n if (!shouldLog(level)) return;\n const stream = level === \"error\" || level === \"warn\" ? process.stderr : process.stdout;\n stream.write(`[syn] ${msg}${args.length ? \" \" + args.map(String).join(\" \") : \"\"}\\n`);\n}\n\nexport const log = {\n debug: (m: string, ...a: unknown[]) => emit(\"debug\", m, ...a),\n info: (m: string, ...a: unknown[]) => emit(\"info\", m, ...a),\n warn: (m: string, ...a: unknown[]) => emit(\"warn\", m, ...a),\n error: (m: string, ...a: unknown[]) => emit(\"error\", m, ...a),\n};\n","// Finds a free port in the 8080–8099 range. Writes the chosen port to\n// .synthra-graph/mcp_port so PowerShell/Bash hook scripts can read it.\n// TODO: M2\n\nimport { createServer } from \"node:net\";\n\nexport const PORT_RANGE_START = 8080;\nexport const PORT_RANGE_END = 8099;\n\nexport async function findFreePort(\n start = PORT_RANGE_START,\n end = PORT_RANGE_END,\n): Promise<number> {\n for (let port = start; port <= end; port++) {\n if (await isFree(port)) return port;\n }\n throw new Error(`Synthra: no free port in ${start}-${end}`);\n}\n\nfunction isFree(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const s = createServer();\n s.once(\"error\", () => resolve(false));\n s.once(\"listening\", () => s.close(() => resolve(true)));\n s.listen(port, \"127.0.0.1\");\n });\n}\n","// Reads token_log.jsonl + gate_log.jsonl for the active project AND every\n// project registered in ~/.synthra/projects.json, then computes the\n// dashboard's rendered shape: per-project + global aggregate + recent calls\n// across all projects.\n\nimport { readFile } from \"node:fs/promises\";\n\nimport { resolvePaths, type SynthraPaths } from \"../shared/paths.js\";\nimport { estimateCostUsd } from \"../shared/pricing.js\";\nimport { listProjects } from \"../shared/project-registry.js\";\n\nconst AVG_TOKENS_PER_BLOCKED_GREP = 500;\n\nexport interface TokenLogEntry {\n /** Stop-hook-supplied timestamp (preferred). */\n ts?: string;\n /** Server-side fallback added by handleLog when ts isn't provided. */\n written_at?: string;\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n model: string;\n description?: string;\n project: string;\n}\n\nexport interface GateLogEntry {\n ts: string;\n tool: string;\n decision: \"allow\" | \"block\";\n query: string | null;\n reason?: string;\n}\n\nexport interface ProjectStats {\n path: string;\n name: string;\n last_seen: string | null;\n total_turns: number;\n total_input_tokens: number;\n total_output_tokens: number;\n total_cache_read: number;\n total_cache_create: number;\n total_gate_calls: number;\n blocked_count: number;\n estimated_tokens_saved: number;\n estimated_cost_usd: number;\n models: Record<string, number>;\n}\n\nexport interface RecentTurn {\n ts: string;\n project_name: string;\n project_path: string;\n input: number;\n output: number;\n cache_read: number;\n cache_create: number;\n model: string;\n cost_usd: number;\n}\n\nexport interface RecentGate {\n ts: string;\n project_name: string;\n project_path: string;\n tool: string;\n decision: \"allow\" | \"block\";\n query: string | null;\n}\n\nexport interface DashboardData {\n active: {\n project_root: string;\n project_name: string;\n stats: ProjectStats;\n };\n global: {\n project_count: number;\n total_turns: number;\n total_input_tokens: number;\n total_output_tokens: number;\n total_cache_read: number;\n total_cache_create: number;\n total_gate_calls: number;\n blocked_count: number;\n estimated_tokens_saved: number;\n saved_percent: number;\n estimated_cost_usd: number;\n };\n projects: ProjectStats[];\n recent_turns: RecentTurn[];\n recent_gates: RecentGate[];\n}\n\nasync function readJsonl<T>(path: string): Promise<T[]> {\n try {\n const text = await readFile(path, \"utf8\");\n return text\n .split(/\\r?\\n/)\n .filter((l) => l.length > 0)\n .map((l) => {\n try {\n return JSON.parse(l) as T;\n } catch {\n return null;\n }\n })\n .filter((v): v is T => v !== null);\n } catch {\n return [];\n }\n}\n\nfunction basename(p: string): string {\n const parts = p.split(/[\\\\/]/);\n return parts[parts.length - 1] || p;\n}\n\ninterface ProjectFiles {\n path: string;\n name: string;\n last_seen: string | null;\n tokens: TokenLogEntry[];\n gates: GateLogEntry[];\n}\n\nfunction summarize(p: ProjectFiles): ProjectStats {\n let totalIn = 0;\n let totalOut = 0;\n let totalCacheRead = 0;\n let totalCacheCreate = 0;\n let costUsd = 0;\n const models: Record<string, number> = {};\n\n for (const t of p.tokens) {\n totalIn += t.input_tokens ?? 0;\n totalOut += t.output_tokens ?? 0;\n totalCacheRead += t.cache_read_input_tokens ?? 0;\n totalCacheCreate += t.cache_creation_input_tokens ?? 0;\n costUsd += estimateCostUsd(t);\n if (t.model) models[t.model] = (models[t.model] ?? 0) + 1;\n }\n\n const blocked = p.gates.filter((g) => g.decision === \"block\").length;\n const saved = blocked * AVG_TOKENS_PER_BLOCKED_GREP;\n\n return {\n path: p.path,\n name: p.name,\n last_seen: p.last_seen,\n total_turns: p.tokens.length,\n total_input_tokens: totalIn,\n total_output_tokens: totalOut,\n total_cache_read: totalCacheRead,\n total_cache_create: totalCacheCreate,\n total_gate_calls: p.gates.length,\n blocked_count: blocked,\n estimated_tokens_saved: saved,\n estimated_cost_usd: Math.round(costUsd * 100) / 100,\n models,\n };\n}\n\nasync function loadProjectFiles(\n path: string,\n name: string,\n lastSeen: string | null,\n): Promise<ProjectFiles> {\n const paths = resolvePaths(path);\n const [rawTokens, gates] = await Promise.all([\n readJsonl<TokenLogEntry>(paths.tokenLog),\n readJsonl<GateLogEntry>(paths.gateLog),\n ]);\n return { path, name, last_seen: lastSeen, tokens: dedupeTokens(rawTokens), gates };\n}\n\n/**\n * Collapse duplicate token-log entries from co-installed AI tools.\n *\n * Synthra is friendly with other tools that share the .synthra-graph/\n * token_log.jsonl shape (GrapeRoot writes to it too). Both Stop hooks\n * fire on the same turn and emit nearly-identical entries within ~10ms,\n * which double-counts every metric in the dashboard.\n *\n * Strategy: group by (project, usage counts, second-rounded timestamp);\n * inside a group, keep the entry with the most credible model field —\n * a real Claude model > \"<synthetic>\" > empty.\n */\nfunction dedupeTokens(entries: TokenLogEntry[]): TokenLogEntry[] {\n const score = (model: string | undefined): number => {\n if (!model) return 0;\n if (model === \"<synthetic>\") return 1;\n return 2; // real model name\n };\n\n const groups = new Map<string, TokenLogEntry[]>();\n for (const e of entries) {\n const ts = e.ts ?? e.written_at ?? \"\";\n const second = ts.slice(0, 19); // YYYY-MM-DDTHH:mm:ss\n const key = [\n e.project ?? \"\",\n e.input_tokens ?? 0,\n e.output_tokens ?? 0,\n e.cache_creation_input_tokens ?? 0,\n e.cache_read_input_tokens ?? 0,\n second,\n ].join(\"|\");\n const arr = groups.get(key) ?? [];\n arr.push(e);\n groups.set(key, arr);\n }\n\n const out: TokenLogEntry[] = [];\n for (const arr of groups.values()) {\n if (arr.length === 1) {\n out.push(arr[0]!);\n continue;\n }\n arr.sort((a, b) => score(b.model) - score(a.model));\n out.push(arr[0]!);\n }\n\n // Preserve chronological order in the per-project list.\n out.sort((a, b) => {\n const at = a.ts ?? a.written_at ?? \"\";\n const bt = b.ts ?? b.written_at ?? \"\";\n return at.localeCompare(bt);\n });\n return out;\n}\n\nexport async function computeDashboardData(\n activePaths: SynthraPaths,\n recentN = 25,\n): Promise<DashboardData> {\n const registered = await listProjects();\n\n // Always include the active project, even if not yet in the registry.\n const activePath = activePaths.projectRoot;\n const activeName = basename(activePath);\n const knownPaths = new Set(registered.map((p) => p.path));\n const allEntries: Array<{ path: string; name: string; last_seen: string | null }> = [\n ...registered.map((p) => ({ path: p.path, name: p.name, last_seen: p.last_seen })),\n ];\n if (!knownPaths.has(activePath)) {\n allEntries.unshift({ path: activePath, name: activeName, last_seen: null });\n }\n\n const loaded = await Promise.all(\n allEntries.map((e) => loadProjectFiles(e.path, e.name, e.last_seen)),\n );\n\n const projects = loaded\n .map(summarize)\n .sort((a, b) => b.total_input_tokens + b.total_output_tokens - (a.total_input_tokens + a.total_output_tokens));\n\n const activeFiles =\n loaded.find((p) => p.path === activePath) ?? {\n path: activePath,\n name: activeName,\n last_seen: null,\n tokens: [],\n gates: [],\n };\n const activeStats = summarize(activeFiles);\n\n // Global aggregates\n let g_in = 0,\n g_out = 0,\n g_cr = 0,\n g_cc = 0,\n g_gate = 0,\n g_block = 0,\n g_cost = 0,\n g_turns = 0;\n for (const s of projects) {\n g_turns += s.total_turns;\n g_in += s.total_input_tokens;\n g_out += s.total_output_tokens;\n g_cr += s.total_cache_read;\n g_cc += s.total_cache_create;\n g_gate += s.total_gate_calls;\n g_block += s.blocked_count;\n g_cost += s.estimated_cost_usd;\n }\n const g_saved = g_block * AVG_TOKENS_PER_BLOCKED_GREP;\n const g_used = g_in + g_out + g_cc;\n const g_saved_pct = g_used + g_saved > 0 ? (g_saved / (g_used + g_saved)) * 100 : 0;\n\n // Recent turns + gates across all projects, sorted by ts descending\n const allTurns: RecentTurn[] = [];\n const allGates: RecentGate[] = [];\n for (const p of loaded) {\n for (const t of p.tokens) {\n allTurns.push({\n // Fall back to written_at — the Stop hook today posts entries without\n // a `ts` field, and the server tags them with written_at on receive.\n ts: t.ts ?? t.written_at ?? \"\",\n project_name: p.name,\n project_path: p.path,\n input: t.input_tokens ?? 0,\n output: t.output_tokens ?? 0,\n cache_read: t.cache_read_input_tokens ?? 0,\n cache_create: t.cache_creation_input_tokens ?? 0,\n model: t.model ?? \"\",\n cost_usd: Math.round(estimateCostUsd(t) * 1000) / 1000,\n });\n }\n for (const gate of p.gates) {\n allGates.push({\n ts: gate.ts,\n project_name: p.name,\n project_path: p.path,\n tool: gate.tool,\n decision: gate.decision,\n query: gate.query,\n });\n }\n }\n allTurns.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));\n allGates.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0));\n\n return {\n active: {\n project_root: activePath,\n project_name: activeName,\n stats: activeStats,\n },\n global: {\n project_count: projects.length,\n total_turns: g_turns,\n total_input_tokens: g_in,\n total_output_tokens: g_out,\n total_cache_read: g_cr,\n total_cache_create: g_cc,\n total_gate_calls: g_gate,\n blocked_count: g_block,\n estimated_tokens_saved: g_saved,\n saved_percent: Math.round(g_saved_pct * 10) / 10,\n estimated_cost_usd: Math.round(g_cost * 100) / 100,\n },\n projects,\n recent_turns: allTurns.slice(0, recentN),\n recent_gates: allGates.slice(0, recentN),\n };\n}\n\n// Legacy shapes from the M2 stub — kept for compat.\nexport interface TurnBreakdown {\n systemPromptTokens: number;\n conversationHistoryTokens: number;\n synthraPackTokens: number;\n userMessageTokens: number;\n responseTokens: number;\n totalTokens: number;\n costUsd: number;\n}\n\nexport interface SavingsDelta {\n withSynthra: TurnBreakdown;\n estimatedWithoutSynthra: TurnBreakdown;\n savedUsd: number;\n savedPercent: number;\n}\n\nexport function computeDelta(breakdown: TurnBreakdown, blockedGreps: number): SavingsDelta {\n const savedTokens = blockedGreps * AVG_TOKENS_PER_BLOCKED_GREP;\n const without: TurnBreakdown = {\n ...breakdown,\n conversationHistoryTokens: breakdown.conversationHistoryTokens + savedTokens,\n totalTokens: breakdown.totalTokens + savedTokens,\n costUsd: breakdown.costUsd + (savedTokens / 1_000_000) * 3,\n };\n const savedUsd = without.costUsd - breakdown.costUsd;\n const savedPercent = without.totalTokens > 0 ? (savedTokens / without.totalTokens) * 100 : 0;\n return {\n withSynthra: breakdown,\n estimatedWithoutSynthra: without,\n savedUsd,\n savedPercent: Math.round(savedPercent * 10) / 10,\n };\n}\n","// Resolves Synthra's storage locations inside a project root.\n\nimport { join } from \"node:path\";\n\nexport interface SynthraPaths {\n projectRoot: string;\n graphDir: string;\n contextDir: string;\n infoGraph: string;\n symbolIndex: string;\n sessionState: string;\n activityLog: string;\n tokenLog: string;\n gateLog: string;\n mcpPort: string;\n mcpServerLog: string;\n mcpServerErrLog: string;\n contextStore: string;\n contextMd: string;\n branchesDir: string;\n claudeDir: string;\n claudeSettings: string;\n claudeHooksDir: string;\n claudeMd: string;\n gitignore: string;\n}\n\nexport function resolvePaths(projectRoot: string): SynthraPaths {\n const graphDir = join(projectRoot, \".synthra-graph\");\n const contextDir = join(projectRoot, \".synthra\");\n const claudeDir = join(projectRoot, \".claude\");\n\n return {\n projectRoot,\n graphDir,\n contextDir,\n infoGraph: join(graphDir, \"info_graph.json\"),\n symbolIndex: join(graphDir, \"symbol_index.json\"),\n sessionState: join(graphDir, \"session.json\"),\n activityLog: join(graphDir, \"activity.jsonl\"),\n tokenLog: join(graphDir, \"token_log.jsonl\"),\n gateLog: join(graphDir, \"gate_log.jsonl\"),\n mcpPort: join(graphDir, \"mcp_port\"),\n mcpServerLog: join(graphDir, \"mcp_server.log\"),\n mcpServerErrLog: join(graphDir, \"mcp_server.err.log\"),\n contextStore: join(contextDir, \"context-store.json\"),\n contextMd: join(contextDir, \"CONTEXT.md\"),\n branchesDir: join(contextDir, \"branches\"),\n claudeDir,\n claudeSettings: join(claudeDir, \"settings.local.json\"),\n claudeHooksDir: join(claudeDir, \"hooks\"),\n claudeMd: join(projectRoot, \"CLAUDE.md\"),\n gitignore: join(projectRoot, \".gitignore\"),\n };\n}\n","// Approximate per-million-token pricing for Claude models, in USD.\n// Sourced from Anthropic's published rates. Tilde everywhere — these can shift.\n//\n// Used only for the dashboard's \"~$X\" estimate; not for billing.\n\nexport interface ModelPricing {\n /** Cost per 1M raw-input tokens. */\n input: number;\n /** Cost per 1M output tokens. */\n output: number;\n /** Cost per 1M cache-read tokens (typically ~10% of input). */\n cacheRead: number;\n /** Cost per 1M cache-creation tokens (typically input × 1.25). */\n cacheCreate: number;\n}\n\nconst PRICING: Record<string, ModelPricing> = {\n // Opus-class models — premium tier\n \"claude-opus-4-7\": { input: 15, output: 75, cacheRead: 1.5, cacheCreate: 18.75 },\n \"claude-opus-4-6\": { input: 15, output: 75, cacheRead: 1.5, cacheCreate: 18.75 },\n \"claude-opus-4-5\": { input: 15, output: 75, cacheRead: 1.5, cacheCreate: 18.75 },\n // Sonnet-class — workhorse\n \"claude-sonnet-4-6\": { input: 3, output: 15, cacheRead: 0.3, cacheCreate: 3.75 },\n \"claude-sonnet-4-5\": { input: 3, output: 15, cacheRead: 0.3, cacheCreate: 3.75 },\n // Haiku-class — fast and cheap\n \"claude-haiku-4-5\": { input: 1, output: 5, cacheRead: 0.1, cacheCreate: 1.25 },\n};\n\nconst FALLBACK: ModelPricing = { input: 3, output: 15, cacheRead: 0.3, cacheCreate: 3.75 };\n\nexport function pricingFor(model: string | undefined | null): ModelPricing {\n if (!model) return FALLBACK;\n const direct = PRICING[model];\n if (direct) return direct;\n // Loose prefix match: \"claude-opus-…\" / \"claude-sonnet-…\" / \"claude-haiku-…\"\n if (model.includes(\"opus\")) return PRICING[\"claude-opus-4-7\"] ?? FALLBACK;\n if (model.includes(\"sonnet\")) return PRICING[\"claude-sonnet-4-6\"] ?? FALLBACK;\n if (model.includes(\"haiku\")) return PRICING[\"claude-haiku-4-5\"] ?? FALLBACK;\n return FALLBACK;\n}\n\nexport interface UsageRecord {\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n model?: string;\n}\n\n/** Approximate USD cost of a single usage record. */\nexport function estimateCostUsd(usage: UsageRecord): number {\n const p = pricingFor(usage.model);\n return (\n (usage.input_tokens / 1_000_000) * p.input +\n (usage.output_tokens / 1_000_000) * p.output +\n ((usage.cache_read_input_tokens ?? 0) / 1_000_000) * p.cacheRead +\n ((usage.cache_creation_input_tokens ?? 0) / 1_000_000) * p.cacheCreate\n );\n}\n","// Global registry of projects that have run `syn .` on this machine.\n// Stored at ~/.synthra/projects.json so the dashboard can enumerate them\n// without walking the filesystem.\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\n\nconst REGISTRY_DIR = join(homedir(), \".synthra\");\nconst REGISTRY_PATH = join(REGISTRY_DIR, \"projects.json\");\nconst SCHEMA_VERSION = 1;\n\nexport interface ProjectRegistryEntry {\n path: string; // absolute project root\n name: string; // basename for display\n first_seen: string; // ISO timestamp\n last_seen: string; // ISO timestamp\n}\n\ninterface Registry {\n schema_version: number;\n projects: ProjectRegistryEntry[];\n}\n\nasync function readRegistry(): Promise<Registry> {\n try {\n const raw = await readFile(REGISTRY_PATH, \"utf8\");\n const parsed = JSON.parse(raw) as Partial<Registry>;\n if (!Array.isArray(parsed.projects)) return { schema_version: SCHEMA_VERSION, projects: [] };\n return { schema_version: parsed.schema_version ?? SCHEMA_VERSION, projects: parsed.projects };\n } catch {\n return { schema_version: SCHEMA_VERSION, projects: [] };\n }\n}\n\nasync function writeRegistry(registry: Registry): Promise<void> {\n await mkdir(dirname(REGISTRY_PATH), { recursive: true });\n await writeFile(REGISTRY_PATH, JSON.stringify(registry, null, 2) + \"\\n\", \"utf8\");\n}\n\n/** Upsert this project's entry. Updates `last_seen`; preserves `first_seen`. */\nexport async function recordProject(projectRoot: string): Promise<void> {\n const now = new Date().toISOString();\n const registry = await readRegistry();\n const existing = registry.projects.find((p) => p.path === projectRoot);\n if (existing) {\n existing.last_seen = now;\n existing.name = basename(projectRoot);\n } else {\n registry.projects.push({\n path: projectRoot,\n name: basename(projectRoot),\n first_seen: now,\n last_seen: now,\n });\n }\n try {\n await writeRegistry(registry);\n } catch {\n // Registry is best-effort — a write failure shouldn't block the session.\n }\n}\n\nexport async function listProjects(): Promise<ProjectRegistryEntry[]> {\n const registry = await readRegistry();\n // Sort by last_seen descending so the most active project surfaces first.\n return registry.projects\n .slice()\n .sort((a, b) => (a.last_seen > b.last_seen ? -1 : a.last_seen < b.last_seen ? 1 : 0));\n}\n\nexport { REGISTRY_PATH, REGISTRY_DIR };\n","<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Synthra — Token Dashboard</title>\n <link rel=\"stylesheet\" href=\"./style.css\" />\n</head>\n<body>\n <header>\n <div class=\"brand\">\n <h1>Synthra</h1>\n <span class=\"tag\">Token Dashboard</span>\n </div>\n <div class=\"meta\">\n <span class=\"active-project\" id=\"active-project\">…</span>\n <span class=\"dot\" id=\"dot\"></span>\n <span id=\"status\">connecting…</span>\n </div>\n </header>\n\n <main>\n <section>\n <h2>Global totals <span class=\"muted\">(all projects)</span></h2>\n <div class=\"cards\" id=\"cards\"></div>\n </section>\n\n <section>\n <h2>Projects</h2>\n <div class=\"projects\" id=\"projects\"></div>\n <p class=\"empty hidden\" id=\"projects-empty\">No projects registered yet. Run <code>syn .</code> in any project to add it.</p>\n </section>\n\n <section>\n <h2>Recent calls <span class=\"muted\">(across all projects)</span></h2>\n <table id=\"turns\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Project</th>\n <th>Model</th>\n <th class=\"num\">Input</th>\n <th class=\"num\">Output</th>\n <th class=\"num\">Cache R / W</th>\n <th class=\"num\">Cost</th>\n </tr>\n </thead>\n <tbody></tbody>\n </table>\n <p class=\"empty hidden\" id=\"turns-empty\">No turns logged yet. Use Claude via the IDE extension or <code>claude</code> CLI while <code>syn .</code> is running.</p>\n </section>\n\n <section>\n <h2>Recent gate decisions</h2>\n <table id=\"gates\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Project</th>\n <th>Tool</th>\n <th>Decision</th>\n <th>Query</th>\n </tr>\n </thead>\n <tbody></tbody>\n </table>\n <p class=\"empty hidden\" id=\"gates-empty\">No gate decisions yet.</p>\n </section>\n </main>\n\n <footer>\n <span>Token Counter MCP · live polling every 2s</span>\n <span class=\"muted\">Cost figures are approximate — see /docs/PROTOCOL.md</span>\n </footer>\n\n <script>\n const $ = (sel) => document.querySelector(sel);\n const cardsEl = $(\"#cards\");\n const projectsEl = $(\"#projects\");\n const turnsBody = $(\"#turns tbody\");\n const gatesBody = $(\"#gates tbody\");\n const turnsEmpty = $(\"#turns-empty\");\n const gatesEmpty = $(\"#gates-empty\");\n const projectsEmpty = $(\"#projects-empty\");\n const statusEl = $(\"#status\");\n const dotEl = $(\"#dot\");\n const activeProjectEl = $(\"#active-project\");\n\n function fmt(n) {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return \"0\";\n if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + \"M\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"K\";\n return n.toLocaleString();\n }\n\n function fmtFull(n) {\n return (typeof n === \"number\" ? n : 0).toLocaleString();\n }\n\n function fmtCost(usd) {\n if (typeof usd !== \"number\") return \"~$0.00\";\n if (usd >= 1) return \"~$\" + usd.toFixed(2);\n if (usd >= 0.01) return \"~$\" + usd.toFixed(3);\n return \"~$\" + usd.toFixed(4);\n }\n\n function fmtTs(iso) {\n try {\n const d = new Date(iso);\n const today = new Date();\n const isToday = d.toDateString() === today.toDateString();\n if (isToday) return d.toLocaleTimeString();\n return d.toLocaleString();\n } catch {\n return iso;\n }\n }\n\n function renderCards(g) {\n cardsEl.innerHTML = \"\";\n const cards = [\n { label: \"Total cost\", value: fmtCost(g.estimated_cost_usd), accent: true },\n { label: \"Turns\", value: fmt(g.total_turns) },\n { label: \"Input\", value: fmt(g.total_input_tokens) },\n { label: \"Output\", value: fmt(g.total_output_tokens) },\n { label: \"Cache read\", value: fmt(g.total_cache_read) },\n { label: \"Cache write\", value: fmt(g.total_cache_create) },\n { label: \"Projects\", value: fmt(g.project_count) },\n { label: \"Blocked Grep / Glob\", value: fmt(g.blocked_count), accent: true },\n { label: \"Tokens saved\", value: fmt(g.estimated_tokens_saved), accent: true },\n ];\n for (const c of cards) {\n const el = document.createElement(\"div\");\n el.className = \"card\" + (c.accent ? \" accent\" : \"\");\n el.innerHTML = '<div class=\"card-label\">' + c.label + '</div><div class=\"card-value\">' + c.value + '</div>';\n cardsEl.appendChild(el);\n }\n }\n\n function renderProjects(projects, globalCost) {\n projectsEl.innerHTML = \"\";\n if (!projects.length) {\n projectsEmpty.classList.remove(\"hidden\");\n return;\n }\n projectsEmpty.classList.add(\"hidden\");\n const maxTokens = Math.max(1, ...projects.map((p) => p.total_input_tokens + p.total_output_tokens));\n for (const p of projects) {\n const total = p.total_input_tokens + p.total_output_tokens;\n const pct = Math.round((total / maxTokens) * 100);\n const sharePct = globalCost > 0 ? ((p.estimated_cost_usd / globalCost) * 100).toFixed(1) : \"0.0\";\n const row = document.createElement(\"div\");\n row.className = \"project-row\";\n row.innerHTML =\n '<div class=\"project-name\">' +\n '<strong>' + p.name + '</strong>' +\n '<code class=\"project-path\">' + p.path + '</code>' +\n '</div>' +\n '<div class=\"project-stats\">' +\n '<div class=\"stat\"><span class=\"stat-value cost\">' + fmtCost(p.estimated_cost_usd) + '</span><span class=\"stat-label\">cost (' + sharePct + '%)</span></div>' +\n '<div class=\"stat\"><span class=\"stat-value\">' + fmt(total) + '</span><span class=\"stat-label\">tokens</span></div>' +\n '<div class=\"stat\"><span class=\"stat-value\">' + fmt(p.total_turns) + '</span><span class=\"stat-label\">turns</span></div>' +\n '<div class=\"stat\"><span class=\"stat-value\">' + fmt(p.blocked_count) + '</span><span class=\"stat-label\">blocks</span></div>' +\n '</div>' +\n '<div class=\"bar\"><div class=\"bar-fill\" style=\"width:' + pct + '%\"></div></div>';\n projectsEl.appendChild(row);\n }\n }\n\n function renderTurns(turns) {\n turnsBody.innerHTML = \"\";\n if (!turns.length) {\n turnsEmpty.classList.remove(\"hidden\");\n return;\n }\n turnsEmpty.classList.add(\"hidden\");\n for (const t of turns) {\n const tr = document.createElement(\"tr\");\n const modelCell =\n t.model && t.model !== \"<synthetic>\"\n ? \"<code>\" + t.model + \"</code>\"\n : '<span class=\"muted\">' + (t.model === \"<synthetic>\" ? \"synthetic\" : \"unknown\") + \"</span>\";\n tr.innerHTML =\n \"<td>\" + fmtTs(t.ts) + \"</td>\" +\n \"<td><code>\" + t.project_name + \"</code></td>\" +\n \"<td>\" + modelCell + \"</td>\" +\n '<td class=\"num\">' + fmtFull(t.input) + \"</td>\" +\n '<td class=\"num\">' + fmtFull(t.output) + \"</td>\" +\n '<td class=\"num\">' + fmt(t.cache_read) + \" / \" + fmt(t.cache_create) + \"</td>\" +\n '<td class=\"num cost\">' + fmtCost(t.cost_usd) + \"</td>\";\n turnsBody.appendChild(tr);\n }\n }\n\n function renderGates(gates) {\n gatesBody.innerHTML = \"\";\n if (!gates.length) {\n gatesEmpty.classList.remove(\"hidden\");\n return;\n }\n gatesEmpty.classList.add(\"hidden\");\n for (const g of gates) {\n const tr = document.createElement(\"tr\");\n const cls = g.decision === \"block\" ? \"decision-block\" : \"decision-allow\";\n tr.innerHTML =\n \"<td>\" + fmtTs(g.ts) + \"</td>\" +\n \"<td><code>\" + g.project_name + \"</code></td>\" +\n \"<td><code>\" + g.tool + \"</code></td>\" +\n '<td class=\"' + cls + '\">' + g.decision + \"</td>\" +\n \"<td><code>\" + (g.query || \"\") + \"</code></td>\";\n gatesBody.appendChild(tr);\n }\n }\n\n async function tick() {\n try {\n const res = await fetch(\"/data\");\n if (!res.ok) throw new Error(\"HTTP \" + res.status);\n const data = await res.json();\n activeProjectEl.textContent = data.active.project_root;\n renderCards(data.global);\n renderProjects(data.projects, data.global.estimated_cost_usd);\n renderTurns(data.recent_turns);\n renderGates(data.recent_gates);\n statusEl.textContent = \"live · \" + new Date().toLocaleTimeString();\n dotEl.classList.add(\"live\");\n dotEl.classList.remove(\"dead\");\n } catch (e) {\n statusEl.textContent = \"disconnected · \" + e.message;\n dotEl.classList.add(\"dead\");\n dotEl.classList.remove(\"live\");\n }\n }\n\n tick();\n setInterval(tick, 2000);\n </script>\n</body>\n</html>\n","/* Synthra token dashboard — palette per project brief. */\n\n:root {\n --color-heading: #ECEBD8;\n --color-body: #EDECD9;\n --color-bg: #000000;\n --color-surface: #140009;\n --color-surface-raised: #1E000D;\n --color-border: #4D0020;\n --color-accent: #FF0073;\n --color-accent-darker: #EB006A;\n --color-form-bg: #2E0014;\n --color-muted: rgba(237, 236, 217, 0.55);\n --color-very-muted: rgba(237, 236, 217, 0.35);\n --color-block: #FF0073;\n --color-allow: #ECEBD8;\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nhtml, body {\n background: var(--color-bg);\n color: var(--color-body);\n font-family:\n ui-sans-serif, -apple-system, BlinkMacSystemFont, \"Segoe UI\",\n system-ui, sans-serif;\n font-size: 14px;\n line-height: 1.5;\n min-height: 100vh;\n}\n\ncode, .num, table, .project-path {\n font-family: ui-monospace, \"SF Mono\", Menlo, Consolas, monospace;\n}\n\nheader {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n padding: 1.1rem 2rem;\n background: var(--color-surface);\n border-bottom: 1px solid var(--color-border);\n}\n\nheader .brand {\n display: flex;\n align-items: baseline;\n gap: 0.75rem;\n}\n\nheader h1 {\n color: var(--color-heading);\n font-size: 1.35rem;\n font-weight: 700;\n letter-spacing: 0.02em;\n}\n\nheader .tag {\n color: var(--color-muted);\n font-size: 0.78rem;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n}\n\nheader .meta {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 0.75rem;\n font-size: 0.78rem;\n font-family: ui-monospace, monospace;\n color: var(--color-muted);\n}\n\nheader .active-project {\n max-width: 480px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\nmain {\n padding: 2rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n max-width: 1400px;\n margin: 0 auto;\n}\n\nsection {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\nh2 {\n color: var(--color-heading);\n font-size: 0.85rem;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\nh2 .muted {\n color: var(--color-very-muted);\n font-size: 0.78rem;\n font-weight: 400;\n letter-spacing: 0.06em;\n text-transform: none;\n margin-left: 0.5rem;\n}\n\n/* Cards row */\n.cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: 0.7rem;\n}\n\n.card {\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n padding: 0.85rem 1rem;\n}\n\n.card.accent {\n background: var(--color-surface-raised);\n border-color: var(--color-accent);\n}\n\n.card-label {\n color: var(--color-muted);\n font-size: 0.66rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: 0.35rem;\n}\n\n.card-value {\n color: var(--color-heading);\n font-family: ui-monospace, monospace;\n font-size: 1.5rem;\n font-weight: 600;\n}\n\n.card.accent .card-value {\n color: var(--color-accent);\n}\n\n/* Projects list */\n.projects {\n display: flex;\n flex-direction: column;\n gap: 0.6rem;\n}\n\n.project-row {\n display: grid;\n grid-template-columns: minmax(220px, 1fr) auto;\n grid-template-rows: auto auto;\n gap: 0.6rem 1.25rem;\n align-items: center;\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n padding: 0.9rem 1.2rem;\n}\n\n.project-row:hover {\n background: var(--color-surface-raised);\n}\n\n.project-name {\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n overflow: hidden;\n}\n\n.project-name strong {\n color: var(--color-heading);\n font-size: 0.95rem;\n font-weight: 600;\n}\n\n.project-name .project-path {\n color: var(--color-very-muted);\n font-size: 0.72rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.project-stats {\n display: flex;\n gap: 1.6rem;\n justify-self: end;\n text-align: right;\n}\n\n.stat {\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n min-width: 70px;\n}\n\n.stat-value {\n color: var(--color-heading);\n font-family: ui-monospace, monospace;\n font-size: 0.95rem;\n font-weight: 600;\n}\n\n.stat-value.cost {\n color: var(--color-accent);\n}\n\n.stat-label {\n color: var(--color-muted);\n font-size: 0.66rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.bar {\n grid-column: 1 / -1;\n height: 3px;\n background: var(--color-form-bg);\n border-radius: 2px;\n overflow: hidden;\n}\n\n.bar-fill {\n height: 100%;\n background: linear-gradient(90deg, var(--color-accent-darker), var(--color-accent));\n border-radius: 2px;\n transition: width 400ms ease;\n}\n\n/* Tables */\ntable {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.83rem;\n background: var(--color-surface);\n border: 1px solid var(--color-border);\n border-radius: 6px;\n overflow: hidden;\n}\n\ntable thead th {\n text-align: left;\n color: var(--color-muted);\n text-transform: uppercase;\n font-size: 0.66rem;\n letter-spacing: 0.08em;\n font-weight: 600;\n padding: 0.65rem 0.85rem;\n border-bottom: 1px solid var(--color-border);\n background: var(--color-surface);\n}\n\ntable thead th.num {\n text-align: right;\n}\n\ntable tbody td {\n padding: 0.55rem 0.85rem;\n border-bottom: 1px solid rgba(77, 0, 32, 0.4);\n color: var(--color-body);\n}\n\ntable tbody td.num {\n text-align: right;\n}\n\ntable tbody td.cost {\n color: var(--color-accent);\n font-weight: 600;\n}\n\ntable tbody tr:last-child td {\n border-bottom: none;\n}\n\ntable tbody tr:hover {\n background: var(--color-surface-raised);\n}\n\ncode {\n color: var(--color-heading);\n background: var(--color-form-bg);\n padding: 0.1rem 0.4rem;\n border-radius: 3px;\n font-size: 0.85em;\n}\n\n.decision-block {\n color: var(--color-block);\n font-weight: 700;\n text-transform: uppercase;\n font-size: 0.72rem;\n letter-spacing: 0.06em;\n}\n\n.decision-allow {\n color: var(--color-allow);\n opacity: 0.7;\n text-transform: uppercase;\n font-size: 0.72rem;\n letter-spacing: 0.06em;\n}\n\n.empty {\n color: var(--color-muted);\n font-style: italic;\n padding: 1rem;\n background: var(--color-surface);\n border: 1px dashed var(--color-border);\n border-radius: 6px;\n}\n\n.hidden {\n display: none;\n}\n\nfooter {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0.85rem 2rem;\n background: var(--color-surface);\n border-top: 1px solid var(--color-border);\n color: var(--color-muted);\n font-size: 0.72rem;\n font-family: ui-monospace, monospace;\n}\n\nfooter .muted {\n color: var(--color-very-muted);\n}\n\n.dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--color-muted);\n transition: background 200ms ease, box-shadow 200ms ease;\n}\n\n.dot.live {\n background: var(--color-accent);\n box-shadow: 0 0 8px var(--color-accent-darker);\n}\n\n.dot.dead {\n background: var(--color-border);\n box-shadow: none;\n}\n"],"mappings":"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","names":["readFile","join","readFile","basename"]}