@jefuriiij/synthra 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/cli/index.js +591 -172
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +388 -39
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +98 -28
- 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.
|
|
21
|
+
version: "0.7.0",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -145,12 +145,230 @@ function isFree(port) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// src/dashboard/arsenal.ts
|
|
149
|
+
import { readFile, readdir } from "fs/promises";
|
|
150
|
+
import { homedir } from "os";
|
|
151
|
+
import { basename, dirname, join } from "path";
|
|
152
|
+
var DESC_MAX = 300;
|
|
153
|
+
var TOOLS_MAX = 200;
|
|
154
|
+
async function readText(path) {
|
|
155
|
+
try {
|
|
156
|
+
return await readFile(path, "utf8");
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function readJson(path) {
|
|
162
|
+
const text = await readText(path);
|
|
163
|
+
if (text === null) return null;
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(text);
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function listNames(dir) {
|
|
171
|
+
try {
|
|
172
|
+
return await readdir(dir);
|
|
173
|
+
} catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function clip(s, max) {
|
|
178
|
+
const t = s.trim();
|
|
179
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
180
|
+
}
|
|
181
|
+
function parseFrontmatter(md) {
|
|
182
|
+
const m = md.match(/^?\s*---\r?\n([\s\S]*?)\r?\n---/);
|
|
183
|
+
if (!m) return {};
|
|
184
|
+
const lines = (m[1] ?? "").split(/\r?\n/);
|
|
185
|
+
const out = {};
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
const line = lines[i] ?? "";
|
|
188
|
+
const kv = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
|
|
189
|
+
if (!kv) continue;
|
|
190
|
+
const key = kv[1] ?? "";
|
|
191
|
+
if (!key) continue;
|
|
192
|
+
let val = kv[2] ?? "";
|
|
193
|
+
if (val.startsWith('"') && !/[^\\]"\s*$/.test(val.slice(1)) || val === '"') {
|
|
194
|
+
const buf = [val];
|
|
195
|
+
while (i + 1 < lines.length && !/"\s*$/.test(buf[buf.length - 1] ?? "")) {
|
|
196
|
+
i += 1;
|
|
197
|
+
buf.push(lines[i] ?? "");
|
|
198
|
+
}
|
|
199
|
+
val = buf.join(" ");
|
|
200
|
+
}
|
|
201
|
+
val = val.trim().replace(/^["']|["']$/g, "").trim();
|
|
202
|
+
out[key] = val;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function skillItem(fm, fallbackName, scope, source) {
|
|
207
|
+
const meta = {};
|
|
208
|
+
if (fm["argument-hint"]) meta.argument_hint = fm["argument-hint"];
|
|
209
|
+
if (fm["user-invocable"]) meta.user_invocable = fm["user-invocable"];
|
|
210
|
+
return {
|
|
211
|
+
name: fm.name || fallbackName,
|
|
212
|
+
description: clip(fm.description || "", DESC_MAX),
|
|
213
|
+
scope,
|
|
214
|
+
...source ? { source } : {},
|
|
215
|
+
...Object.keys(meta).length ? { meta } : {}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function agentItem(fm, fallbackName, scope, source) {
|
|
219
|
+
const meta = {};
|
|
220
|
+
if (fm.tools) meta.tools = clip(fm.tools, TOOLS_MAX);
|
|
221
|
+
if (fm.model) meta.model = fm.model;
|
|
222
|
+
return {
|
|
223
|
+
name: fm.name || fallbackName,
|
|
224
|
+
description: clip(fm.description || "", DESC_MAX),
|
|
225
|
+
scope,
|
|
226
|
+
...source ? { source } : {},
|
|
227
|
+
...Object.keys(meta).length ? { meta } : {}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function scanSkillsDir(dir, scope, source, out) {
|
|
231
|
+
for (const name of await listNames(dir)) {
|
|
232
|
+
const md = await readText(join(dir, name, "SKILL.md"));
|
|
233
|
+
if (md === null) continue;
|
|
234
|
+
out.push(skillItem(parseFrontmatter(md), name, scope, source));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function scanAgentsDir(dir, scope, source, out) {
|
|
238
|
+
for (const file of await listNames(dir)) {
|
|
239
|
+
if (!file.endsWith(".md")) continue;
|
|
240
|
+
const md = await readText(join(dir, file));
|
|
241
|
+
if (md === null) continue;
|
|
242
|
+
out.push(agentItem(parseFrontmatter(md), basename(file, ".md"), scope, source));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function mcpItemsFrom(json, scope, source) {
|
|
246
|
+
if (!json || typeof json !== "object") return [];
|
|
247
|
+
const record = json;
|
|
248
|
+
const servers = record.mcpServers && typeof record.mcpServers === "object" ? record.mcpServers : record;
|
|
249
|
+
const items = [];
|
|
250
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
251
|
+
if (!raw || typeof raw !== "object") continue;
|
|
252
|
+
const cfg = raw;
|
|
253
|
+
const type = typeof cfg.type === "string" ? cfg.type : cfg.command ? "stdio" : "http";
|
|
254
|
+
const url = typeof cfg.url === "string" ? cfg.url.split("?")[0] : typeof cfg.command === "string" ? cfg.command : "";
|
|
255
|
+
const meta = { type };
|
|
256
|
+
if (url) meta.url = url;
|
|
257
|
+
items.push({ name, description: "", scope, ...source ? { source } : {}, meta });
|
|
258
|
+
}
|
|
259
|
+
return items;
|
|
260
|
+
}
|
|
261
|
+
var SCOPE_ORDER = { project: 0, personal: 1, plugin: 2 };
|
|
262
|
+
function sortItems(items) {
|
|
263
|
+
return items.sort((a, b) => {
|
|
264
|
+
if (a.scope !== b.scope) return SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
|
|
265
|
+
const sa = a.source ?? "";
|
|
266
|
+
const sb = b.source ?? "";
|
|
267
|
+
if (sa !== sb) return sa < sb ? -1 : 1;
|
|
268
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
var cache = null;
|
|
272
|
+
var CACHE_TTL_MS = 15e3;
|
|
273
|
+
async function computeArsenal(projectRoot, homeDir = homedir()) {
|
|
274
|
+
const key = `${projectRoot}\0${homeDir}`;
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
if (cache && cache.key === key && now - cache.at < CACHE_TTL_MS) return cache.data;
|
|
277
|
+
const homeClaude = join(homeDir, ".claude");
|
|
278
|
+
const projClaude = join(projectRoot, ".claude");
|
|
279
|
+
const skills = [];
|
|
280
|
+
const agents = [];
|
|
281
|
+
const mcp = [];
|
|
282
|
+
await scanSkillsDir(join(projClaude, "skills"), "project", void 0, skills);
|
|
283
|
+
await scanSkillsDir(join(homeClaude, "skills"), "personal", void 0, skills);
|
|
284
|
+
await scanAgentsDir(join(projClaude, "agents"), "project", void 0, agents);
|
|
285
|
+
await scanAgentsDir(join(homeClaude, "agents"), "personal", void 0, agents);
|
|
286
|
+
mcp.push(...mcpItemsFrom(await readJson(join(projectRoot, ".mcp.json")), "project", void 0));
|
|
287
|
+
mcp.push(
|
|
288
|
+
...mcpItemsFrom(
|
|
289
|
+
(await readJson(join(homeDir, ".claude.json")))?.mcpServers,
|
|
290
|
+
"personal",
|
|
291
|
+
void 0
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
const installedRaw = await readJson(
|
|
295
|
+
join(homeClaude, "plugins", "installed_plugins.json")
|
|
296
|
+
);
|
|
297
|
+
const pluginsMap = installedRaw?.plugins ?? installedRaw ?? {};
|
|
298
|
+
const settings = await readJson(
|
|
299
|
+
join(homeClaude, "settings.json")
|
|
300
|
+
);
|
|
301
|
+
const enabledMap = settings?.enabledPlugins ?? {};
|
|
302
|
+
let pluginCount = 0;
|
|
303
|
+
{
|
|
304
|
+
for (const [pluginKey, entries] of Object.entries(pluginsMap)) {
|
|
305
|
+
const entry = Array.isArray(entries) ? entries[0] : void 0;
|
|
306
|
+
if (!entry?.installPath) continue;
|
|
307
|
+
pluginCount += 1;
|
|
308
|
+
const pluginName = pluginKey.split("@")[0];
|
|
309
|
+
const enabled = enabledMap[pluginKey] !== false;
|
|
310
|
+
const root = entry.installPath;
|
|
311
|
+
const manifest = await readJson(
|
|
312
|
+
join(root, ".claude-plugin", "plugin.json")
|
|
313
|
+
);
|
|
314
|
+
const agentFiles = /* @__PURE__ */ new Set();
|
|
315
|
+
for (const f of await listNames(join(root, "agents"))) {
|
|
316
|
+
if (f.endsWith(".md")) agentFiles.add(join(root, "agents", f));
|
|
317
|
+
}
|
|
318
|
+
for (const rel of manifest?.agents ?? []) agentFiles.add(join(root, rel));
|
|
319
|
+
const pAgents = [];
|
|
320
|
+
for (const file of agentFiles) {
|
|
321
|
+
const md = await readText(file);
|
|
322
|
+
if (md !== null) pAgents.push(agentItem(parseFrontmatter(md), basename(file, ".md"), "plugin", pluginName));
|
|
323
|
+
}
|
|
324
|
+
const skillMds = /* @__PURE__ */ new Set();
|
|
325
|
+
for (const name of await listNames(join(root, "skills"))) {
|
|
326
|
+
skillMds.add(join(root, "skills", name, "SKILL.md"));
|
|
327
|
+
}
|
|
328
|
+
for (const rel of manifest?.skills ?? []) {
|
|
329
|
+
skillMds.add(rel.endsWith(".md") ? join(root, rel) : join(root, rel, "SKILL.md"));
|
|
330
|
+
}
|
|
331
|
+
const pSkills = [];
|
|
332
|
+
for (const md of skillMds) {
|
|
333
|
+
const text = await readText(md);
|
|
334
|
+
if (text !== null) pSkills.push(skillItem(parseFrontmatter(text), basename(dirname(md)), "plugin", pluginName));
|
|
335
|
+
}
|
|
336
|
+
const pMcp = mcpItemsFrom(await readJson(join(root, ".mcp.json")), "plugin", pluginName);
|
|
337
|
+
for (const it of [...pSkills, ...pAgents, ...pMcp]) it.enabled = enabled;
|
|
338
|
+
skills.push(...pSkills);
|
|
339
|
+
agents.push(...pAgents);
|
|
340
|
+
mcp.push(...pMcp);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
const dedupedSkills = skills.filter((s) => {
|
|
345
|
+
const k = `${s.scope}:${s.source ?? ""}:${s.name}`;
|
|
346
|
+
if (seen.has(k)) return false;
|
|
347
|
+
seen.add(k);
|
|
348
|
+
return true;
|
|
349
|
+
});
|
|
350
|
+
const data = {
|
|
351
|
+
skills: sortItems(dedupedSkills),
|
|
352
|
+
agents: sortItems(agents),
|
|
353
|
+
mcp: sortItems(mcp),
|
|
354
|
+
counts: {
|
|
355
|
+
skills: dedupedSkills.length,
|
|
356
|
+
agents: agents.length,
|
|
357
|
+
mcp: mcp.length,
|
|
358
|
+
plugins: pluginCount
|
|
359
|
+
},
|
|
360
|
+
scanned_at: new Date(now).toISOString()
|
|
361
|
+
};
|
|
362
|
+
cache = { key, at: now, data };
|
|
363
|
+
return data;
|
|
364
|
+
}
|
|
365
|
+
|
|
148
366
|
// src/dashboard/delta.ts
|
|
149
|
-
import { readFile as
|
|
367
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
150
368
|
|
|
151
369
|
// src/learn/store.ts
|
|
152
|
-
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
153
|
-
import { dirname } from "path";
|
|
370
|
+
import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
371
|
+
import { dirname as dirname2 } from "path";
|
|
154
372
|
|
|
155
373
|
// src/learn/usage.ts
|
|
156
374
|
var LEARN_SCHEMA_VERSION = 1;
|
|
@@ -217,7 +435,7 @@ function recomputeFromLog(events) {
|
|
|
217
435
|
// src/learn/store.ts
|
|
218
436
|
async function readLearnStore(path) {
|
|
219
437
|
try {
|
|
220
|
-
const raw = await
|
|
438
|
+
const raw = await readFile2(path, "utf8");
|
|
221
439
|
const parsed = JSON.parse(raw);
|
|
222
440
|
if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
223
441
|
return emptyStore();
|
|
@@ -233,14 +451,14 @@ async function readLearnStore(path) {
|
|
|
233
451
|
}
|
|
234
452
|
async function writeLearnStore(path, store) {
|
|
235
453
|
try {
|
|
236
|
-
await mkdir(
|
|
454
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
237
455
|
await writeFile(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
238
456
|
} catch {
|
|
239
457
|
}
|
|
240
458
|
}
|
|
241
459
|
async function readAccessLog(path) {
|
|
242
460
|
try {
|
|
243
|
-
const raw = await
|
|
461
|
+
const raw = await readFile2(path, "utf8");
|
|
244
462
|
const out = [];
|
|
245
463
|
for (const line of raw.split("\n")) {
|
|
246
464
|
const t = line.trim();
|
|
@@ -260,43 +478,43 @@ async function readAccessLog(path) {
|
|
|
260
478
|
}
|
|
261
479
|
async function appendAccess(path, ev) {
|
|
262
480
|
try {
|
|
263
|
-
await mkdir(
|
|
481
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
264
482
|
await appendFile(path, JSON.stringify(ev) + "\n", "utf8");
|
|
265
483
|
} catch {
|
|
266
484
|
}
|
|
267
485
|
}
|
|
268
486
|
|
|
269
487
|
// src/shared/paths.ts
|
|
270
|
-
import { join } from "path";
|
|
488
|
+
import { join as join2 } from "path";
|
|
271
489
|
function resolvePaths(projectRoot) {
|
|
272
|
-
const graphDir =
|
|
273
|
-
const contextDir =
|
|
274
|
-
const claudeDir =
|
|
490
|
+
const graphDir = join2(projectRoot, ".synthra-graph");
|
|
491
|
+
const contextDir = join2(projectRoot, ".synthra");
|
|
492
|
+
const claudeDir = join2(projectRoot, ".claude");
|
|
275
493
|
return {
|
|
276
494
|
projectRoot,
|
|
277
495
|
graphDir,
|
|
278
496
|
contextDir,
|
|
279
|
-
infoGraph:
|
|
280
|
-
symbolIndex:
|
|
281
|
-
sessionState:
|
|
282
|
-
activityLog:
|
|
283
|
-
tokenLog:
|
|
284
|
-
gateLog:
|
|
285
|
-
toolLog:
|
|
286
|
-
accessLog:
|
|
287
|
-
learnStore:
|
|
288
|
-
parseCache:
|
|
289
|
-
mcpPort:
|
|
290
|
-
mcpServerLog:
|
|
291
|
-
mcpServerErrLog:
|
|
292
|
-
contextStore:
|
|
293
|
-
contextMd:
|
|
294
|
-
branchesDir:
|
|
497
|
+
infoGraph: join2(graphDir, "info_graph.json"),
|
|
498
|
+
symbolIndex: join2(graphDir, "symbol_index.json"),
|
|
499
|
+
sessionState: join2(graphDir, "session.json"),
|
|
500
|
+
activityLog: join2(graphDir, "activity.jsonl"),
|
|
501
|
+
tokenLog: join2(graphDir, "token_log.jsonl"),
|
|
502
|
+
gateLog: join2(graphDir, "gate_log.jsonl"),
|
|
503
|
+
toolLog: join2(graphDir, "tool_log.jsonl"),
|
|
504
|
+
accessLog: join2(graphDir, "access_log.jsonl"),
|
|
505
|
+
learnStore: join2(graphDir, "learn_store.json"),
|
|
506
|
+
parseCache: join2(graphDir, "parse_cache.json"),
|
|
507
|
+
mcpPort: join2(graphDir, "mcp_port"),
|
|
508
|
+
mcpServerLog: join2(graphDir, "mcp_server.log"),
|
|
509
|
+
mcpServerErrLog: join2(graphDir, "mcp_server.err.log"),
|
|
510
|
+
contextStore: join2(contextDir, "context-store.json"),
|
|
511
|
+
contextMd: join2(contextDir, "CONTEXT.md"),
|
|
512
|
+
branchesDir: join2(contextDir, "branches"),
|
|
295
513
|
claudeDir,
|
|
296
|
-
claudeSettings:
|
|
297
|
-
claudeHooksDir:
|
|
298
|
-
claudeMd:
|
|
299
|
-
gitignore:
|
|
514
|
+
claudeSettings: join2(claudeDir, "settings.local.json"),
|
|
515
|
+
claudeHooksDir: join2(claudeDir, "hooks"),
|
|
516
|
+
claudeMd: join2(projectRoot, "CLAUDE.md"),
|
|
517
|
+
gitignore: join2(projectRoot, ".gitignore")
|
|
300
518
|
};
|
|
301
519
|
}
|
|
302
520
|
|
|
@@ -332,15 +550,15 @@ function estimateCostUsd(usage) {
|
|
|
332
550
|
}
|
|
333
551
|
|
|
334
552
|
// src/shared/project-registry.ts
|
|
335
|
-
import { mkdir as mkdir2, readFile as
|
|
336
|
-
import { homedir } from "os";
|
|
337
|
-
import { basename, dirname as
|
|
338
|
-
var REGISTRY_DIR =
|
|
339
|
-
var REGISTRY_PATH =
|
|
553
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
554
|
+
import { homedir as homedir2 } from "os";
|
|
555
|
+
import { basename as basename2, dirname as dirname3, join as join3 } from "path";
|
|
556
|
+
var REGISTRY_DIR = join3(homedir2(), ".synthra");
|
|
557
|
+
var REGISTRY_PATH = join3(REGISTRY_DIR, "projects.json");
|
|
340
558
|
var SCHEMA_VERSION = 1;
|
|
341
559
|
async function readRegistry() {
|
|
342
560
|
try {
|
|
343
|
-
const raw = await
|
|
561
|
+
const raw = await readFile3(REGISTRY_PATH, "utf8");
|
|
344
562
|
const parsed = JSON.parse(raw);
|
|
345
563
|
if (!Array.isArray(parsed.projects)) return { schema_version: SCHEMA_VERSION, projects: [] };
|
|
346
564
|
return { schema_version: parsed.schema_version ?? SCHEMA_VERSION, projects: parsed.projects };
|
|
@@ -349,7 +567,7 @@ async function readRegistry() {
|
|
|
349
567
|
}
|
|
350
568
|
}
|
|
351
569
|
async function writeRegistry(registry) {
|
|
352
|
-
await mkdir2(
|
|
570
|
+
await mkdir2(dirname3(REGISTRY_PATH), { recursive: true });
|
|
353
571
|
await writeFile2(REGISTRY_PATH, JSON.stringify(registry, null, 2) + "\n", "utf8");
|
|
354
572
|
}
|
|
355
573
|
async function recordProject(projectRoot) {
|
|
@@ -358,11 +576,11 @@ async function recordProject(projectRoot) {
|
|
|
358
576
|
const existing = registry.projects.find((p) => p.path === projectRoot);
|
|
359
577
|
if (existing) {
|
|
360
578
|
existing.last_seen = now;
|
|
361
|
-
existing.name =
|
|
579
|
+
existing.name = basename2(projectRoot);
|
|
362
580
|
} else {
|
|
363
581
|
registry.projects.push({
|
|
364
582
|
path: projectRoot,
|
|
365
|
-
name:
|
|
583
|
+
name: basename2(projectRoot),
|
|
366
584
|
first_seen: now,
|
|
367
585
|
last_seen: now
|
|
368
586
|
});
|
|
@@ -392,7 +610,7 @@ function topHotFiles(store, nowMs, limit = 8) {
|
|
|
392
610
|
}
|
|
393
611
|
async function readJsonl(path) {
|
|
394
612
|
try {
|
|
395
|
-
const text = await
|
|
613
|
+
const text = await readFile4(path, "utf8");
|
|
396
614
|
return text.split(/\r?\n/).filter((l) => l.length > 0).map((l) => {
|
|
397
615
|
try {
|
|
398
616
|
return JSON.parse(l);
|
|
@@ -404,7 +622,7 @@ async function readJsonl(path) {
|
|
|
404
622
|
return [];
|
|
405
623
|
}
|
|
406
624
|
}
|
|
407
|
-
function
|
|
625
|
+
function basename3(p) {
|
|
408
626
|
const parts = p.split(/[\\/]/);
|
|
409
627
|
return parts[parts.length - 1] || p;
|
|
410
628
|
}
|
|
@@ -502,7 +720,7 @@ function dedupeTokens(entries) {
|
|
|
502
720
|
async function computeDashboardData(activePaths, recentN = 500) {
|
|
503
721
|
const registered = await listProjects();
|
|
504
722
|
const activePath = activePaths.projectRoot;
|
|
505
|
-
const activeName =
|
|
723
|
+
const activeName = basename3(activePath);
|
|
506
724
|
const knownPaths = new Set(registered.map((p) => p.path));
|
|
507
725
|
const allEntries = [
|
|
508
726
|
...registered.map((p) => ({ path: p.path, name: p.name, last_seen: p.last_seen }))
|
|
@@ -618,6 +836,7 @@ var public_default = `<!doctype html>
|
|
|
618
836
|
|
|
619
837
|
<!-- ============ Top nav ============ -->
|
|
620
838
|
<header class="topnav">
|
|
839
|
+
<button class="arsenal-toggle has-tooltip" id="arsenal-toggle" data-tooltip="Your arsenal \u2014 every skill, agent, and MCP server installed for this project, personally, or via plugins, with descriptions. So you never have to drop to the CLI to remember what's available." aria-label="Toggle arsenal">\u2694 <span>Arsenal</span></button>
|
|
621
840
|
<div class="brand">
|
|
622
841
|
<div class="brand-mark"></div>
|
|
623
842
|
<div class="brand-name">Synth<em>ra</em></div>
|
|
@@ -644,6 +863,26 @@ var public_default = `<!doctype html>
|
|
|
644
863
|
</div>
|
|
645
864
|
</header>
|
|
646
865
|
|
|
866
|
+
<!-- ============ Arsenal drawer ============ -->
|
|
867
|
+
<div class="arsenal-scrim" id="arsenal-scrim"></div>
|
|
868
|
+
<aside class="arsenal-drawer" id="arsenal-drawer" aria-hidden="true">
|
|
869
|
+
<div class="arsenal-head">
|
|
870
|
+
<div class="arsenal-title">\u2694 Arsenal</div>
|
|
871
|
+
<button class="arsenal-icon" id="arsenal-refresh" title="Rescan" aria-label="Rescan">\u21BB</button>
|
|
872
|
+
<button class="arsenal-icon" id="arsenal-close" title="Close" aria-label="Close">\u2715</button>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="arsenal-tabs">
|
|
875
|
+
<button class="arsenal-tab active" data-tab="skills">Skills <span class="at-count" id="at-skills">0</span></button>
|
|
876
|
+
<button class="arsenal-tab" data-tab="agents">Agents <span class="at-count" id="at-agents">0</span></button>
|
|
877
|
+
<button class="arsenal-tab" data-tab="mcp">MCP <span class="at-count" id="at-mcp">0</span></button>
|
|
878
|
+
</div>
|
|
879
|
+
<input class="arsenal-filter" id="arsenal-filter" type="text" placeholder="Filter by name or description\u2026" autocomplete="off" />
|
|
880
|
+
<div class="arsenal-list" id="arsenal-list">
|
|
881
|
+
<div class="empty">Open to load your arsenal\u2026</div>
|
|
882
|
+
</div>
|
|
883
|
+
<div class="arsenal-foot" id="arsenal-foot"></div>
|
|
884
|
+
</aside>
|
|
885
|
+
|
|
647
886
|
<!-- ============ Main 3-column grid ============ -->
|
|
648
887
|
<main class="grid-main">
|
|
649
888
|
|
|
@@ -1572,6 +1811,115 @@ var public_default = `<!doctype html>
|
|
|
1572
1811
|
document.addEventListener('scroll', hideTooltip, true);
|
|
1573
1812
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideTooltip(); });
|
|
1574
1813
|
|
|
1814
|
+
// ----- Arsenal drawer -----
|
|
1815
|
+
let arsenalData = null; // cached payload after first load
|
|
1816
|
+
let arsenalTab = 'skills';
|
|
1817
|
+
let arsenalLoading = false;
|
|
1818
|
+
|
|
1819
|
+
function scopeBadge(item) {
|
|
1820
|
+
const s = item.scope;
|
|
1821
|
+
const label = s === 'plugin' ? (item.source || 'plugin') : s;
|
|
1822
|
+
const dis = (s === 'plugin' && item.enabled === false) ? ' off' : '';
|
|
1823
|
+
return '<span class="scope-badge ' + s + dis + '">' + escapeHtml(label) + (dis ? ' \xB7 off' : '') + '</span>';
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function escapeHtml(s) {
|
|
1827
|
+
return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
|
|
1828
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"' }[c]
|
|
1829
|
+
));
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function renderArsenal() {
|
|
1833
|
+
const listEl = $('#arsenal-list');
|
|
1834
|
+
const footEl = $('#arsenal-foot');
|
|
1835
|
+
if (arsenalLoading) { listEl.innerHTML = '<div class="empty">Scanning\u2026</div>'; return; }
|
|
1836
|
+
if (!arsenalData) { listEl.innerHTML = '<div class="empty">Open to load your arsenal\u2026</div>'; return; }
|
|
1837
|
+
|
|
1838
|
+
$('#at-skills').textContent = arsenalData.counts.skills;
|
|
1839
|
+
$('#at-agents').textContent = arsenalData.counts.agents;
|
|
1840
|
+
$('#at-mcp').textContent = arsenalData.counts.mcp;
|
|
1841
|
+
footEl.textContent = arsenalData.counts.plugins + ' plugins \xB7 scanned ' +
|
|
1842
|
+
new Date(arsenalData.scanned_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1843
|
+
|
|
1844
|
+
const q = ($('#arsenal-filter').value || '').toLowerCase().trim();
|
|
1845
|
+
let items = arsenalData[arsenalTab] || [];
|
|
1846
|
+
if (q) items = items.filter((it) =>
|
|
1847
|
+
it.name.toLowerCase().includes(q) ||
|
|
1848
|
+
(it.description || '').toLowerCase().includes(q) ||
|
|
1849
|
+
(it.source || '').toLowerCase().includes(q));
|
|
1850
|
+
|
|
1851
|
+
listEl.innerHTML = '';
|
|
1852
|
+
if (!items.length) {
|
|
1853
|
+
listEl.innerHTML = '<div class="empty">' + (q ? 'No matches.' : 'Nothing installed in this category.') + '</div>';
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const frag = document.createDocumentFragment();
|
|
1857
|
+
for (const it of items) {
|
|
1858
|
+
const row = document.createElement('div');
|
|
1859
|
+
row.className = 'arsenal-item';
|
|
1860
|
+
const meta = it.meta || {};
|
|
1861
|
+
const metaBits = [];
|
|
1862
|
+
if (meta.model) metaBits.push('model: ' + meta.model);
|
|
1863
|
+
if (meta.type) metaBits.push(meta.type);
|
|
1864
|
+
if (meta.url) metaBits.push(meta.url);
|
|
1865
|
+
if (meta.argument_hint) metaBits.push('args: ' + meta.argument_hint);
|
|
1866
|
+
if (meta.tools) metaBits.push('tools: ' + meta.tools);
|
|
1867
|
+
row.innerHTML =
|
|
1868
|
+
'<div class="ai-head">' +
|
|
1869
|
+
'<span class="ai-name">' + escapeHtml(it.name) + '</span>' +
|
|
1870
|
+
scopeBadge(it) +
|
|
1871
|
+
'</div>' +
|
|
1872
|
+
(it.description ? '<div class="ai-desc">' + escapeHtml(it.description) + '</div>' : '') +
|
|
1873
|
+
(metaBits.length ? '<div class="ai-meta">' + escapeHtml(metaBits.join(' \xB7 ')) + '</div>' : '');
|
|
1874
|
+
row.addEventListener('click', () => row.classList.toggle('open'));
|
|
1875
|
+
frag.appendChild(row);
|
|
1876
|
+
}
|
|
1877
|
+
listEl.appendChild(frag);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
async function loadArsenal(force) {
|
|
1881
|
+
if (arsenalData && !force) { renderArsenal(); return; }
|
|
1882
|
+
arsenalLoading = true; renderArsenal();
|
|
1883
|
+
try {
|
|
1884
|
+
const res = await fetch('/arsenal');
|
|
1885
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1886
|
+
arsenalData = await res.json();
|
|
1887
|
+
} catch (e) {
|
|
1888
|
+
arsenalData = null;
|
|
1889
|
+
$('#arsenal-list').innerHTML = '<div class="empty">Failed to load arsenal.</div>';
|
|
1890
|
+
} finally {
|
|
1891
|
+
arsenalLoading = false;
|
|
1892
|
+
renderArsenal();
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function openArsenal() {
|
|
1897
|
+
$('#arsenal-drawer').classList.add('open');
|
|
1898
|
+
$('#arsenal-drawer').setAttribute('aria-hidden', 'false');
|
|
1899
|
+
$('#arsenal-scrim').classList.add('on');
|
|
1900
|
+
loadArsenal(false);
|
|
1901
|
+
}
|
|
1902
|
+
function closeArsenal() {
|
|
1903
|
+
$('#arsenal-drawer').classList.remove('open');
|
|
1904
|
+
$('#arsenal-drawer').setAttribute('aria-hidden', 'true');
|
|
1905
|
+
$('#arsenal-scrim').classList.remove('on');
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
$('#arsenal-toggle').addEventListener('click', () =>
|
|
1909
|
+
$('#arsenal-drawer').classList.contains('open') ? closeArsenal() : openArsenal());
|
|
1910
|
+
$('#arsenal-close').addEventListener('click', closeArsenal);
|
|
1911
|
+
$('#arsenal-scrim').addEventListener('click', closeArsenal);
|
|
1912
|
+
$('#arsenal-refresh').addEventListener('click', () => loadArsenal(true));
|
|
1913
|
+
$('#arsenal-filter').addEventListener('input', renderArsenal);
|
|
1914
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeArsenal(); });
|
|
1915
|
+
for (const tab of document.querySelectorAll('.arsenal-tab')) {
|
|
1916
|
+
tab.addEventListener('click', () => {
|
|
1917
|
+
arsenalTab = tab.getAttribute('data-tab');
|
|
1918
|
+
for (const t of document.querySelectorAll('.arsenal-tab')) t.classList.toggle('active', t === tab);
|
|
1919
|
+
renderArsenal();
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1575
1923
|
setHeroDate();
|
|
1576
1924
|
tick();
|
|
1577
1925
|
setInterval(tick, 10000);
|
|
@@ -1581,7 +1929,7 @@ var public_default = `<!doctype html>
|
|
|
1581
1929
|
`;
|
|
1582
1930
|
|
|
1583
1931
|
// src/dashboard/public/style.css
|
|
1584
|
-
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-fable: #2EE08F;\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/* Savings + Cost share one row at the top of the center column */\n.hero-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n align-items: stretch;\n flex-shrink: 0;\n}\n\n.hero-row > .card { flex-shrink: 1; }\n\n@media (max-width: 1100px) {\n .hero-row { grid-template-columns: 1fr; }\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.fable {\n background: var(--c-fable);\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.fable {\n color: #6BEFB4;\n border-color: rgba(46, 224, 143, .32);\n background: rgba(46, 224, 143, .07);\n}\n\n.model-pill.fable .sq {\n background: var(--c-fable);\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/* File paths stay as-is (no uppercase/wide tracking) and may overflow \u2014 clip. */\n.cs-k.path {\n text-transform: none;\n letter-spacing: 0.01em;\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14rem;\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/* Hot-files list: cap height + scroll so a long list never squeezes the Moat */\n#hot-files-list {\n max-height: 190px;\n overflow-y: auto;\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#hot-files-list::-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#hot-files-list::-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#hot-files-list::-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 .col-right > *:nth-child(4) { animation-delay: 440ms; }\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';
|
|
1932
|
+
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-fable: #2EE08F;\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/* Savings + Cost share one row at the top of the center column */\n.hero-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n align-items: stretch;\n flex-shrink: 0;\n}\n\n.hero-row > .card { flex-shrink: 1; }\n\n@media (max-width: 1100px) {\n .hero-row { grid-template-columns: 1fr; }\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.fable {\n background: var(--c-fable);\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.fable {\n color: #6BEFB4;\n border-color: rgba(46, 224, 143, .32);\n background: rgba(46, 224, 143, .07);\n}\n\n.model-pill.fable .sq {\n background: var(--c-fable);\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/* File paths stay as-is (no uppercase/wide tracking) and may overflow \u2014 clip. */\n.cs-k.path {\n text-transform: none;\n letter-spacing: 0.01em;\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14rem;\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/* Hot-files list: cap height + scroll so a long list never squeezes the Moat */\n#hot-files-list {\n max-height: 190px;\n overflow-y: auto;\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#hot-files-list::-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#hot-files-list::-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#hot-files-list::-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 .col-right > *:nth-child(4) { animation-delay: 440ms; }\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\n/* ============================================================\n Arsenal drawer (skills \xB7 agents \xB7 MCP)\n ============================================================ */\n.arsenal-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-right: 16px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 9px;\n background: var(--surface-1);\n color: var(--mist);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: border-color 160ms ease, background 160ms ease;\n}\n.arsenal-toggle:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.arsenal-scrim {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .55);\n opacity: 0;\n pointer-events: none;\n transition: opacity 180ms ease;\n z-index: 60;\n}\n.arsenal-scrim.on {\n opacity: 1;\n pointer-events: auto;\n}\n\n.arsenal-drawer {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: 360px;\n max-width: 88vw;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 16px 14px;\n background: var(--surface-3);\n backdrop-filter: blur(8px);\n border-right: 1px solid var(--rule-hover);\n transform: translateX(-102%);\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n z-index: 70;\n}\n.arsenal-drawer.open {\n transform: translateX(0);\n}\n\n.arsenal-head {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.arsenal-title {\n font-family: var(--font-serif);\n font-size: 20px;\n color: var(--text);\n flex: 1;\n}\n.arsenal-icon {\n width: 28px;\n height: 28px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n background: var(--surface-1);\n color: var(--text-dim);\n cursor: pointer;\n font-size: 13px;\n transition: border-color 160ms ease, color 160ms ease;\n}\n.arsenal-icon:hover {\n border-color: var(--rule-hover);\n color: var(--mist);\n}\n\n.arsenal-tabs {\n display: flex;\n gap: 6px;\n}\n.arsenal-tab {\n flex: 1;\n padding: 7px 6px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n background: transparent;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: border-color 160ms ease, color 160ms ease, background 160ms ease;\n}\n.arsenal-tab:hover {\n color: var(--text-dim);\n}\n.arsenal-tab.active {\n color: var(--text);\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n.at-count {\n color: var(--text-mute);\n font-variant-numeric: tabular-nums;\n}\n\n.arsenal-filter {\n padding: 8px 10px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n background: var(--surface-1);\n color: var(--mist);\n font-family: var(--font-sans);\n font-size: 12px;\n outline: none;\n}\n.arsenal-filter:focus {\n border-color: var(--rule-hover);\n}\n.arsenal-filter::placeholder {\n color: var(--text-mute);\n}\n\n.arsenal-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding-right: 4px;\n}\n\n.arsenal-item {\n border: 1px solid var(--rule-2);\n border-radius: 9px;\n background: var(--surface-1);\n padding: 9px 11px;\n cursor: pointer;\n transition: border-color 140ms ease, background 140ms ease;\n}\n.arsenal-item:hover {\n border-color: var(--rule);\n background: var(--surface-2);\n}\n.ai-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.ai-name {\n flex: 1;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n word-break: break-word;\n}\n.ai-desc {\n margin-top: 6px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--text-dim);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.ai-meta {\n margin-top: 6px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n word-break: break-word;\n display: none;\n}\n.arsenal-item.open .ai-desc {\n -webkit-line-clamp: unset;\n line-clamp: unset;\n}\n.arsenal-item.open .ai-meta {\n display: block;\n}\n\n.scope-badge {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n max-width: 140px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.scope-badge.project {\n color: var(--c-fable);\n background: rgba(46, 224, 143, .08);\n border: 1px solid rgba(46, 224, 143, .26);\n}\n.scope-badge.personal {\n color: var(--c-sonnet);\n background: rgba(255, 185, 56, .08);\n border: 1px solid rgba(255, 185, 56, .26);\n}\n.scope-badge.plugin {\n color: var(--sky);\n background: rgba(155, 194, 239, .06);\n border: 1px solid var(--rule);\n}\n.scope-badge.off {\n opacity: 0.5;\n}\n\n.arsenal-foot {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n text-align: center;\n padding-top: 4px;\n border-top: 1px solid var(--rule-2);\n}\n\n.arsenal-list::-webkit-scrollbar {\n width: 6px;\n}\n.arsenal-list::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n.arsenal-list::-webkit-scrollbar-track {\n background: transparent;\n}\n';
|
|
1585
1933
|
|
|
1586
1934
|
// src/dashboard/public/favicon.svg
|
|
1587
1935
|
var favicon_default = '<svg width="107" height="107" viewBox="0 0 107 107" fill="none" xmlns="http://www.w3.org/2000/svg">\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" fill="url(#paint0_radial_21_11)"/>\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" stroke="url(#paint1_linear_21_11)"/>\n<path d="M26.408 72.558C25.5813 72.558 24.6513 72.4753 23.618 72.31C22.626 72.1447 21.6753 71.938 20.766 71.69C19.898 71.442 19.216 71.1733 18.72 70.884C18.4307 70.7187 18.2033 70.5533 18.038 70.388C17.914 70.1813 17.852 69.8507 17.852 69.396L17.542 59.662C17.542 58.8353 17.8107 58.422 18.348 58.422C18.8027 58.422 19.1127 58.794 19.278 59.538L19.836 61.894C21.2413 67.8047 23.866 70.76 27.71 70.76C29.7767 70.76 31.4093 70.078 32.608 68.714C33.848 67.3087 34.468 65.3453 34.468 62.824C34.468 60.3853 33.8273 58.112 32.546 56.004C31.306 53.896 29.3013 51.8087 26.532 49.742C23.4733 47.5513 21.2413 45.4433 19.836 43.418C18.4307 41.3513 17.728 39.1607 17.728 36.846C17.728 33.7873 18.782 31.3487 20.89 29.53C23.0393 27.67 25.7673 26.74 29.074 26.74C30.314 26.74 31.5127 26.8847 32.67 27.174C33.8273 27.4633 34.778 27.8767 35.522 28.414C35.77 28.5793 35.956 28.7653 36.08 28.972C36.2453 29.1787 36.328 29.4473 36.328 29.778L36.514 39.14C36.514 39.8427 36.2453 40.194 35.708 40.194C35.336 40.194 35.0673 39.9047 34.902 39.326L34.468 37.776C33.5587 34.5933 32.608 32.2787 31.616 30.832C30.6653 29.344 29.2807 28.6 27.462 28.6C25.6847 28.6 24.2587 29.1787 23.184 30.336C22.1507 31.4933 21.634 33.126 21.634 35.234C21.634 37.0113 22.2127 38.706 23.37 40.318C24.5273 41.93 26.5527 43.8933 29.446 46.208C32.546 48.7293 34.7987 51.168 36.204 53.524C37.6507 55.8387 38.374 58.3187 38.374 60.964C38.374 63.2787 37.8573 65.304 36.824 67.04C35.7907 68.776 34.3647 70.14 32.546 71.132C30.7687 72.0827 28.7227 72.558 26.408 72.558ZM44.1831 84.71C43.0671 84.71 42.1784 84.3173 41.5171 83.532C40.8558 82.788 40.5251 81.92 40.5251 80.928C40.5251 80.1427 40.7524 79.4813 41.2071 78.944C41.6618 78.4067 42.2818 78.138 43.0671 78.138C43.7284 78.138 44.2038 78.2827 44.4931 78.572C44.7824 78.8613 44.9891 79.192 45.1131 79.564C45.2371 79.936 45.3611 80.2667 45.4851 80.556C45.6091 80.8453 45.8571 80.99 46.2291 80.99C46.6838 80.99 47.0971 80.6593 47.4691 79.998C47.8411 79.378 48.3578 78.0347 49.0191 75.968C49.4324 74.728 49.6804 73.6533 49.7631 72.744C49.8871 71.8347 49.8664 70.8633 49.7011 69.83C49.5771 68.7967 49.2671 67.536 48.7711 66.048L42.0131 44.534C41.7238 43.542 41.4344 42.9013 41.1451 42.612C40.8971 42.3227 40.4838 42.116 39.9051 41.992L38.9751 41.806C38.4378 41.682 38.1691 41.4133 38.1691 41C38.1691 40.5867 38.4584 40.38 39.0371 40.38H49.2671C49.8458 40.38 50.1351 40.5867 50.1351 41C50.1351 41.4547 49.8664 41.7233 49.3291 41.806L48.3371 41.93C47.3451 42.054 46.7044 42.302 46.4151 42.674C46.1671 43.0047 46.1878 43.6247 46.4771 44.534L51.9331 62.7C52.0571 63.1133 52.2431 63.32 52.4911 63.32C52.7804 63.32 52.9871 63.1133 53.1111 62.7L58.3811 45.65C58.7118 44.534 58.7944 43.7073 58.6291 43.17C58.5051 42.5913 57.9264 42.2193 56.8931 42.054L55.5911 41.806C55.0538 41.682 54.7851 41.4133 54.7851 41C54.7851 40.5867 55.0744 40.38 55.6531 40.38H63.5271C64.1058 40.38 64.3951 40.6073 64.3951 41.062C64.3951 41.4753 64.1471 41.7647 63.6511 41.93L63.0931 42.116C62.4731 42.3227 61.9564 42.6947 61.5431 43.232C61.1298 43.7693 60.6958 44.6993 60.2411 46.022L51.0031 74.852C50.0938 77.7453 49.2671 79.8947 48.5231 81.3C47.8204 82.7053 47.1384 83.6147 46.4771 84.028C45.8158 84.4827 45.0511 84.71 44.1831 84.71ZM64.9342 72C64.3142 72 64.0042 71.7727 64.0042 71.318C64.0042 70.946 64.2729 70.698 64.8102 70.574L65.5542 70.45C66.4222 70.2847 66.9802 70.0367 67.2282 69.706C67.5176 69.334 67.6622 68.714 67.6622 67.846V47.014C67.6622 46.27 67.5382 45.774 67.2902 45.526C67.0836 45.2367 66.6909 45.0507 66.1122 44.968L64.8102 44.782C64.2729 44.7407 64.0042 44.5133 64.0042 44.1C64.0042 43.7693 64.3349 43.542 64.9962 43.418C66.2776 43.2113 67.2489 42.8807 67.9102 42.426C68.6129 41.9713 69.3362 41.4133 70.0802 40.752C70.4522 40.38 70.7622 40.194 71.0102 40.194C71.3822 40.194 71.5682 40.442 71.5682 40.938V43.728C71.5682 44.1 71.6922 44.348 71.9402 44.472C72.2296 44.5547 72.5396 44.4307 72.8702 44.1C74.5236 42.5293 75.9702 41.4547 77.2102 40.876C78.4916 40.2973 79.8349 40.008 81.2402 40.008C83.1002 40.008 84.5882 40.6693 85.7042 41.992C86.8202 43.2733 87.3782 45.1747 87.3782 47.696V67.846C87.3782 68.714 87.5022 69.334 87.7502 69.706C88.0396 70.0367 88.6182 70.264 89.4862 70.388L90.8502 70.574C91.3049 70.6567 91.5322 70.9047 91.5322 71.318C91.5322 71.7727 91.2842 72 90.7882 72H80.3722C79.7936 72 79.5042 71.7727 79.5042 71.318C79.5042 70.9047 79.7316 70.6567 80.1862 70.574L81.0542 70.45C81.9222 70.326 82.4802 70.078 82.7282 69.706C83.0176 69.334 83.1622 68.714 83.1622 67.846V48.378C83.1622 46.3527 82.7902 44.9267 82.0462 44.1C81.3022 43.2733 80.2482 42.86 78.8842 42.86C77.5616 42.86 76.3629 43.2527 75.2882 44.038C74.2549 44.782 73.4282 45.7947 72.8082 47.076C72.1882 48.316 71.8782 49.7007 71.8782 51.23V67.846C71.8782 68.714 72.0022 69.334 72.2502 69.706C72.5396 70.078 73.1182 70.3053 73.9862 70.388L75.6602 70.574C76.1149 70.6567 76.3422 70.884 76.3422 71.256C76.3422 71.752 76.0322 72 75.4122 72H64.9342Z" fill="white"/>\n<defs>\n<radialGradient id="paint0_radial_21_11" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(14 16) rotate(43.5498) scale(167.638 156.2)">\n<stop stop-color="#2C5DB8"/>\n<stop offset="1" stop-color="#04081A"/>\n</radialGradient>\n<linearGradient id="paint1_linear_21_11" x1="1.77511" y1="1.50277e-06" x2="105.225" y2="107" gradientUnits="userSpaceOnUse">\n<stop stop-color="#5C8FE6"/>\n<stop offset="1" stop-color="#335080"/>\n</linearGradient>\n</defs>\n</svg>\n';
|
|
@@ -1610,6 +1958,7 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1610
1958
|
return c.body(favicon_default);
|
|
1611
1959
|
});
|
|
1612
1960
|
app.get("/health", (c) => c.json({ ok: true }));
|
|
1961
|
+
app.get("/arsenal", async (c) => c.json(await computeArsenal(paths.projectRoot)));
|
|
1613
1962
|
app.get("/data", async (c) => {
|
|
1614
1963
|
const data = await computeDashboardData(paths, RECENT_N);
|
|
1615
1964
|
return c.json(data);
|
|
@@ -1627,8 +1976,8 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1627
1976
|
}
|
|
1628
1977
|
|
|
1629
1978
|
// src/hooks/installer.ts
|
|
1630
|
-
import { mkdir as mkdir3, readFile as
|
|
1631
|
-
import { dirname as
|
|
1979
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1980
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
1632
1981
|
|
|
1633
1982
|
// src/hooks/scripts/pre-compact.ps1
|
|
1634
1983
|
var pre_compact_default = '# PreCompact hook \u2014 Windows PowerShell.\n# Re-injects the primer after Claude auto-compacts. Same logic as prime.ps1.\n\n$ErrorActionPreference = "SilentlyContinue"\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\ntry {\n $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$port/prime" -Method GET -TimeoutSec 3\n if ($resp.primer) { Write-Output $resp.primer }\n} catch {\n # silent\n}\nexit 0\n';
|
|
@@ -1932,7 +2281,7 @@ function chosenScriptExt() {
|
|
|
1932
2281
|
}
|
|
1933
2282
|
async function readSettings(path) {
|
|
1934
2283
|
try {
|
|
1935
|
-
const raw = await
|
|
2284
|
+
const raw = await readFile5(path, "utf8");
|
|
1936
2285
|
return JSON.parse(raw);
|
|
1937
2286
|
} catch {
|
|
1938
2287
|
return {};
|
|
@@ -1954,7 +2303,7 @@ function stripOurHooks(config) {
|
|
|
1954
2303
|
function mergeOurHooks(config, paths) {
|
|
1955
2304
|
const hooks = config.hooks = config.hooks ?? {};
|
|
1956
2305
|
for (const s of SCRIPTS) {
|
|
1957
|
-
const scriptPath =
|
|
2306
|
+
const scriptPath = join4(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
|
|
1958
2307
|
const entry = {
|
|
1959
2308
|
...s.matcher ? { matcher: s.matcher } : {},
|
|
1960
2309
|
hooks: [
|
|
@@ -1974,11 +2323,11 @@ async function installHooks(paths) {
|
|
|
1974
2323
|
await mkdir3(paths.claudeHooksDir, { recursive: true });
|
|
1975
2324
|
const scriptsWritten = [];
|
|
1976
2325
|
for (const s of SCRIPTS) {
|
|
1977
|
-
const target =
|
|
2326
|
+
const target = join4(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
|
|
1978
2327
|
await writeFile3(target, chosenScriptBody(s), "utf8");
|
|
1979
2328
|
scriptsWritten.push(target);
|
|
1980
2329
|
}
|
|
1981
|
-
await mkdir3(
|
|
2330
|
+
await mkdir3(dirname4(paths.claudeSettings), { recursive: true });
|
|
1982
2331
|
const existing = await readSettings(paths.claudeSettings);
|
|
1983
2332
|
const stripped = stripOurHooks(existing);
|
|
1984
2333
|
const merged = mergeOurHooks(stripped, paths);
|
|
@@ -1994,7 +2343,7 @@ import { writeFile as writeFile11 } from "fs/promises";
|
|
|
1994
2343
|
|
|
1995
2344
|
// src/activity/activity-log.ts
|
|
1996
2345
|
import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
|
|
1997
|
-
import { dirname as
|
|
2346
|
+
import { dirname as dirname5 } from "path";
|
|
1998
2347
|
var DEFAULT_RING_SIZE = 100;
|
|
1999
2348
|
var ActivityStore = class {
|
|
2000
2349
|
ring = [];
|
|
@@ -2031,7 +2380,7 @@ var ActivityStore = class {
|
|
|
2031
2380
|
}
|
|
2032
2381
|
async persist(event) {
|
|
2033
2382
|
try {
|
|
2034
|
-
await mkdir4(
|
|
2383
|
+
await mkdir4(dirname5(this.persistPath), { recursive: true });
|
|
2035
2384
|
await appendFile2(this.persistPath, JSON.stringify(event) + "\n", "utf8");
|
|
2036
2385
|
} catch {
|
|
2037
2386
|
}
|
|
@@ -2040,8 +2389,8 @@ var ActivityStore = class {
|
|
|
2040
2389
|
|
|
2041
2390
|
// src/activity/file-watcher.ts
|
|
2042
2391
|
import chokidar from "chokidar";
|
|
2043
|
-
import { readFile as
|
|
2044
|
-
import { join as
|
|
2392
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2393
|
+
import { join as join5, relative, sep } from "path";
|
|
2045
2394
|
import ignore from "ignore";
|
|
2046
2395
|
var ALWAYS_IGNORE = [
|
|
2047
2396
|
".git",
|
|
@@ -2063,7 +2412,7 @@ var ALWAYS_IGNORE = [
|
|
|
2063
2412
|
];
|
|
2064
2413
|
async function readIgnoreFile(path) {
|
|
2065
2414
|
try {
|
|
2066
|
-
const text = await
|
|
2415
|
+
const text = await readFile6(path, "utf8");
|
|
2067
2416
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
2068
2417
|
} catch {
|
|
2069
2418
|
return [];
|
|
@@ -2072,8 +2421,8 @@ async function readIgnoreFile(path) {
|
|
|
2072
2421
|
async function buildMatcher(root) {
|
|
2073
2422
|
const ig = ignore();
|
|
2074
2423
|
ig.add(ALWAYS_IGNORE.map((d) => `${d}/`));
|
|
2075
|
-
ig.add(await readIgnoreFile(
|
|
2076
|
-
ig.add(await readIgnoreFile(
|
|
2424
|
+
ig.add(await readIgnoreFile(join5(root, ".gitignore")));
|
|
2425
|
+
ig.add(await readIgnoreFile(join5(root, ".synthraignore")));
|
|
2077
2426
|
return ig;
|
|
2078
2427
|
}
|
|
2079
2428
|
function toPosixRel(root, abs) {
|
|
@@ -2128,14 +2477,14 @@ function createFileWatcher(root, onEvent) {
|
|
|
2128
2477
|
// src/activity/git-watcher.ts
|
|
2129
2478
|
import { execFile } from "child_process";
|
|
2130
2479
|
import { watch } from "fs";
|
|
2131
|
-
import { readFile as
|
|
2132
|
-
import { join as
|
|
2480
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2481
|
+
import { join as join6 } from "path";
|
|
2133
2482
|
import { promisify } from "util";
|
|
2134
2483
|
var execFileAsync = promisify(execFile);
|
|
2135
2484
|
var POLL_MS = 2e3;
|
|
2136
2485
|
async function readHeadBranch(projectRoot) {
|
|
2137
2486
|
try {
|
|
2138
|
-
const head = await
|
|
2487
|
+
const head = await readFile7(join6(projectRoot, ".git", "HEAD"), "utf8");
|
|
2139
2488
|
const m = head.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
2140
2489
|
return m?.[1] ?? null;
|
|
2141
2490
|
} catch {
|
|
@@ -2202,7 +2551,7 @@ function createGitWatcher(root, onEvent) {
|
|
|
2202
2551
|
lastBranch = await readHeadBranch(root);
|
|
2203
2552
|
lastStatus = await readStatusPorcelain(root);
|
|
2204
2553
|
try {
|
|
2205
|
-
headWatcher = watch(
|
|
2554
|
+
headWatcher = watch(join6(root, ".git", "HEAD"), () => {
|
|
2206
2555
|
void checkHead();
|
|
2207
2556
|
});
|
|
2208
2557
|
headWatcher.on("error", () => {
|
|
@@ -2234,7 +2583,7 @@ function parseStatusFiles(porcelain) {
|
|
|
2234
2583
|
import { resolve } from "path";
|
|
2235
2584
|
|
|
2236
2585
|
// src/scanner/extract.ts
|
|
2237
|
-
import { dirname as
|
|
2586
|
+
import { dirname as dirname6, join as join7, posix } from "path";
|
|
2238
2587
|
|
|
2239
2588
|
// src/graph/types.ts
|
|
2240
2589
|
var SCHEMA_VERSION2 = 2;
|
|
@@ -2648,11 +2997,11 @@ function buildCallEdges(symbolsByFile, callsByFile) {
|
|
|
2648
2997
|
}
|
|
2649
2998
|
|
|
2650
2999
|
// src/scanner/parse-cache.ts
|
|
2651
|
-
import { mkdir as mkdir5, readFile as
|
|
2652
|
-
import { dirname as
|
|
3000
|
+
import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3001
|
+
import { dirname as dirname7 } from "path";
|
|
2653
3002
|
|
|
2654
3003
|
// src/scanner/parser.ts
|
|
2655
|
-
import { readFile as
|
|
3004
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2656
3005
|
import { createRequire } from "module";
|
|
2657
3006
|
import { Language, Parser } from "web-tree-sitter";
|
|
2658
3007
|
|
|
@@ -3513,7 +3862,7 @@ function emptyParseCache() {
|
|
|
3513
3862
|
}
|
|
3514
3863
|
async function readParseCache(path) {
|
|
3515
3864
|
try {
|
|
3516
|
-
const raw = await
|
|
3865
|
+
const raw = await readFile9(path, "utf8");
|
|
3517
3866
|
const parsed = JSON.parse(raw);
|
|
3518
3867
|
if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
3519
3868
|
return emptyParseCache();
|
|
@@ -3523,16 +3872,16 @@ async function readParseCache(path) {
|
|
|
3523
3872
|
return emptyParseCache();
|
|
3524
3873
|
}
|
|
3525
3874
|
}
|
|
3526
|
-
async function writeParseCache(path,
|
|
3875
|
+
async function writeParseCache(path, cache2) {
|
|
3527
3876
|
try {
|
|
3528
|
-
await mkdir5(
|
|
3529
|
-
await writeFile4(path, `${JSON.stringify(
|
|
3877
|
+
await mkdir5(dirname7(path), { recursive: true });
|
|
3878
|
+
await writeFile4(path, `${JSON.stringify(cache2)}
|
|
3530
3879
|
`, "utf8");
|
|
3531
3880
|
} catch {
|
|
3532
3881
|
}
|
|
3533
3882
|
}
|
|
3534
3883
|
async function incrementalParse(parsable, prev, opts = {}) {
|
|
3535
|
-
const
|
|
3884
|
+
const cache2 = emptyParseCache();
|
|
3536
3885
|
const parsed = [];
|
|
3537
3886
|
let reused = 0;
|
|
3538
3887
|
let reparsed = 0;
|
|
@@ -3540,7 +3889,7 @@ async function incrementalParse(parsable, prev, opts = {}) {
|
|
|
3540
3889
|
for (const f of parsable) {
|
|
3541
3890
|
let source;
|
|
3542
3891
|
try {
|
|
3543
|
-
source = await
|
|
3892
|
+
source = await readFile9(f.absPath, "utf8");
|
|
3544
3893
|
} catch {
|
|
3545
3894
|
continue;
|
|
3546
3895
|
}
|
|
@@ -3554,25 +3903,25 @@ async function incrementalParse(parsable, prev, opts = {}) {
|
|
|
3554
3903
|
imports: cached.imports,
|
|
3555
3904
|
calls: cached.calls
|
|
3556
3905
|
});
|
|
3557
|
-
|
|
3906
|
+
cache2.files[f.relPath] = cached;
|
|
3558
3907
|
reused += 1;
|
|
3559
3908
|
continue;
|
|
3560
3909
|
}
|
|
3561
3910
|
try {
|
|
3562
3911
|
const p = await parseSource(f, source);
|
|
3563
3912
|
parsed.push(p);
|
|
3564
|
-
|
|
3913
|
+
cache2.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
|
|
3565
3914
|
reparsed += 1;
|
|
3566
3915
|
} catch {
|
|
3567
3916
|
parseErrors += 1;
|
|
3568
3917
|
}
|
|
3569
3918
|
}
|
|
3570
|
-
return { parsed, cache, reused, reparsed, parseErrors };
|
|
3919
|
+
return { parsed, cache: cache2, reused, reparsed, parseErrors };
|
|
3571
3920
|
}
|
|
3572
3921
|
|
|
3573
3922
|
// src/scanner/walker.ts
|
|
3574
|
-
import { readFile as
|
|
3575
|
-
import { extname, join as
|
|
3923
|
+
import { readFile as readFile10, readdir as readdir2, stat } from "fs/promises";
|
|
3924
|
+
import { extname, join as join8, relative as relative2, sep as sep2 } from "path";
|
|
3576
3925
|
import ignore2 from "ignore";
|
|
3577
3926
|
var DEFAULT_IGNORE = [
|
|
3578
3927
|
".git/",
|
|
@@ -3652,7 +4001,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
|
3652
4001
|
]);
|
|
3653
4002
|
async function readIgnoreFile2(path) {
|
|
3654
4003
|
try {
|
|
3655
|
-
const text = await
|
|
4004
|
+
const text = await readFile10(path, "utf8");
|
|
3656
4005
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
3657
4006
|
} catch {
|
|
3658
4007
|
return [];
|
|
@@ -3661,8 +4010,8 @@ async function readIgnoreFile2(path) {
|
|
|
3661
4010
|
async function buildMatcher2(root, extra) {
|
|
3662
4011
|
const ig = ignore2();
|
|
3663
4012
|
ig.add(DEFAULT_IGNORE);
|
|
3664
|
-
ig.add(await readIgnoreFile2(
|
|
3665
|
-
ig.add(await readIgnoreFile2(
|
|
4013
|
+
ig.add(await readIgnoreFile2(join8(root, ".gitignore")));
|
|
4014
|
+
ig.add(await readIgnoreFile2(join8(root, ".synthraignore")));
|
|
3666
4015
|
if (extra.length) ig.add(extra);
|
|
3667
4016
|
return ig;
|
|
3668
4017
|
}
|
|
@@ -3675,12 +4024,12 @@ async function* walk(root, options = {}) {
|
|
|
3675
4024
|
async function* recurse(dir) {
|
|
3676
4025
|
let entries;
|
|
3677
4026
|
try {
|
|
3678
|
-
entries = await
|
|
4027
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
3679
4028
|
} catch {
|
|
3680
4029
|
return;
|
|
3681
4030
|
}
|
|
3682
4031
|
for (const entry of entries) {
|
|
3683
|
-
const abs =
|
|
4032
|
+
const abs = join8(dir, entry.name);
|
|
3684
4033
|
const rel = relative2(root, abs);
|
|
3685
4034
|
if (!rel) continue;
|
|
3686
4035
|
const relPosix = toPosix2(rel);
|
|
@@ -3707,38 +4056,38 @@ async function* walk(root, options = {}) {
|
|
|
3707
4056
|
}
|
|
3708
4057
|
|
|
3709
4058
|
// src/graph/store.ts
|
|
3710
|
-
import { mkdir as mkdir6, readFile as
|
|
3711
|
-
import { dirname as
|
|
4059
|
+
import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
|
|
4060
|
+
import { dirname as dirname8 } from "path";
|
|
3712
4061
|
async function writeJson(path, data, pretty) {
|
|
3713
|
-
await mkdir6(
|
|
4062
|
+
await mkdir6(dirname8(path), { recursive: true });
|
|
3714
4063
|
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
3715
4064
|
await writeFile5(path, text + "\n", "utf8");
|
|
3716
4065
|
}
|
|
3717
|
-
async function
|
|
3718
|
-
const text = await
|
|
4066
|
+
async function readJson2(path) {
|
|
4067
|
+
const text = await readFile11(path, "utf8");
|
|
3719
4068
|
return JSON.parse(text);
|
|
3720
4069
|
}
|
|
3721
4070
|
async function writeGraph(path, graph) {
|
|
3722
4071
|
await writeJson(path, graph, false);
|
|
3723
4072
|
}
|
|
3724
4073
|
async function readGraph(path) {
|
|
3725
|
-
return
|
|
4074
|
+
return readJson2(path);
|
|
3726
4075
|
}
|
|
3727
4076
|
async function writeSymbolIndex(path, index) {
|
|
3728
4077
|
await writeJson(path, index, true);
|
|
3729
4078
|
}
|
|
3730
4079
|
async function readSymbolIndex(path) {
|
|
3731
|
-
const parsed = await
|
|
4080
|
+
const parsed = await readJson2(path);
|
|
3732
4081
|
return Object.assign(/* @__PURE__ */ Object.create(null), parsed);
|
|
3733
4082
|
}
|
|
3734
4083
|
|
|
3735
4084
|
// src/cli/bootstrap.ts
|
|
3736
|
-
import { mkdir as mkdir7, readFile as
|
|
3737
|
-
import { basename as
|
|
4085
|
+
import { mkdir as mkdir7, readFile as readFile13, stat as stat2, writeFile as writeFile7 } from "fs/promises";
|
|
4086
|
+
import { basename as basename5 } from "path";
|
|
3738
4087
|
|
|
3739
4088
|
// src/hooks/claude-md.ts
|
|
3740
|
-
import { readFile as
|
|
3741
|
-
import { basename as
|
|
4089
|
+
import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
4090
|
+
import { basename as basename4, dirname as dirname9 } from "path";
|
|
3742
4091
|
var POLICY_VERSION = 7;
|
|
3743
4092
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3744
4093
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
@@ -3905,13 +4254,13 @@ function onboardingSkeleton(projectName) {
|
|
|
3905
4254
|
async function patchClaudeMd(path, projectName) {
|
|
3906
4255
|
let existing;
|
|
3907
4256
|
try {
|
|
3908
|
-
existing = await
|
|
4257
|
+
existing = await readFile12(path, "utf8");
|
|
3909
4258
|
} catch {
|
|
3910
4259
|
existing = null;
|
|
3911
4260
|
}
|
|
3912
4261
|
const block = policyBlock();
|
|
3913
4262
|
if (existing === null) {
|
|
3914
|
-
const name = projectName ||
|
|
4263
|
+
const name = projectName || basename4(dirname9(path)) || "this project";
|
|
3915
4264
|
await writeFile6(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
|
|
3916
4265
|
return { created: true, updated: false, skipped: false };
|
|
3917
4266
|
}
|
|
@@ -3952,7 +4301,7 @@ async function ensureDir(path) {
|
|
|
3952
4301
|
async function patchGitignore(path) {
|
|
3953
4302
|
let existing = "";
|
|
3954
4303
|
try {
|
|
3955
|
-
existing = await
|
|
4304
|
+
existing = await readFile13(path, "utf8");
|
|
3956
4305
|
} catch {
|
|
3957
4306
|
}
|
|
3958
4307
|
const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
|
|
@@ -3969,7 +4318,7 @@ async function bootstrap(paths) {
|
|
|
3969
4318
|
const contextCreated = await ensureDir(paths.contextDir);
|
|
3970
4319
|
const gitignoreUpdated = await patchGitignore(paths.gitignore);
|
|
3971
4320
|
const claudeMdExistedBefore = await exists(paths.claudeMd);
|
|
3972
|
-
const patch = await patchClaudeMd(paths.claudeMd,
|
|
4321
|
+
const patch = await patchClaudeMd(paths.claudeMd, basename5(paths.projectRoot));
|
|
3973
4322
|
return {
|
|
3974
4323
|
graphCreated,
|
|
3975
4324
|
contextCreated,
|
|
@@ -4036,7 +4385,7 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
4036
4385
|
if (verbose) log.info(` walked ${walked.length} files`);
|
|
4037
4386
|
const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
|
|
4038
4387
|
const prevCache = await readParseCache(paths.parseCache);
|
|
4039
|
-
const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
|
|
4388
|
+
const { parsed, cache: cache2, reused, reparsed, parseErrors } = await incrementalParse(
|
|
4040
4389
|
parsable,
|
|
4041
4390
|
prevCache,
|
|
4042
4391
|
{ full: opts.full }
|
|
@@ -4050,7 +4399,7 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
4050
4399
|
const symbolIndex = buildSymbolIndex(graph);
|
|
4051
4400
|
await writeGraph(paths.infoGraph, graph);
|
|
4052
4401
|
await writeSymbolIndex(paths.symbolIndex, symbolIndex);
|
|
4053
|
-
await writeParseCache(paths.parseCache,
|
|
4402
|
+
await writeParseCache(paths.parseCache, cache2);
|
|
4054
4403
|
if (verbose) {
|
|
4055
4404
|
log.info(
|
|
4056
4405
|
` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
|
|
@@ -4130,7 +4479,7 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
4130
4479
|
|
|
4131
4480
|
// src/server/mcp.ts
|
|
4132
4481
|
import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
|
|
4133
|
-
import { dirname as
|
|
4482
|
+
import { dirname as dirname12 } from "path";
|
|
4134
4483
|
|
|
4135
4484
|
// src/graph/rank.ts
|
|
4136
4485
|
var KW_BASE_WEIGHT = 2;
|
|
@@ -4383,14 +4732,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
4383
4732
|
|
|
4384
4733
|
// src/memory/branches.ts
|
|
4385
4734
|
import { execFile as execFile2 } from "child_process";
|
|
4386
|
-
import { readFile as
|
|
4387
|
-
import { join as
|
|
4735
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4736
|
+
import { join as join9 } from "path";
|
|
4388
4737
|
import { promisify as promisify2 } from "util";
|
|
4389
4738
|
var execFileAsync2 = promisify2(execFile2);
|
|
4390
4739
|
async function currentBranch(projectRoot) {
|
|
4391
4740
|
try {
|
|
4392
|
-
const headPath =
|
|
4393
|
-
const head = await
|
|
4741
|
+
const headPath = join9(projectRoot, ".git", "HEAD");
|
|
4742
|
+
const head = await readFile14(headPath, "utf8");
|
|
4394
4743
|
const trimmed = head.trim();
|
|
4395
4744
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
4396
4745
|
if (match?.[1]) return match[1];
|
|
@@ -4426,22 +4775,22 @@ function sanitizeBranchName(name) {
|
|
|
4426
4775
|
function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
4427
4776
|
if (isDefault) {
|
|
4428
4777
|
return {
|
|
4429
|
-
contextStore:
|
|
4430
|
-
contextMd:
|
|
4778
|
+
contextStore: join9(contextDir, "context-store.json"),
|
|
4779
|
+
contextMd: join9(contextDir, "CONTEXT.md"),
|
|
4431
4780
|
branchDir: null
|
|
4432
4781
|
};
|
|
4433
4782
|
}
|
|
4434
|
-
const branchDir =
|
|
4783
|
+
const branchDir = join9(contextDir, "branches", sanitizeBranchName(branch));
|
|
4435
4784
|
return {
|
|
4436
|
-
contextStore:
|
|
4437
|
-
contextMd:
|
|
4785
|
+
contextStore: join9(branchDir, "context-store.json"),
|
|
4786
|
+
contextMd: join9(branchDir, "CONTEXT.md"),
|
|
4438
4787
|
branchDir
|
|
4439
4788
|
};
|
|
4440
4789
|
}
|
|
4441
4790
|
|
|
4442
4791
|
// src/memory/context-md.ts
|
|
4443
|
-
import { mkdir as mkdir8, readFile as
|
|
4444
|
-
import { dirname as
|
|
4792
|
+
import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
4793
|
+
import { dirname as dirname10 } from "path";
|
|
4445
4794
|
var MAX_BULLETS = 3;
|
|
4446
4795
|
function deriveContextMd(entries, branch) {
|
|
4447
4796
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -4484,17 +4833,17 @@ function formatContextMd(ctx) {
|
|
|
4484
4833
|
return lines.join("\n");
|
|
4485
4834
|
}
|
|
4486
4835
|
async function writeContextMd(path, ctx) {
|
|
4487
|
-
await mkdir8(
|
|
4836
|
+
await mkdir8(dirname10(path), { recursive: true });
|
|
4488
4837
|
await writeFile8(path, formatContextMd(ctx), "utf8");
|
|
4489
4838
|
}
|
|
4490
4839
|
|
|
4491
4840
|
// src/memory/context-store.ts
|
|
4492
|
-
import { mkdir as mkdir9, readFile as
|
|
4493
|
-
import { dirname as
|
|
4841
|
+
import { mkdir as mkdir9, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
4842
|
+
import { dirname as dirname11 } from "path";
|
|
4494
4843
|
var SCHEMA_VERSION3 = 1;
|
|
4495
4844
|
async function readEntries(path) {
|
|
4496
4845
|
try {
|
|
4497
|
-
const raw = await
|
|
4846
|
+
const raw = await readFile16(path, "utf8");
|
|
4498
4847
|
const parsed = JSON.parse(raw);
|
|
4499
4848
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
4500
4849
|
} catch {
|
|
@@ -4502,7 +4851,7 @@ async function readEntries(path) {
|
|
|
4502
4851
|
}
|
|
4503
4852
|
}
|
|
4504
4853
|
async function writeEntries(path, entries) {
|
|
4505
|
-
await mkdir9(
|
|
4854
|
+
await mkdir9(dirname11(path), { recursive: true });
|
|
4506
4855
|
const store = { schema_version: SCHEMA_VERSION3, entries };
|
|
4507
4856
|
await writeFile9(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
4508
4857
|
}
|
|
@@ -4749,6 +5098,31 @@ async function pack(files, opts) {
|
|
|
4749
5098
|
};
|
|
4750
5099
|
}
|
|
4751
5100
|
|
|
5101
|
+
// src/shared/config.ts
|
|
5102
|
+
function num(name, fallback) {
|
|
5103
|
+
const v = process.env[name];
|
|
5104
|
+
if (!v) return fallback;
|
|
5105
|
+
const n = Number(v);
|
|
5106
|
+
return Number.isFinite(n) ? n : fallback;
|
|
5107
|
+
}
|
|
5108
|
+
function str(name, fallback) {
|
|
5109
|
+
return process.env[name] ?? fallback;
|
|
5110
|
+
}
|
|
5111
|
+
function loadConfig() {
|
|
5112
|
+
return {
|
|
5113
|
+
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
5114
|
+
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
5115
|
+
readDepsMaxChars: num("SYN_READ_DEPS_CHARS", 900),
|
|
5116
|
+
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
5117
|
+
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
5118
|
+
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
5119
|
+
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
5120
|
+
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
5121
|
+
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
5122
|
+
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
|
|
4752
5126
|
// src/server/mcp.ts
|
|
4753
5127
|
var PROTOCOL_VERSION = "2024-11-05";
|
|
4754
5128
|
var SERVER_INFO = { name: "synthra", version: "0.0.1" };
|
|
@@ -4788,7 +5162,7 @@ var TOOLS = [
|
|
|
4788
5162
|
},
|
|
4789
5163
|
{
|
|
4790
5164
|
name: "graph_read",
|
|
4791
|
-
description: "Return the source code for a specific file or symbol. Target is either a project-relative file path (e.g. 'src/auth.ts') or 'file::symbol' (e.g. 'src/auth.ts::AuthService').",
|
|
5165
|
+
description: "Return the source code for a specific file or symbol. Target is either a project-relative file path (e.g. 'src/auth.ts') or 'file::symbol' (e.g. 'src/auth.ts::AuthService'). A symbol read also returns its dependency surface \u2014 the signatures of the symbols it calls (edit against these instead of guessing or re-reading their files) and the names of the symbols that call it.",
|
|
4792
5166
|
inputSchema: {
|
|
4793
5167
|
type: "object",
|
|
4794
5168
|
properties: {
|
|
@@ -5065,6 +5439,72 @@ function resolveFileTarget(graph, filePath) {
|
|
|
5065
5439
|
if (matches.length > 1) return { ambiguous: matches.map((n) => n.path) };
|
|
5066
5440
|
return { none: true };
|
|
5067
5441
|
}
|
|
5442
|
+
var DEPS_SIG_MAX = 140;
|
|
5443
|
+
var DEPS_MAX_CALLEES = 10;
|
|
5444
|
+
var DEPS_MAX_CALLERS = 12;
|
|
5445
|
+
function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars) {
|
|
5446
|
+
const symById = /* @__PURE__ */ new Map();
|
|
5447
|
+
for (const n of graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
5448
|
+
const calleeIds = [];
|
|
5449
|
+
const callerIds = [];
|
|
5450
|
+
const seenCallee = /* @__PURE__ */ new Set();
|
|
5451
|
+
const seenCaller = /* @__PURE__ */ new Set();
|
|
5452
|
+
for (const e of graph.edges) {
|
|
5453
|
+
if (e.kind !== "calls") continue;
|
|
5454
|
+
if (e.from === symbol.id && e.to !== symbol.id && !seenCallee.has(e.to)) {
|
|
5455
|
+
seenCallee.add(e.to);
|
|
5456
|
+
calleeIds.push(e.to);
|
|
5457
|
+
} else if (e.to === symbol.id && e.from !== symbol.id && !seenCaller.has(e.from)) {
|
|
5458
|
+
seenCaller.add(e.from);
|
|
5459
|
+
callerIds.push(e.from);
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
const resolve6 = (ids) => ids.map((id) => symById.get(id)).filter((n) => !!n);
|
|
5463
|
+
const callees = resolve6(calleeIds).sort(
|
|
5464
|
+
(a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1
|
|
5465
|
+
);
|
|
5466
|
+
const callers = resolve6(callerIds);
|
|
5467
|
+
if (callees.length === 0 && callers.length === 0) return "";
|
|
5468
|
+
const lines = [];
|
|
5469
|
+
let used = 0;
|
|
5470
|
+
if (callees.length > 0) {
|
|
5471
|
+
const head = "Depends on (signatures \u2014 don't guess these):";
|
|
5472
|
+
lines.push(head);
|
|
5473
|
+
used += head.length + 1;
|
|
5474
|
+
let shown = 0;
|
|
5475
|
+
for (const c of callees.slice(0, DEPS_MAX_CALLEES)) {
|
|
5476
|
+
const sig = c.signature.trim().slice(0, DEPS_SIG_MAX);
|
|
5477
|
+
const entry = `\u2022 ${sig} \u2192 mcp__synthra__graph_read("${c.file}::${c.name}")`;
|
|
5478
|
+
if (used + entry.length + 1 > maxChars) break;
|
|
5479
|
+
lines.push(entry);
|
|
5480
|
+
used += entry.length + 1;
|
|
5481
|
+
shown += 1;
|
|
5482
|
+
}
|
|
5483
|
+
const omitted = callees.length - shown;
|
|
5484
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more`);
|
|
5485
|
+
}
|
|
5486
|
+
if (callers.length > 0) {
|
|
5487
|
+
const sep3 = lines.length > 0 ? 1 : 0;
|
|
5488
|
+
const head = `Used by (${callers.length}): `;
|
|
5489
|
+
const shown = [];
|
|
5490
|
+
let cUsed = used + sep3 + head.length;
|
|
5491
|
+
for (const c of callers.slice(0, DEPS_MAX_CALLERS)) {
|
|
5492
|
+
const part = `${c.name} \u2192 ${c.file}`;
|
|
5493
|
+
const join13 = shown.length > 0 ? 3 : 0;
|
|
5494
|
+
if (cUsed + join13 + part.length > maxChars) break;
|
|
5495
|
+
shown.push(part);
|
|
5496
|
+
cUsed += join13 + part.length;
|
|
5497
|
+
}
|
|
5498
|
+
if (lines.length > 0) lines.push("");
|
|
5499
|
+
if (shown.length > 0) {
|
|
5500
|
+
const omitted = callers.length - shown.length;
|
|
5501
|
+
lines.push(head + shown.join(" \xB7 ") + (omitted > 0 ? ` \u2026+${omitted} more` : ""));
|
|
5502
|
+
} else {
|
|
5503
|
+
lines.push(`Used by (${callers.length} callers)`);
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
return lines.join("\n");
|
|
5507
|
+
}
|
|
5068
5508
|
async function graphRead(args, ctx) {
|
|
5069
5509
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
5070
5510
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -5103,10 +5543,15 @@ ${fileNode.content}`);
|
|
|
5103
5543
|
|
|
5104
5544
|
---
|
|
5105
5545
|
\u270E To edit this symbol: Read("${fileNode.path}", offset=${offset}, limit=${limit}) then Edit \u2014 that satisfies Claude Code's read-gate at ~${limit} lines; do NOT re-read the whole file.`;
|
|
5546
|
+
const deps = buildDepsFooter(symbol, ctx.graph);
|
|
5547
|
+
const depsBlock = deps ? `
|
|
5548
|
+
|
|
5549
|
+
---
|
|
5550
|
+
${deps}` : "";
|
|
5106
5551
|
return textContent(
|
|
5107
5552
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
5108
5553
|
|
|
5109
|
-
${body}${editHint}`
|
|
5554
|
+
${body}${depsBlock}${editHint}`
|
|
5110
5555
|
);
|
|
5111
5556
|
}
|
|
5112
5557
|
var editedFiles = /* @__PURE__ */ new Set();
|
|
@@ -5192,7 +5637,7 @@ async function contextRecall(args, ctx) {
|
|
|
5192
5637
|
}
|
|
5193
5638
|
async function logToolCall(ctx, tool) {
|
|
5194
5639
|
try {
|
|
5195
|
-
await mkdir10(
|
|
5640
|
+
await mkdir10(dirname12(ctx.paths.toolLog), { recursive: true });
|
|
5196
5641
|
await appendFile3(
|
|
5197
5642
|
ctx.paths.toolLog,
|
|
5198
5643
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
@@ -5291,12 +5736,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
5291
5736
|
}
|
|
5292
5737
|
|
|
5293
5738
|
// src/memory/session.ts
|
|
5294
|
-
import { mkdir as mkdir11, readFile as
|
|
5295
|
-
import { dirname as
|
|
5739
|
+
import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
|
|
5740
|
+
import { dirname as dirname13 } from "path";
|
|
5296
5741
|
var SESSION_SCHEMA_VERSION = 1;
|
|
5297
5742
|
async function readSession(path) {
|
|
5298
5743
|
try {
|
|
5299
|
-
const raw = await
|
|
5744
|
+
const raw = await readFile17(path, "utf8");
|
|
5300
5745
|
const parsed = JSON.parse(raw);
|
|
5301
5746
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
5302
5747
|
return parsed;
|
|
@@ -5305,7 +5750,7 @@ async function readSession(path) {
|
|
|
5305
5750
|
}
|
|
5306
5751
|
}
|
|
5307
5752
|
async function writeSession(path, state) {
|
|
5308
|
-
await mkdir11(
|
|
5753
|
+
await mkdir11(dirname13(path), { recursive: true });
|
|
5309
5754
|
await writeFile10(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
5310
5755
|
}
|
|
5311
5756
|
|
|
@@ -5352,33 +5797,7 @@ async function handleContextUpdate(req, ctx) {
|
|
|
5352
5797
|
|
|
5353
5798
|
// src/server/routes/gate.ts
|
|
5354
5799
|
import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
|
|
5355
|
-
import { dirname as
|
|
5356
|
-
|
|
5357
|
-
// src/shared/config.ts
|
|
5358
|
-
function num(name, fallback) {
|
|
5359
|
-
const v = process.env[name];
|
|
5360
|
-
if (!v) return fallback;
|
|
5361
|
-
const n = Number(v);
|
|
5362
|
-
return Number.isFinite(n) ? n : fallback;
|
|
5363
|
-
}
|
|
5364
|
-
function str(name, fallback) {
|
|
5365
|
-
return process.env[name] ?? fallback;
|
|
5366
|
-
}
|
|
5367
|
-
function loadConfig() {
|
|
5368
|
-
return {
|
|
5369
|
-
hardMaxReadChars: num("SYN_HARD_MAX_READ_CHARS", 4e3),
|
|
5370
|
-
gateHintMaxChars: num("SYN_GATE_HINT_CHARS", 1200),
|
|
5371
|
-
turnReadBudgetChars: num("SYN_TURN_READ_BUDGET_CHARS", 18e3),
|
|
5372
|
-
fallbackMaxCallsPerTurn: num("SYN_FALLBACK_MAX_CALLS_PER_TURN", 1),
|
|
5373
|
-
retrieveCacheTtlSec: num("SYN_RETRIEVE_CACHE_TTL_SEC", 900),
|
|
5374
|
-
mcpPort: process.env.SYN_MCP_PORT ? num("SYN_MCP_PORT", 0) : null,
|
|
5375
|
-
dashboardPort: num("SYN_DASHBOARD_PORT", 8901),
|
|
5376
|
-
logLevel: str("SYN_LOG_LEVEL", "info"),
|
|
5377
|
-
claudeBin: str("SYN_CLAUDE_BIN", "claude")
|
|
5378
|
-
};
|
|
5379
|
-
}
|
|
5380
|
-
|
|
5381
|
-
// src/server/routes/gate.ts
|
|
5800
|
+
import { dirname as dirname14 } from "path";
|
|
5382
5801
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
5383
5802
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5384
5803
|
function extractQuery(toolName, input) {
|
|
@@ -5440,7 +5859,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
5440
5859
|
var LOG_REASON_MAX_CHARS = 240;
|
|
5441
5860
|
async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
|
|
5442
5861
|
try {
|
|
5443
|
-
await mkdir12(
|
|
5862
|
+
await mkdir12(dirname14(ctx.paths.gateLog), { recursive: true });
|
|
5444
5863
|
const entry = {
|
|
5445
5864
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5446
5865
|
tool: toolName,
|
|
@@ -5568,14 +5987,14 @@ async function handleGate(req, ctx) {
|
|
|
5568
5987
|
|
|
5569
5988
|
// src/server/routes/log.ts
|
|
5570
5989
|
import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
|
|
5571
|
-
import { dirname as
|
|
5990
|
+
import { dirname as dirname15 } from "path";
|
|
5572
5991
|
async function handleLog(entry, ctx) {
|
|
5573
5992
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
5574
5993
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
5575
5994
|
}
|
|
5576
5995
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5577
5996
|
const record = { ...entry, written_at };
|
|
5578
|
-
await mkdir13(
|
|
5997
|
+
await mkdir13(dirname15(ctx.paths.tokenLog), { recursive: true });
|
|
5579
5998
|
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
5580
5999
|
return { ok: true, written_at };
|
|
5581
6000
|
}
|
|
@@ -5808,18 +6227,18 @@ async function startServer(paths, options = {}) {
|
|
|
5808
6227
|
}
|
|
5809
6228
|
|
|
5810
6229
|
// src/cli/session-discovery.ts
|
|
5811
|
-
import { readdir as
|
|
5812
|
-
import { homedir as
|
|
5813
|
-
import { join as
|
|
6230
|
+
import { readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
6231
|
+
import { homedir as homedir3 } from "os";
|
|
6232
|
+
import { join as join10 } from "path";
|
|
5814
6233
|
function encodeProjectPath(projectRoot) {
|
|
5815
6234
|
return projectRoot.replace(/[\\/:]/g, "-");
|
|
5816
6235
|
}
|
|
5817
6236
|
async function findLatestSession(projectRoot) {
|
|
5818
6237
|
const encoded = encodeProjectPath(projectRoot);
|
|
5819
|
-
const dir =
|
|
6238
|
+
const dir = join10(homedir3(), ".claude", "projects", encoded);
|
|
5820
6239
|
let entries;
|
|
5821
6240
|
try {
|
|
5822
|
-
entries = await
|
|
6241
|
+
entries = await readdir3(dir);
|
|
5823
6242
|
} catch {
|
|
5824
6243
|
return null;
|
|
5825
6244
|
}
|
|
@@ -5827,7 +6246,7 @@ async function findLatestSession(projectRoot) {
|
|
|
5827
6246
|
if (jsonlFiles.length === 0) return null;
|
|
5828
6247
|
let latest = null;
|
|
5829
6248
|
for (const file of jsonlFiles) {
|
|
5830
|
-
const path =
|
|
6249
|
+
const path = join10(dir, file);
|
|
5831
6250
|
try {
|
|
5832
6251
|
const s = await stat3(path);
|
|
5833
6252
|
if (!latest || s.mtime > latest.modifiedAt) {
|
|
@@ -5882,8 +6301,8 @@ async function dashboardCommand(rawPath) {
|
|
|
5882
6301
|
}
|
|
5883
6302
|
|
|
5884
6303
|
// src/cli/doctor-command.ts
|
|
5885
|
-
import { readFile as
|
|
5886
|
-
import { join as
|
|
6304
|
+
import { readFile as readFile18, stat as stat4 } from "fs/promises";
|
|
6305
|
+
import { join as join11, resolve as resolve3 } from "path";
|
|
5887
6306
|
import spawn from "cross-spawn";
|
|
5888
6307
|
var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
5889
6308
|
function binWorks(bin, args) {
|
|
@@ -5951,7 +6370,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5951
6370
|
});
|
|
5952
6371
|
} else {
|
|
5953
6372
|
try {
|
|
5954
|
-
const graph = JSON.parse(await
|
|
6373
|
+
const graph = JSON.parse(await readFile18(paths.infoGraph, "utf8"));
|
|
5955
6374
|
const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
|
|
5956
6375
|
let status = "ok";
|
|
5957
6376
|
const ageMs = Date.now() - Date.parse(graph.generated_at);
|
|
@@ -5975,7 +6394,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5975
6394
|
}
|
|
5976
6395
|
}
|
|
5977
6396
|
checks.push(
|
|
5978
|
-
await exists2(
|
|
6397
|
+
await exists2(join11(projectRoot, ".mcp.json")) ? {
|
|
5979
6398
|
status: "ok",
|
|
5980
6399
|
label: "MCP registration",
|
|
5981
6400
|
detail: ".mcp.json present (IDE can see graph_* tools)"
|
|
@@ -5992,7 +6411,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5992
6411
|
detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
|
|
5993
6412
|
});
|
|
5994
6413
|
} else {
|
|
5995
|
-
const md = await
|
|
6414
|
+
const md = await readFile18(paths.claudeMd, "utf8");
|
|
5996
6415
|
if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
|
|
5997
6416
|
checks.push({
|
|
5998
6417
|
status: "ok",
|
|
@@ -6015,7 +6434,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
6015
6434
|
detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
|
|
6016
6435
|
});
|
|
6017
6436
|
} else {
|
|
6018
|
-
const s = await
|
|
6437
|
+
const s = await readFile18(paths.claudeSettings, "utf8");
|
|
6019
6438
|
checks.push(
|
|
6020
6439
|
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
|
|
6021
6440
|
status: "warn",
|
|
@@ -6045,14 +6464,14 @@ async function doctorCommand(rawPath) {
|
|
|
6045
6464
|
}
|
|
6046
6465
|
|
|
6047
6466
|
// src/cli/self-update.ts
|
|
6048
|
-
import { mkdir as mkdir14, readFile as
|
|
6049
|
-
import { homedir as
|
|
6050
|
-
import { join as
|
|
6467
|
+
import { mkdir as mkdir14, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
|
|
6468
|
+
import { homedir as homedir4 } from "os";
|
|
6469
|
+
import { join as join12 } from "path";
|
|
6051
6470
|
import { createInterface } from "readline/promises";
|
|
6052
6471
|
import spawn2 from "cross-spawn";
|
|
6053
6472
|
var PKG_NAME = "@jefuriiij/synthra";
|
|
6054
|
-
var SYNTHRA_DIR =
|
|
6055
|
-
var LAST_SEEN_PATH =
|
|
6473
|
+
var SYNTHRA_DIR = join12(homedir4(), ".synthra");
|
|
6474
|
+
var LAST_SEEN_PATH = join12(SYNTHRA_DIR, "last-seen-version.json");
|
|
6056
6475
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
|
|
6057
6476
|
var FETCH_TIMEOUT_MS = 2e3;
|
|
6058
6477
|
var currentVersionCache = null;
|
|
@@ -6103,7 +6522,7 @@ async function checkForUpdate() {
|
|
|
6103
6522
|
}
|
|
6104
6523
|
async function readLastSeen() {
|
|
6105
6524
|
try {
|
|
6106
|
-
const raw = await
|
|
6525
|
+
const raw = await readFile19(LAST_SEEN_PATH, "utf8");
|
|
6107
6526
|
const parsed = JSON.parse(raw);
|
|
6108
6527
|
return parsed.version ?? null;
|
|
6109
6528
|
} catch {
|
|
@@ -6146,7 +6565,7 @@ async function readInstalledChangelog() {
|
|
|
6146
6565
|
const root = await npmGlobalRoot();
|
|
6147
6566
|
if (!root) return null;
|
|
6148
6567
|
try {
|
|
6149
|
-
return await
|
|
6568
|
+
return await readFile19(join12(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
6150
6569
|
} catch {
|
|
6151
6570
|
return null;
|
|
6152
6571
|
}
|