@jefuriiij/synthra 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/dist/cli/index.js +312 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +61 -8
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +57 -15
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.1.
|
|
21
|
+
version: "0.1.24",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -88,7 +88,7 @@ var init_package = __esm({
|
|
|
88
88
|
// src/cli/index.ts
|
|
89
89
|
init_package();
|
|
90
90
|
import sade from "sade";
|
|
91
|
-
import { resolve as
|
|
91
|
+
import { resolve as resolve5 } from "path";
|
|
92
92
|
|
|
93
93
|
// src/dashboard/server.ts
|
|
94
94
|
init_package();
|
|
@@ -130,10 +130,10 @@ async function findFreePort(start = PORT_RANGE_START, end = PORT_RANGE_END) {
|
|
|
130
130
|
throw new Error(`Synthra: no free port in ${start}-${end}`);
|
|
131
131
|
}
|
|
132
132
|
function isFree(port) {
|
|
133
|
-
return new Promise((
|
|
133
|
+
return new Promise((resolve6) => {
|
|
134
134
|
const s = createServer();
|
|
135
|
-
s.once("error", () =>
|
|
136
|
-
s.once("listening", () => s.close(() =>
|
|
135
|
+
s.once("error", () => resolve6(false));
|
|
136
|
+
s.once("listening", () => s.close(() => resolve6(true)));
|
|
137
137
|
s.listen(port, "127.0.0.1");
|
|
138
138
|
});
|
|
139
139
|
}
|
|
@@ -157,6 +157,7 @@ function resolvePaths(projectRoot) {
|
|
|
157
157
|
activityLog: join(graphDir, "activity.jsonl"),
|
|
158
158
|
tokenLog: join(graphDir, "token_log.jsonl"),
|
|
159
159
|
gateLog: join(graphDir, "gate_log.jsonl"),
|
|
160
|
+
toolLog: join(graphDir, "tool_log.jsonl"),
|
|
160
161
|
mcpPort: join(graphDir, "mcp_port"),
|
|
161
162
|
mcpServerLog: join(graphDir, "mcp_server.log"),
|
|
162
163
|
mcpServerErrLog: join(graphDir, "mcp_server.err.log"),
|
|
@@ -246,6 +247,14 @@ async function listProjects() {
|
|
|
246
247
|
|
|
247
248
|
// src/dashboard/delta.ts
|
|
248
249
|
var AVG_TOKENS_PER_BLOCKED_GREP = 500;
|
|
250
|
+
function countToolCalls(entries) {
|
|
251
|
+
const out = {};
|
|
252
|
+
for (const e of entries) {
|
|
253
|
+
if (!e.tool) continue;
|
|
254
|
+
out[e.tool] = (out[e.tool] ?? 0) + 1;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
249
258
|
async function readJsonl(path) {
|
|
250
259
|
try {
|
|
251
260
|
const text = await readFile2(path, "utf8");
|
|
@@ -294,16 +303,24 @@ function summarize(p) {
|
|
|
294
303
|
blocked_count: blocked,
|
|
295
304
|
estimated_tokens_saved: saved,
|
|
296
305
|
estimated_cost_usd: Math.round(costUsd * 100) / 100,
|
|
306
|
+
total_tool_calls: p.tools.length,
|
|
307
|
+
tool_calls: countToolCalls(p.tools),
|
|
297
308
|
models
|
|
298
309
|
};
|
|
299
310
|
}
|
|
311
|
+
function dedupeEnabled() {
|
|
312
|
+
const v = process.env.SYN_DASHBOARD_DEDUPE;
|
|
313
|
+
return v !== "0" && v !== "off" && v !== "false";
|
|
314
|
+
}
|
|
300
315
|
async function loadProjectFiles(path, name, lastSeen) {
|
|
301
316
|
const paths = resolvePaths(path);
|
|
302
|
-
const [rawTokens, gates] = await Promise.all([
|
|
317
|
+
const [rawTokens, gates, tools] = await Promise.all([
|
|
303
318
|
readJsonl(paths.tokenLog),
|
|
304
|
-
readJsonl(paths.gateLog)
|
|
319
|
+
readJsonl(paths.gateLog),
|
|
320
|
+
readJsonl(paths.toolLog)
|
|
305
321
|
]);
|
|
306
|
-
|
|
322
|
+
const tokens = dedupeEnabled() ? dedupeTokens(rawTokens) : rawTokens;
|
|
323
|
+
return { path, name, last_seen: lastSeen, tokens, gates, tools };
|
|
307
324
|
}
|
|
308
325
|
function dedupeTokens(entries) {
|
|
309
326
|
const score2 = (model) => {
|
|
@@ -363,10 +380,12 @@ async function computeDashboardData(activePaths, recentN = 500) {
|
|
|
363
380
|
name: activeName,
|
|
364
381
|
last_seen: null,
|
|
365
382
|
tokens: [],
|
|
366
|
-
gates: []
|
|
383
|
+
gates: [],
|
|
384
|
+
tools: []
|
|
367
385
|
};
|
|
368
386
|
const activeStats = summarize(activeFiles);
|
|
369
|
-
let g_in = 0, g_out = 0, g_cr = 0, g_cc = 0, g_gate = 0, g_block = 0, g_cost = 0, g_turns = 0;
|
|
387
|
+
let g_in = 0, g_out = 0, g_cr = 0, g_cc = 0, g_gate = 0, g_block = 0, g_cost = 0, g_turns = 0, g_tools = 0;
|
|
388
|
+
const g_tool_calls = {};
|
|
370
389
|
for (const s of projects) {
|
|
371
390
|
g_turns += s.total_turns;
|
|
372
391
|
g_in += s.total_input_tokens;
|
|
@@ -376,6 +395,8 @@ async function computeDashboardData(activePaths, recentN = 500) {
|
|
|
376
395
|
g_gate += s.total_gate_calls;
|
|
377
396
|
g_block += s.blocked_count;
|
|
378
397
|
g_cost += s.estimated_cost_usd;
|
|
398
|
+
g_tools += s.total_tool_calls;
|
|
399
|
+
for (const [k, v] of Object.entries(s.tool_calls)) g_tool_calls[k] = (g_tool_calls[k] ?? 0) + v;
|
|
379
400
|
}
|
|
380
401
|
const g_saved = g_block * AVG_TOKENS_PER_BLOCKED_GREP;
|
|
381
402
|
const g_used = g_in + g_out + g_cc;
|
|
@@ -428,7 +449,9 @@ async function computeDashboardData(activePaths, recentN = 500) {
|
|
|
428
449
|
blocked_count: g_block,
|
|
429
450
|
estimated_tokens_saved: g_saved,
|
|
430
451
|
saved_percent: Math.round(g_saved_pct * 10) / 10,
|
|
431
|
-
estimated_cost_usd: Math.round(g_cost * 100) / 100
|
|
452
|
+
estimated_cost_usd: Math.round(g_cost * 100) / 100,
|
|
453
|
+
total_tool_calls: g_tools,
|
|
454
|
+
tool_calls: g_tool_calls
|
|
432
455
|
},
|
|
433
456
|
projects,
|
|
434
457
|
recent_turns: allTurns.slice(0, recentN),
|
|
@@ -617,6 +640,16 @@ var public_default = `<!doctype html>
|
|
|
617
640
|
</div>
|
|
618
641
|
</div>
|
|
619
642
|
|
|
643
|
+
<!-- Graph tool usage -->
|
|
644
|
+
<div class="card tools-card has-tooltip" data-tooltip="How often Claude actually used Synthra's graph tools (graph_continue / graph_read / \u2026) across all projects. A positive usage signal \u2014 unlike the Moat's block count, it captures every time Claude reached for the graph instead of running a Grep.">
|
|
645
|
+
<div class="card-head">
|
|
646
|
+
<div class="card-eyebrow">Graph tools <em>used</em></div>
|
|
647
|
+
<div class="card-meta">all projects</div>
|
|
648
|
+
</div>
|
|
649
|
+
<div class="moat-value"><span id="tool-total">0</span> <em>calls</em></div>
|
|
650
|
+
<div class="cost-sub" id="tool-breakdown"></div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
620
653
|
<!-- The Moat -->
|
|
621
654
|
<div class="card moat has-tooltip" data-tooltip="Synthra's PreToolUse hook intercepts. Each block = Synthra recognized the graph already had high-confidence context for the query, so it stopped Claude from running an exploratory Grep or Glob. The list below shows the latest decisions across all projects.">
|
|
622
655
|
<div class="card-head">
|
|
@@ -979,6 +1012,25 @@ var public_default = `<!doctype html>
|
|
|
979
1012
|
$('#blocks').textContent = fmtPlain(g.blocked_count);
|
|
980
1013
|
}
|
|
981
1014
|
|
|
1015
|
+
function renderToolUsage(g) {
|
|
1016
|
+
$('#tool-total').innerHTML = fmt(g.total_tool_calls || 0);
|
|
1017
|
+
const el = $('#tool-breakdown');
|
|
1018
|
+
el.innerHTML = '';
|
|
1019
|
+
const entries = Object.entries(g.tool_calls || {}).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
1020
|
+
if (!entries.length) {
|
|
1021
|
+
el.innerHTML = '<div class="empty">No graph-tool calls yet.</div>';
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const frag = document.createDocumentFragment();
|
|
1025
|
+
for (const [tool, n] of entries) {
|
|
1026
|
+
const row = document.createElement('div');
|
|
1027
|
+
row.className = 'cs-row';
|
|
1028
|
+
row.innerHTML = '<span class="cs-k">' + tool + '</span><span class="cs-v">' + fmtPlain(n) + '</span>';
|
|
1029
|
+
frag.appendChild(row);
|
|
1030
|
+
}
|
|
1031
|
+
el.appendChild(frag);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
982
1034
|
function renderTurns(turns) {
|
|
983
1035
|
const tbody = $('#turns-body');
|
|
984
1036
|
const empty = $('#turns-empty');
|
|
@@ -1244,6 +1296,7 @@ var public_default = `<!doctype html>
|
|
|
1244
1296
|
renderSavings(data.global);
|
|
1245
1297
|
renderCostHero(data.global);
|
|
1246
1298
|
renderMoat(data.global);
|
|
1299
|
+
renderToolUsage(data.global);
|
|
1247
1300
|
renderTurns(turns);
|
|
1248
1301
|
renderGateMini(gates);
|
|
1249
1302
|
|
|
@@ -1332,7 +1385,7 @@ var public_default = `<!doctype html>
|
|
|
1332
1385
|
`;
|
|
1333
1386
|
|
|
1334
1387
|
// src/dashboard/public/style.css
|
|
1335
|
-
var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n';
|
|
1388
|
+
var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n .col-right > *:nth-child(3) { animation-delay: 320ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n';
|
|
1336
1389
|
|
|
1337
1390
|
// src/dashboard/server.ts
|
|
1338
1391
|
var FALLBACK_RANGE = 9;
|
|
@@ -1362,8 +1415,8 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1362
1415
|
port,
|
|
1363
1416
|
url: `http://127.0.0.1:${port}`,
|
|
1364
1417
|
async stop() {
|
|
1365
|
-
await new Promise((
|
|
1366
|
-
nodeServer.close((err2) => err2 ? reject(err2) :
|
|
1418
|
+
await new Promise((resolve6, reject) => {
|
|
1419
|
+
nodeServer.close((err2) => err2 ? reject(err2) : resolve6());
|
|
1367
1420
|
});
|
|
1368
1421
|
}
|
|
1369
1422
|
};
|
|
@@ -1401,33 +1454,33 @@ exit 0
|
|
|
1401
1454
|
var pre_tool_use_default = '# PreToolUse hook \u2014 Windows PowerShell.\n# THE MOAT (improvement #1). Reads the tool call from stdin (JSON), POSTs it\n# to /gate, and if the server says "block" emits a JSON deny-decision to\n# stdout. Claude Code reads stdout JSON to enforce the decision.\n# Always exits 0; failure-to-reach-server leaves Claude untouched.\n\n$ErrorActionPreference = "SilentlyContinue"\n\n$raw = [Console]::In.ReadToEnd()\nif (-not $raw) { exit 0 }\n\ntry {\n $hookInput = $raw | ConvertFrom-Json -ErrorAction Stop\n} catch {\n exit 0\n}\n\n$portFile = Join-Path $PWD ".synthra-graph\\mcp_port"\nif (-not (Test-Path $portFile)) { exit 0 }\n$port = (Get-Content -Path $portFile -Raw).Trim()\nif (-not $port) { exit 0 }\n\n$payload = @{\n tool_name = $hookInput.tool_name\n tool_input = $hookInput.tool_input\n} | ConvertTo-Json -Depth 10 -Compress\n\ntry {\n $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$port/gate" -Method POST `\n -Body $payload -ContentType "application/json" -TimeoutSec 3\n} catch {\n exit 0\n}\n\nif ($resp.decision -eq "block") {\n $denyJson = @{\n hookSpecificOutput = @{\n hookEventName = "PreToolUse"\n permissionDecision = "deny"\n permissionDecisionReason = $resp.reason\n }\n } | ConvertTo-Json -Depth 5 -Compress\n Write-Output $denyJson\n}\nexit 0\n';
|
|
1402
1455
|
|
|
1403
1456
|
// src/hooks/scripts/pre-tool-use.sh
|
|
1404
|
-
var pre_tool_use_default2 = `#!/usr/bin/env bash
|
|
1405
|
-
# PreToolUse hook \u2014 bash. POSTs the tool call to /gate; if server returns
|
|
1406
|
-
# "block", emits the deny-decision JSON to stdout for Claude Code to enforce
|
|
1407
|
-
# Always exits 0; server failures leave Claude untouched
|
|
1408
|
-
|
|
1409
|
-
set +e
|
|
1410
|
-
|
|
1411
|
-
PORT_FILE="$PWD/.synthra-graph/mcp_port"
|
|
1412
|
-
if [ ! -f "$PORT_FILE" ]; then exit 0; fi
|
|
1413
|
-
PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
1414
|
-
if [ -z "$PORT" ]; then exit 0; fi
|
|
1415
|
-
|
|
1416
|
-
INPUT=$(cat 2>/dev/null)
|
|
1417
|
-
if [ -z "$INPUT" ]; then exit 0; fi
|
|
1418
|
-
|
|
1419
|
-
RESP=$(curl -sS --max-time 3 -X POST -H "Content-Type: application/json"
|
|
1420
|
-
--data "$INPUT" "http://127.0.0.1:$PORT/gate" 2>/dev/null)
|
|
1421
|
-
|
|
1422
|
-
case "$RESP" in
|
|
1423
|
-
*'"decision":"block"'*)
|
|
1424
|
-
REASON=$(printf '%s' "$RESP" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/p')
|
|
1425
|
-
cat <<EOF
|
|
1426
|
-
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"\${REASON}"}}
|
|
1427
|
-
EOF
|
|
1428
|
-
|
|
1429
|
-
esac
|
|
1430
|
-
exit 0
|
|
1457
|
+
var pre_tool_use_default2 = `#!/usr/bin/env bash\r
|
|
1458
|
+
# PreToolUse hook \u2014 bash. POSTs the tool call to /gate; if server returns\r
|
|
1459
|
+
# "block", emits the deny-decision JSON to stdout for Claude Code to enforce.\r
|
|
1460
|
+
# Always exits 0; server failures leave Claude untouched.\r
|
|
1461
|
+
\r
|
|
1462
|
+
set +e\r
|
|
1463
|
+
\r
|
|
1464
|
+
PORT_FILE="$PWD/.synthra-graph/mcp_port"\r
|
|
1465
|
+
if [ ! -f "$PORT_FILE" ]; then exit 0; fi\r
|
|
1466
|
+
PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')\r
|
|
1467
|
+
if [ -z "$PORT" ]; then exit 0; fi\r
|
|
1468
|
+
\r
|
|
1469
|
+
INPUT=$(cat 2>/dev/null)\r
|
|
1470
|
+
if [ -z "$INPUT" ]; then exit 0; fi\r
|
|
1471
|
+
\r
|
|
1472
|
+
RESP=$(curl -sS --max-time 3 -X POST -H "Content-Type: application/json" \\\r
|
|
1473
|
+
--data "$INPUT" "http://127.0.0.1:$PORT/gate" 2>/dev/null)\r
|
|
1474
|
+
\r
|
|
1475
|
+
case "$RESP" in\r
|
|
1476
|
+
*'"decision":"block"'*)\r
|
|
1477
|
+
REASON=$(printf '%s' "$RESP" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/p')\r
|
|
1478
|
+
cat <<EOF\r
|
|
1479
|
+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"\${REASON}"}}\r
|
|
1480
|
+
EOF\r
|
|
1481
|
+
;;\r
|
|
1482
|
+
esac\r
|
|
1483
|
+
exit 0\r
|
|
1431
1484
|
`;
|
|
1432
1485
|
|
|
1433
1486
|
// src/hooks/scripts/prime.ps1
|
|
@@ -1958,6 +2011,9 @@ import { resolve } from "path";
|
|
|
1958
2011
|
// src/scanner/extract.ts
|
|
1959
2012
|
import { dirname as dirname4, join as join6, posix } from "path";
|
|
1960
2013
|
|
|
2014
|
+
// src/graph/types.ts
|
|
2015
|
+
var SCHEMA_VERSION2 = 1;
|
|
2016
|
+
|
|
1961
2017
|
// src/scanner/hash.ts
|
|
1962
2018
|
import { createHash } from "crypto";
|
|
1963
2019
|
function fileHash(content) {
|
|
@@ -2294,7 +2350,7 @@ async function buildGraph(root, parsed) {
|
|
|
2294
2350
|
nodes,
|
|
2295
2351
|
edges,
|
|
2296
2352
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2297
|
-
schema_version:
|
|
2353
|
+
schema_version: SCHEMA_VERSION2
|
|
2298
2354
|
};
|
|
2299
2355
|
}
|
|
2300
2356
|
function buildSymbolIndex(graph) {
|
|
@@ -2828,6 +2884,7 @@ var TS_QUERY = `
|
|
|
2828
2884
|
(enum_declaration name: (identifier) @enum.name) @enum
|
|
2829
2885
|
(method_definition name: (property_identifier) @method.name) @method
|
|
2830
2886
|
(lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
|
|
2887
|
+
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2831
2888
|
(import_statement source: (string) @import)
|
|
2832
2889
|
`;
|
|
2833
2890
|
var JS_QUERY = `
|
|
@@ -2835,6 +2892,7 @@ var JS_QUERY = `
|
|
|
2835
2892
|
(class_declaration name: (identifier) @class.name) @class
|
|
2836
2893
|
(method_definition name: (property_identifier) @method.name) @method
|
|
2837
2894
|
(lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
|
|
2895
|
+
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2838
2896
|
(import_statement source: (string) @import)
|
|
2839
2897
|
(call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
|
|
2840
2898
|
`;
|
|
@@ -2859,7 +2917,7 @@ function shapeFromCaptures(captures) {
|
|
|
2859
2917
|
const name = captures.get(`${k}.name`);
|
|
2860
2918
|
return decl && name ? { decl, name, kind: sk } : null;
|
|
2861
2919
|
};
|
|
2862
|
-
return findDecl("function", "function") ?? findDecl("class", "class") ?? findDecl("interface", "interface") ?? findDecl("type", "type") ?? findDecl("enum", "enum") ?? findDecl("method", "method") ?? findDecl("const-fn", "function");
|
|
2920
|
+
return findDecl("function", "function") ?? findDecl("class", "class") ?? findDecl("interface", "interface") ?? findDecl("type", "type") ?? findDecl("enum", "enum") ?? findDecl("method", "method") ?? findDecl("const-fn", "function") ?? findDecl("member-fn", "function");
|
|
2863
2921
|
}
|
|
2864
2922
|
async function parseTypeScript(f, source) {
|
|
2865
2923
|
const grammar = grammarFor(f.ext);
|
|
@@ -3267,7 +3325,7 @@ import { basename as basename4 } from "path";
|
|
|
3267
3325
|
// src/hooks/claude-md.ts
|
|
3268
3326
|
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3269
3327
|
import { basename as basename3, dirname as dirname6 } from "path";
|
|
3270
|
-
var POLICY_VERSION =
|
|
3328
|
+
var POLICY_VERSION = 5;
|
|
3271
3329
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3272
3330
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
3273
3331
|
var ANY_BLOCK_RE = /<!--\s*synthra-policy\s+v\d+\s+BEGIN\s*-->[\s\S]*?<!--\s*synthra-policy\s+v\d+\s+END\s*-->\s*/g;
|
|
@@ -3335,6 +3393,10 @@ function policyBlock() {
|
|
|
3335
3393
|
" reads should be rare \u2014 only when you genuinely need the full file.",
|
|
3336
3394
|
"- If `graph_continue`'s `Files` list contains a `::` entry, pass it",
|
|
3337
3395
|
" verbatim to `graph_read`.",
|
|
3396
|
+
"- **Large file?** Don't read it in successive line-range chunks \u2014 call",
|
|
3397
|
+
' `graph_continue` or `graph_read("file::symbol")` to pull the one symbol',
|
|
3398
|
+
" you need. Chunked whole-file Reads are exactly the cost `graph_read`",
|
|
3399
|
+
" exists to avoid.",
|
|
3338
3400
|
"",
|
|
3339
3401
|
"### Editing a file",
|
|
3340
3402
|
"",
|
|
@@ -3580,6 +3642,10 @@ async function scanCommand(rawPath) {
|
|
|
3580
3642
|
return scanProject(rawPath);
|
|
3581
3643
|
}
|
|
3582
3644
|
|
|
3645
|
+
// src/server/mcp.ts
|
|
3646
|
+
import { appendFile as appendFile2, mkdir as mkdir8 } from "fs/promises";
|
|
3647
|
+
import { dirname as dirname9 } from "path";
|
|
3648
|
+
|
|
3583
3649
|
// src/graph/rank.ts
|
|
3584
3650
|
var STOPWORDS2 = /* @__PURE__ */ new Set([
|
|
3585
3651
|
"a",
|
|
@@ -3892,7 +3958,7 @@ async function writeContextMd(path, ctx) {
|
|
|
3892
3958
|
// src/memory/context-store.ts
|
|
3893
3959
|
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
3894
3960
|
import { dirname as dirname8 } from "path";
|
|
3895
|
-
var
|
|
3961
|
+
var SCHEMA_VERSION3 = 1;
|
|
3896
3962
|
async function readEntries(path) {
|
|
3897
3963
|
try {
|
|
3898
3964
|
const raw = await readFile13(path, "utf8");
|
|
@@ -3904,7 +3970,7 @@ async function readEntries(path) {
|
|
|
3904
3970
|
}
|
|
3905
3971
|
async function writeEntries(path, entries) {
|
|
3906
3972
|
await mkdir7(dirname8(path), { recursive: true });
|
|
3907
|
-
const store = { schema_version:
|
|
3973
|
+
const store = { schema_version: SCHEMA_VERSION3, entries };
|
|
3908
3974
|
await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3909
3975
|
}
|
|
3910
3976
|
async function appendEntry(path, entry) {
|
|
@@ -4429,7 +4495,10 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
4429
4495
|
async function graphContinue(args, ctx) {
|
|
4430
4496
|
const query = typeof args?.query === "string" ? args.query : "";
|
|
4431
4497
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|
|
4432
|
-
const retrieval = await retrieve(ctx.graph, query
|
|
4498
|
+
const retrieval = await retrieve(ctx.graph, query, {
|
|
4499
|
+
recentlyEditedPaths: ctx.activity.recentFilePaths(15 * 60 * 1e3),
|
|
4500
|
+
sessionKnownPaths: getRegisteredEdits()
|
|
4501
|
+
});
|
|
4433
4502
|
const packed = await pack(retrieval.files, { query, graph: ctx.graph });
|
|
4434
4503
|
const header = `Confidence: ${retrieval.confidence}
|
|
4435
4504
|
Files: ${retrieval.files.map((f) => f.path).join(", ") || "(none)"}
|
|
@@ -4491,6 +4560,9 @@ function graphRegisterEdit(args, _ctx) {
|
|
|
4491
4560
|
for (const f of files) editedFiles.add(f);
|
|
4492
4561
|
return textContent(`Registered ${files.length} edited file(s). Total tracked this session: ${editedFiles.size}.`);
|
|
4493
4562
|
}
|
|
4563
|
+
function getRegisteredEdits() {
|
|
4564
|
+
return Array.from(editedFiles);
|
|
4565
|
+
}
|
|
4494
4566
|
var VALID_KINDS = /* @__PURE__ */ new Set(["decision", "task", "next", "fact", "blocker"]);
|
|
4495
4567
|
async function contextRemember(args, ctx) {
|
|
4496
4568
|
const text = typeof args?.text === "string" ? args.text.trim() : "";
|
|
@@ -4554,6 +4626,17 @@ async function contextRecall(args, ctx) {
|
|
|
4554
4626
|
}
|
|
4555
4627
|
return textContent(lines.join("\n"));
|
|
4556
4628
|
}
|
|
4629
|
+
async function logToolCall(ctx, tool) {
|
|
4630
|
+
try {
|
|
4631
|
+
await mkdir8(dirname9(ctx.paths.toolLog), { recursive: true });
|
|
4632
|
+
await appendFile2(
|
|
4633
|
+
ctx.paths.toolLog,
|
|
4634
|
+
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
4635
|
+
"utf8"
|
|
4636
|
+
);
|
|
4637
|
+
} catch {
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4557
4640
|
async function handleMcpRequest(body, ctx) {
|
|
4558
4641
|
if (!body || typeof body !== "object") {
|
|
4559
4642
|
return err(null, ERR.invalidRequest, "Request body must be a JSON-RPC 2.0 object.");
|
|
@@ -4580,6 +4663,7 @@ async function handleMcpRequest(body, ctx) {
|
|
|
4580
4663
|
const toolName = typeof params.name === "string" ? params.name : "";
|
|
4581
4664
|
if (!toolName) return err(id, ERR.invalidParams, "'name' is required for tools/call.");
|
|
4582
4665
|
const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
|
|
4666
|
+
void logToolCall(ctx, toolName);
|
|
4583
4667
|
const result = await callTool(toolName, args, ctx);
|
|
4584
4668
|
return ok(id, result);
|
|
4585
4669
|
}
|
|
@@ -4615,8 +4699,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4615
4699
|
}
|
|
4616
4700
|
|
|
4617
4701
|
// src/server/routes/gate.ts
|
|
4618
|
-
import { appendFile as
|
|
4619
|
-
import { dirname as
|
|
4702
|
+
import { appendFile as appendFile3, mkdir as mkdir9 } from "fs/promises";
|
|
4703
|
+
import { dirname as dirname10 } from "path";
|
|
4620
4704
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
4621
4705
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
4622
4706
|
function extractQuery(toolName, input) {
|
|
@@ -4672,7 +4756,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
4672
4756
|
}
|
|
4673
4757
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
4674
4758
|
try {
|
|
4675
|
-
await
|
|
4759
|
+
await mkdir9(dirname10(ctx.paths.gateLog), { recursive: true });
|
|
4676
4760
|
const entry = {
|
|
4677
4761
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4678
4762
|
tool: toolName,
|
|
@@ -4680,7 +4764,7 @@ async function logDecision(ctx, toolName, query, decision, reason) {
|
|
|
4680
4764
|
query,
|
|
4681
4765
|
reason
|
|
4682
4766
|
};
|
|
4683
|
-
await
|
|
4767
|
+
await appendFile3(ctx.paths.gateLog, JSON.stringify(entry) + "\n", "utf8");
|
|
4684
4768
|
} catch {
|
|
4685
4769
|
}
|
|
4686
4770
|
}
|
|
@@ -4744,16 +4828,16 @@ async function handleGate(req, ctx) {
|
|
|
4744
4828
|
}
|
|
4745
4829
|
|
|
4746
4830
|
// src/server/routes/log.ts
|
|
4747
|
-
import { appendFile as
|
|
4748
|
-
import { dirname as
|
|
4831
|
+
import { appendFile as appendFile4, mkdir as mkdir10 } from "fs/promises";
|
|
4832
|
+
import { dirname as dirname11 } from "path";
|
|
4749
4833
|
async function handleLog(entry, ctx) {
|
|
4750
4834
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4751
4835
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4752
4836
|
}
|
|
4753
4837
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4754
4838
|
const record = { ...entry, written_at };
|
|
4755
|
-
await
|
|
4756
|
-
await
|
|
4839
|
+
await mkdir10(dirname11(ctx.paths.tokenLog), { recursive: true });
|
|
4840
|
+
await appendFile4(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4757
4841
|
return { ok: true, written_at };
|
|
4758
4842
|
}
|
|
4759
4843
|
|
|
@@ -4804,10 +4888,20 @@ ${fileCount} files indexed, ${symbolCount} symbols. Prefer the graph_* MCP tools
|
|
|
4804
4888
|
// src/server/http.ts
|
|
4805
4889
|
async function loadContext(paths) {
|
|
4806
4890
|
try {
|
|
4807
|
-
|
|
4891
|
+
let [graph, symbolIndex] = await Promise.all([
|
|
4808
4892
|
readGraph(paths.infoGraph),
|
|
4809
4893
|
readSymbolIndex(paths.symbolIndex)
|
|
4810
4894
|
]);
|
|
4895
|
+
if (graph.schema_version !== SCHEMA_VERSION2) {
|
|
4896
|
+
log.info(
|
|
4897
|
+
`graph schema v${graph.schema_version} \u2260 current v${SCHEMA_VERSION2} \u2014 rescanning\u2026`
|
|
4898
|
+
);
|
|
4899
|
+
await scanProject(paths.projectRoot, { silent: true });
|
|
4900
|
+
[graph, symbolIndex] = await Promise.all([
|
|
4901
|
+
readGraph(paths.infoGraph),
|
|
4902
|
+
readSymbolIndex(paths.symbolIndex)
|
|
4903
|
+
]);
|
|
4904
|
+
}
|
|
4811
4905
|
const activity = new ActivityStore(paths.activityLog);
|
|
4812
4906
|
return { paths, graph, symbolIndex, activity };
|
|
4813
4907
|
} catch (err2) {
|
|
@@ -4910,8 +5004,8 @@ async function startServer(paths, options = {}) {
|
|
|
4910
5004
|
async stop() {
|
|
4911
5005
|
await fileWatcher.stop().catch(() => void 0);
|
|
4912
5006
|
await gitWatcher.stop().catch(() => void 0);
|
|
4913
|
-
await new Promise((
|
|
4914
|
-
nodeServer.close((err2) => err2 ? reject(err2) :
|
|
5007
|
+
await new Promise((resolve6, reject) => {
|
|
5008
|
+
nodeServer.close((err2) => err2 ? reject(err2) : resolve6());
|
|
4915
5009
|
});
|
|
4916
5010
|
}
|
|
4917
5011
|
};
|
|
@@ -5014,15 +5108,143 @@ async function dashboardCommand(rawPath) {
|
|
|
5014
5108
|
});
|
|
5015
5109
|
}
|
|
5016
5110
|
|
|
5111
|
+
// src/cli/doctor-command.ts
|
|
5112
|
+
import { readFile as readFile14, stat as stat4 } from "fs/promises";
|
|
5113
|
+
import { join as join10, resolve as resolve3 } from "path";
|
|
5114
|
+
import spawn from "cross-spawn";
|
|
5115
|
+
var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
5116
|
+
function binWorks(bin, args) {
|
|
5117
|
+
return new Promise((res) => {
|
|
5118
|
+
let proc;
|
|
5119
|
+
try {
|
|
5120
|
+
proc = spawn(bin, args, { stdio: "ignore" });
|
|
5121
|
+
} catch {
|
|
5122
|
+
res(false);
|
|
5123
|
+
return;
|
|
5124
|
+
}
|
|
5125
|
+
proc.on("error", () => res(false));
|
|
5126
|
+
proc.on("exit", (code) => res(code === 0));
|
|
5127
|
+
});
|
|
5128
|
+
}
|
|
5129
|
+
async function exists2(path) {
|
|
5130
|
+
try {
|
|
5131
|
+
await stat4(path);
|
|
5132
|
+
return true;
|
|
5133
|
+
} catch {
|
|
5134
|
+
return false;
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
async function runDoctorChecks(projectRoot) {
|
|
5138
|
+
const paths = resolvePaths(projectRoot);
|
|
5139
|
+
const cfg = loadConfig();
|
|
5140
|
+
const checks = [];
|
|
5141
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
5142
|
+
checks.push(
|
|
5143
|
+
nodeMajor >= 18 ? { status: "ok", label: "Node", detail: `v${process.versions.node}` } : { status: "fail", label: "Node", detail: `v${process.versions.node} \u2014 Synthra needs Node >= 18` }
|
|
5144
|
+
);
|
|
5145
|
+
const hasJq = await binWorks("jq", ["--version"]);
|
|
5146
|
+
if (process.platform === "win32") {
|
|
5147
|
+
checks.push({
|
|
5148
|
+
status: "ok",
|
|
5149
|
+
label: "jq",
|
|
5150
|
+
detail: hasJq ? "present (not required \u2014 Windows uses .ps1 hooks)" : "not required on Windows (.ps1 hooks)"
|
|
5151
|
+
});
|
|
5152
|
+
} else {
|
|
5153
|
+
checks.push(
|
|
5154
|
+
hasJq ? { status: "ok", label: "jq", detail: "present" } : {
|
|
5155
|
+
status: "warn",
|
|
5156
|
+
label: "jq",
|
|
5157
|
+
detail: "missing \u2014 Stop/PreToolUse bash hooks silently no-op (no token logging or gating). Install jq (brew/apt)."
|
|
5158
|
+
}
|
|
5159
|
+
);
|
|
5160
|
+
}
|
|
5161
|
+
const hasClaude = await binWorks(cfg.claudeBin, ["--version"]);
|
|
5162
|
+
checks.push(
|
|
5163
|
+
hasClaude ? { status: "ok", label: "claude CLI", detail: `'${cfg.claudeBin}' on PATH` } : {
|
|
5164
|
+
status: "warn",
|
|
5165
|
+
label: "claude CLI",
|
|
5166
|
+
detail: `'${cfg.claudeBin}' not found \u2014 MCP registration + IDE need it (set SYN_CLAUDE_BIN to override).`
|
|
5167
|
+
}
|
|
5168
|
+
);
|
|
5169
|
+
if (!await exists2(paths.infoGraph)) {
|
|
5170
|
+
checks.push({ status: "warn", label: "Graph", detail: "no info_graph.json \u2014 run `syn .` (or `syn scan`) here." });
|
|
5171
|
+
} else {
|
|
5172
|
+
try {
|
|
5173
|
+
const graph = JSON.parse(await readFile14(paths.infoGraph, "utf8"));
|
|
5174
|
+
const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
|
|
5175
|
+
let status = "ok";
|
|
5176
|
+
const ageMs = Date.now() - Date.parse(graph.generated_at);
|
|
5177
|
+
if (Number.isFinite(ageMs)) parts.push(`scanned ${Math.max(0, Math.round(ageMs / 6e4))}m ago`);
|
|
5178
|
+
if (graph.schema_version !== SCHEMA_VERSION2) {
|
|
5179
|
+
status = "warn";
|
|
5180
|
+
parts.push(`schema v${graph.schema_version} \u2260 v${SCHEMA_VERSION2} (auto-rescans on serve)`);
|
|
5181
|
+
}
|
|
5182
|
+
if (graph.symbol_count === 0) {
|
|
5183
|
+
status = "warn";
|
|
5184
|
+
parts.push("0 symbols \u2014 unsupported language or nothing indexed");
|
|
5185
|
+
}
|
|
5186
|
+
checks.push({ status, label: "Graph", detail: parts.join(" \xB7 ") });
|
|
5187
|
+
} catch {
|
|
5188
|
+
checks.push({ status: "warn", label: "Graph", detail: "info_graph.json unreadable \u2014 re-run `syn scan`." });
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
checks.push(
|
|
5192
|
+
await exists2(join10(projectRoot, ".mcp.json")) ? { status: "ok", label: "MCP registration", detail: ".mcp.json present (IDE can see graph_* tools)" } : {
|
|
5193
|
+
status: "warn",
|
|
5194
|
+
label: "MCP registration",
|
|
5195
|
+
detail: "no .mcp.json \u2014 the IDE extension won't see Synthra's tools; run `syn .`."
|
|
5196
|
+
}
|
|
5197
|
+
);
|
|
5198
|
+
if (!await exists2(paths.claudeMd)) {
|
|
5199
|
+
checks.push({ status: "warn", label: "CLAUDE.md policy", detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block." });
|
|
5200
|
+
} else {
|
|
5201
|
+
const md = await readFile14(paths.claudeMd, "utf8");
|
|
5202
|
+
if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
|
|
5203
|
+
checks.push({ status: "ok", label: "CLAUDE.md policy", detail: `policy block v${POLICY_VERSION}` });
|
|
5204
|
+
} else {
|
|
5205
|
+
const m = md.match(/synthra-policy v(\d+) BEGIN/);
|
|
5206
|
+
checks.push({
|
|
5207
|
+
status: "warn",
|
|
5208
|
+
label: "CLAUDE.md policy",
|
|
5209
|
+
detail: m ? `policy block is v${m[1]}, current is v${POLICY_VERSION} \u2014 re-run \`syn .\` to refresh.` : "no synthra-policy block \u2014 run `syn .`."
|
|
5210
|
+
});
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
if (!await exists2(paths.claudeSettings)) {
|
|
5214
|
+
checks.push({ status: "warn", label: "Hooks", detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks." });
|
|
5215
|
+
} else {
|
|
5216
|
+
const s = await readFile14(paths.claudeSettings, "utf8");
|
|
5217
|
+
checks.push(
|
|
5218
|
+
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : { status: "warn", label: "Hooks", detail: "settings.local.json present but no Synthra hooks \u2014 run `syn .`." }
|
|
5219
|
+
);
|
|
5220
|
+
}
|
|
5221
|
+
return checks;
|
|
5222
|
+
}
|
|
5223
|
+
async function doctorCommand(rawPath) {
|
|
5224
|
+
const projectRoot = resolve3(rawPath);
|
|
5225
|
+
const checks = await runDoctorChecks(projectRoot);
|
|
5226
|
+
log.info("");
|
|
5227
|
+
log.info(` Synthra doctor \u2014 ${projectRoot}`);
|
|
5228
|
+
log.info("");
|
|
5229
|
+
for (const c of checks) {
|
|
5230
|
+
log.info(` ${ICON[c.status]} ${c.label.padEnd(18)}${c.detail}`);
|
|
5231
|
+
}
|
|
5232
|
+
const warn = checks.filter((c) => c.status === "warn").length;
|
|
5233
|
+
const fail = checks.filter((c) => c.status === "fail").length;
|
|
5234
|
+
log.info("");
|
|
5235
|
+
log.info(fail === 0 && warn === 0 ? " All checks passed." : ` ${fail} failed \xB7 ${warn} warning(s).`);
|
|
5236
|
+
log.info("");
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5017
5239
|
// src/cli/self-update.ts
|
|
5018
|
-
import { mkdir as
|
|
5240
|
+
import { mkdir as mkdir11, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
|
|
5019
5241
|
import { homedir as homedir3 } from "os";
|
|
5020
|
-
import { join as
|
|
5242
|
+
import { join as join11 } from "path";
|
|
5021
5243
|
import { createInterface } from "readline/promises";
|
|
5022
|
-
import
|
|
5244
|
+
import spawn2 from "cross-spawn";
|
|
5023
5245
|
var PKG_NAME = "@jefuriiij/synthra";
|
|
5024
|
-
var SYNTHRA_DIR =
|
|
5025
|
-
var LAST_SEEN_PATH =
|
|
5246
|
+
var SYNTHRA_DIR = join11(homedir3(), ".synthra");
|
|
5247
|
+
var LAST_SEEN_PATH = join11(SYNTHRA_DIR, "last-seen-version.json");
|
|
5026
5248
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
|
|
5027
5249
|
var FETCH_TIMEOUT_MS = 2e3;
|
|
5028
5250
|
var currentVersionCache = null;
|
|
@@ -5073,7 +5295,7 @@ async function checkForUpdate() {
|
|
|
5073
5295
|
}
|
|
5074
5296
|
async function readLastSeen() {
|
|
5075
5297
|
try {
|
|
5076
|
-
const raw = await
|
|
5298
|
+
const raw = await readFile15(LAST_SEEN_PATH, "utf8");
|
|
5077
5299
|
const parsed = JSON.parse(raw);
|
|
5078
5300
|
return parsed.version ?? null;
|
|
5079
5301
|
} catch {
|
|
@@ -5082,22 +5304,22 @@ async function readLastSeen() {
|
|
|
5082
5304
|
}
|
|
5083
5305
|
async function writeLastSeen(version) {
|
|
5084
5306
|
try {
|
|
5085
|
-
await
|
|
5307
|
+
await mkdir11(SYNTHRA_DIR, { recursive: true });
|
|
5086
5308
|
const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5087
5309
|
await writeFile9(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
5088
5310
|
} catch {
|
|
5089
5311
|
}
|
|
5090
5312
|
}
|
|
5091
5313
|
function npmGlobalRoot() {
|
|
5092
|
-
return new Promise((
|
|
5314
|
+
return new Promise((resolve6) => {
|
|
5093
5315
|
const chunks = [];
|
|
5094
|
-
const proc =
|
|
5316
|
+
const proc = spawn2("npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
5095
5317
|
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
5096
|
-
proc.on("error", () =>
|
|
5318
|
+
proc.on("error", () => resolve6(null));
|
|
5097
5319
|
proc.on("exit", (code) => {
|
|
5098
|
-
if (code !== 0) return
|
|
5320
|
+
if (code !== 0) return resolve6(null);
|
|
5099
5321
|
const out = Buffer.concat(chunks).toString("utf8").trim();
|
|
5100
|
-
|
|
5322
|
+
resolve6(out || null);
|
|
5101
5323
|
});
|
|
5102
5324
|
});
|
|
5103
5325
|
}
|
|
@@ -5116,7 +5338,7 @@ async function readInstalledChangelog() {
|
|
|
5116
5338
|
const root = await npmGlobalRoot();
|
|
5117
5339
|
if (!root) return null;
|
|
5118
5340
|
try {
|
|
5119
|
-
return await
|
|
5341
|
+
return await readFile15(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
5120
5342
|
} catch {
|
|
5121
5343
|
return null;
|
|
5122
5344
|
}
|
|
@@ -5160,12 +5382,12 @@ async function promptYesNo(question) {
|
|
|
5160
5382
|
}
|
|
5161
5383
|
}
|
|
5162
5384
|
function runNpmUpdate() {
|
|
5163
|
-
return new Promise((
|
|
5164
|
-
const proc =
|
|
5385
|
+
return new Promise((resolve6) => {
|
|
5386
|
+
const proc = spawn2("npm", ["install", "-g", PKG_NAME + "@latest"], {
|
|
5165
5387
|
stdio: "inherit"
|
|
5166
5388
|
});
|
|
5167
|
-
proc.on("error", () =>
|
|
5168
|
-
proc.on("exit", (code) =>
|
|
5389
|
+
proc.on("error", () => resolve6(false));
|
|
5390
|
+
proc.on("exit", (code) => resolve6(code === 0));
|
|
5169
5391
|
});
|
|
5170
5392
|
}
|
|
5171
5393
|
async function promptForUpdateOrLog() {
|
|
@@ -5200,13 +5422,13 @@ async function promptForUpdateOrLog() {
|
|
|
5200
5422
|
}
|
|
5201
5423
|
|
|
5202
5424
|
// src/cli/serve-command.ts
|
|
5203
|
-
import { resolve as
|
|
5204
|
-
import { stat as
|
|
5425
|
+
import { resolve as resolve4 } from "path";
|
|
5426
|
+
import { stat as stat5 } from "fs/promises";
|
|
5205
5427
|
async function serveCommand(rawPath) {
|
|
5206
|
-
const projectRoot =
|
|
5428
|
+
const projectRoot = resolve4(rawPath);
|
|
5207
5429
|
const paths = resolvePaths(projectRoot);
|
|
5208
5430
|
try {
|
|
5209
|
-
await
|
|
5431
|
+
await stat5(paths.infoGraph);
|
|
5210
5432
|
} catch {
|
|
5211
5433
|
log.error(`no graph found at ${paths.infoGraph}`);
|
|
5212
5434
|
log.error("run `syn scan` in this project first.");
|
|
@@ -5230,11 +5452,11 @@ async function serveCommand(rawPath) {
|
|
|
5230
5452
|
}
|
|
5231
5453
|
|
|
5232
5454
|
// src/cli/start-claude.ts
|
|
5233
|
-
import
|
|
5455
|
+
import spawn3 from "cross-spawn";
|
|
5234
5456
|
var MCP_NAME = "synthra";
|
|
5235
5457
|
function runClaude(bin, args, cwd, stdio = "pipe") {
|
|
5236
|
-
return new Promise((
|
|
5237
|
-
const proc =
|
|
5458
|
+
return new Promise((resolve6) => {
|
|
5459
|
+
const proc = spawn3(bin, args, {
|
|
5238
5460
|
cwd,
|
|
5239
5461
|
stdio: stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
5240
5462
|
});
|
|
@@ -5242,8 +5464,8 @@ function runClaude(bin, args, cwd, stdio = "pipe") {
|
|
|
5242
5464
|
let stderr = "";
|
|
5243
5465
|
proc.stdout?.on("data", (c) => stdout += String(c));
|
|
5244
5466
|
proc.stderr?.on("data", (c) => stderr += String(c));
|
|
5245
|
-
proc.on("error", () =>
|
|
5246
|
-
proc.on("exit", (code) =>
|
|
5467
|
+
proc.on("error", () => resolve6({ code: -1, stdout, stderr: stderr || "claude not on PATH" }));
|
|
5468
|
+
proc.on("exit", (code) => resolve6({ code: code ?? 0, stdout, stderr }));
|
|
5247
5469
|
});
|
|
5248
5470
|
}
|
|
5249
5471
|
async function registerMcp(bin, mcpPort, cwd) {
|
|
@@ -5299,11 +5521,11 @@ function printReadyBanner(info) {
|
|
|
5299
5521
|
log.info("");
|
|
5300
5522
|
}
|
|
5301
5523
|
function waitForSignal() {
|
|
5302
|
-
return new Promise((
|
|
5524
|
+
return new Promise((resolve6) => {
|
|
5303
5525
|
const handler = (sig) => {
|
|
5304
5526
|
process.off("SIGINT", handler);
|
|
5305
5527
|
process.off("SIGTERM", handler);
|
|
5306
|
-
|
|
5528
|
+
resolve6(sig);
|
|
5307
5529
|
};
|
|
5308
5530
|
process.on("SIGINT", handler);
|
|
5309
5531
|
process.on("SIGTERM", handler);
|
|
@@ -5311,7 +5533,7 @@ function waitForSignal() {
|
|
|
5311
5533
|
}
|
|
5312
5534
|
async function defaultFlow(rawPath, opts) {
|
|
5313
5535
|
const launchCli = opts["launch-cli"] === true;
|
|
5314
|
-
const projectRoot =
|
|
5536
|
+
const projectRoot = resolve5(rawPath);
|
|
5315
5537
|
const paths = resolvePaths(projectRoot);
|
|
5316
5538
|
const cfg = loadConfig();
|
|
5317
5539
|
await runStartupChangelogCheck();
|
|
@@ -5378,6 +5600,9 @@ function buildProgram() {
|
|
|
5378
5600
|
prog.command("dashboard [path]", "Run the token dashboard server (localhost:8901).").action(async (path) => {
|
|
5379
5601
|
await dashboardCommand(path ?? ".");
|
|
5380
5602
|
});
|
|
5603
|
+
prog.command("doctor [path]", "Diagnose this project's Synthra setup + environment.").action(async (path) => {
|
|
5604
|
+
await doctorCommand(path ?? ".");
|
|
5605
|
+
});
|
|
5381
5606
|
return prog;
|
|
5382
5607
|
}
|
|
5383
5608
|
async function main(argv) {
|