@eat-pray-ai/wingman 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/codeql.yml +103 -0
  3. package/.github/workflows/publish.yml +23 -0
  4. package/.github/workflows/release.yml +52 -0
  5. package/.github/workflows/test.yml +32 -0
  6. package/.idea/workspace.xml +125 -0
  7. package/AGENTS.md +34 -0
  8. package/README.md +145 -0
  9. package/bin/wingman.ts +2 -0
  10. package/dist/bin/wingman.mjs +3 -0
  11. package/dist/src/cli.mjs +2229 -0
  12. package/dist/src/cli.mjs.map +1 -0
  13. package/docs/AGENTS.md +172 -0
  14. package/docs/resume.yaml +68 -0
  15. package/docs/wingman.pdf +2544 -1
  16. package/docs/wingman.png +0 -0
  17. package/docs/wingman.svg +376 -0
  18. package/package.json +51 -0
  19. package/scripts/generate-demo.ts +265 -0
  20. package/src/AGENTS.md +50 -0
  21. package/src/agents/AGENTS.md +52 -0
  22. package/src/agents/claude-code.ts +160 -0
  23. package/src/agents/codex.ts +174 -0
  24. package/src/agents/gemini-cli.ts +100 -0
  25. package/src/agents/opencode.ts +117 -0
  26. package/src/agents/registry.ts +12 -0
  27. package/src/agents/skills.ts +51 -0
  28. package/src/aggregator.ts +142 -0
  29. package/src/cli.ts +194 -0
  30. package/src/inventory.ts +84 -0
  31. package/src/pricing/AGENTS.md +36 -0
  32. package/src/pricing/__tests__/model-info.test.ts +135 -0
  33. package/src/pricing/engine.ts +86 -0
  34. package/src/pricing/models-dev.ts +253 -0
  35. package/src/resume/AGENTS.md +34 -0
  36. package/src/resume/__tests__/renderer.test.ts +286 -0
  37. package/src/resume/renderer.ts +286 -0
  38. package/src/svg/AGENTS.md +22 -0
  39. package/src/svg/components.ts +266 -0
  40. package/src/svg/icons.ts +83 -0
  41. package/src/themes/AGENTS.md +60 -0
  42. package/src/themes/__tests__/themes.test.ts +187 -0
  43. package/src/themes/github-dark/index.ts +46 -0
  44. package/src/themes/github-dark/palette.ts +19 -0
  45. package/src/themes/github-light/index.ts +46 -0
  46. package/src/themes/github-light/palette.ts +19 -0
  47. package/src/themes/onedark/index.ts +46 -0
  48. package/src/themes/onedark/palette.ts +19 -0
  49. package/src/themes/registry.ts +18 -0
  50. package/src/themes/shared/charts.ts +112 -0
  51. package/src/themes/shared/context.ts +47 -0
  52. package/src/themes/shared/footer.ts +52 -0
  53. package/src/themes/shared/header.ts +29 -0
  54. package/src/themes/shared/heatmap.ts +229 -0
  55. package/src/themes/shared/helpers.ts +103 -0
  56. package/src/themes/shared/inventory.ts +105 -0
  57. package/src/themes/shared/legend.ts +91 -0
  58. package/src/themes/shared/sections.ts +20 -0
  59. package/src/themes/shared/stats.ts +60 -0
  60. package/src/types.ts +106 -0
  61. package/tsconfig.json +18 -0
  62. package/tsdown.config.ts +10 -0
@@ -0,0 +1,2229 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import Database from "better-sqlite3";
6
+ import { parse } from "jsonc-parser";
7
+ import { parse as parse$1 } from "smol-toml";
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ //#region src/agents/skills.ts
10
+ /**
11
+ * Scan a skills directory for SKILL.md files and return skill names.
12
+ * Handles both flat and nested layouts:
13
+ * skills/my-skill/SKILL.md → extracts `name` from frontmatter, falls back to dir name
14
+ * skills/.system/foo/SKILL.md → same, under .system prefix
15
+ * skills/.curated/foo/SKILL.md → same
16
+ */
17
+ function scanSkillDir(dir) {
18
+ if (!existsSync(dir)) return [];
19
+ const skills = [];
20
+ try {
21
+ for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) {
22
+ const skillMd = join(dir, entry.name, "SKILL.md");
23
+ if (existsSync(skillMd)) skills.push(parseSkillName(skillMd, entry.name));
24
+ else try {
25
+ for (const sub of readdirSync(join(dir, entry.name), { withFileTypes: true })) if (sub.isDirectory()) {
26
+ const nestedMd = join(dir, entry.name, sub.name, "SKILL.md");
27
+ if (existsSync(nestedMd)) skills.push(parseSkillName(nestedMd, sub.name));
28
+ }
29
+ } catch {}
30
+ }
31
+ } catch {}
32
+ return skills;
33
+ }
34
+ /** Extract `name` from SKILL.md YAML frontmatter, fallback to dirName */
35
+ function parseSkillName(path, dirName) {
36
+ try {
37
+ const match = readFileSync(path, "utf-8").match(/^---\s*\n([\s\S]*?)\n---/);
38
+ if (match) {
39
+ const nameMatch = match[1].match(/^name:\s*(.+)$/m);
40
+ if (nameMatch) return nameMatch[1].trim().replace(/^["']|["']$/g, "");
41
+ }
42
+ } catch {}
43
+ return dirName;
44
+ }
45
+ //#endregion
46
+ //#region src/agents/claude-code.ts
47
+ const CLAUDE_DIR = join(homedir(), ".claude");
48
+ const PROJECTS_DIR = join(CLAUDE_DIR, "projects");
49
+ const GLOBAL_SKILLS_DIR = join(CLAUDE_DIR, "skills");
50
+ function parsePluginDir(name, installPath, version) {
51
+ const info = {
52
+ name,
53
+ version,
54
+ skills: [],
55
+ agents: [],
56
+ commands: [],
57
+ sources: []
58
+ };
59
+ if (!installPath || !existsSync(installPath)) return info;
60
+ const skillsDir = join(installPath, "skills");
61
+ if (existsSync(skillsDir)) try {
62
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) if (entry.isDirectory()) info.skills.push(entry.name);
63
+ } catch {}
64
+ const agentsDir = join(installPath, "agents");
65
+ if (existsSync(agentsDir)) try {
66
+ for (const entry of readdirSync(agentsDir, { withFileTypes: true })) if (entry.isFile() && entry.name.endsWith(".md")) info.agents.push(entry.name.replace(/\.md$/, ""));
67
+ } catch {}
68
+ const commandsDir = join(installPath, "commands");
69
+ if (existsSync(commandsDir)) try {
70
+ for (const entry of readdirSync(commandsDir, { withFileTypes: true })) if (entry.isFile() && entry.name.endsWith(".md")) info.commands.push(entry.name.replace(/\.md$/, ""));
71
+ } catch {}
72
+ return info;
73
+ }
74
+ var claude_code_default = {
75
+ name: "claude-code",
76
+ displayName: "Claude Code",
77
+ async detect() {
78
+ return existsSync(CLAUDE_DIR);
79
+ },
80
+ async collect(since, until) {
81
+ const records = [];
82
+ try {
83
+ if (!existsSync(PROJECTS_DIR)) return records;
84
+ const parseJsonl = (filepath, sessionId) => {
85
+ try {
86
+ const content = readFileSync(filepath, "utf-8");
87
+ for (const line of content.split("\n")) {
88
+ if (!line.trim()) continue;
89
+ try {
90
+ const entry = JSON.parse(line);
91
+ if (entry.type !== "assistant") continue;
92
+ if (entry.message?.model === "<synthetic>") continue;
93
+ const ts = new Date(entry.timestamp);
94
+ if (ts < since || ts >= until) continue;
95
+ const usage = entry.message?.usage;
96
+ if (!usage) continue;
97
+ records.push({
98
+ agent: "claude-code",
99
+ model: entry.message.model ?? "unknown",
100
+ provider: "anthropic",
101
+ timestamp: ts,
102
+ tokens: {
103
+ input: usage.input_tokens ?? 0,
104
+ output: usage.output_tokens ?? 0,
105
+ cacheRead: usage.cache_read_input_tokens ?? 0,
106
+ cacheWrite: usage.cache_creation_input_tokens ?? 0
107
+ },
108
+ sessionId: sessionId ?? entry.sessionId
109
+ });
110
+ } catch {}
111
+ }
112
+ } catch {}
113
+ };
114
+ const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
115
+ for (const projectDir of projectDirs) {
116
+ const projectPath = join(PROJECTS_DIR, projectDir.name);
117
+ for (const file of readdirSync(projectPath, { withFileTypes: true })) if (file.isFile() && file.name.endsWith(".jsonl")) {
118
+ const sessionId = file.name.replace(/\.jsonl$/, "");
119
+ parseJsonl(join(projectPath, file.name), sessionId);
120
+ }
121
+ for (const entry of readdirSync(projectPath, { withFileTypes: true })) {
122
+ if (!entry.isDirectory()) continue;
123
+ const subagentsDir = join(projectPath, entry.name, "subagents");
124
+ if (!existsSync(subagentsDir)) continue;
125
+ for (const sub of readdirSync(subagentsDir, { withFileTypes: true })) if (sub.isFile() && sub.name.endsWith(".jsonl")) parseJsonl(join(subagentsDir, sub.name), entry.name);
126
+ }
127
+ }
128
+ } catch {}
129
+ return records;
130
+ },
131
+ async config() {
132
+ const cfg = {
133
+ mcpServers: [],
134
+ plugins: [],
135
+ models: [],
136
+ skills: []
137
+ };
138
+ try {
139
+ const pluginsPath = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
140
+ if (existsSync(pluginsPath)) {
141
+ const pluginsData = JSON.parse(readFileSync(pluginsPath, "utf-8"));
142
+ if (pluginsData.plugins && typeof pluginsData.plugins === "object") for (const [key, entries] of Object.entries(pluginsData.plugins)) {
143
+ const name = key.replace(/@[^@]+$/, "");
144
+ const entry = Array.isArray(entries) ? entries[0] : void 0;
145
+ const installPath = entry?.installPath;
146
+ const version = entry?.version;
147
+ const info = parsePluginDir(name, installPath, version);
148
+ cfg.plugins.push(info);
149
+ }
150
+ }
151
+ } catch {}
152
+ cfg.skills.push(...scanSkillDir(GLOBAL_SKILLS_DIR));
153
+ return cfg;
154
+ }
155
+ };
156
+ //#endregion
157
+ //#region src/agents/opencode.ts
158
+ const DB_PATH$1 = join(homedir(), ".local", "share", "opencode", "opencode.db");
159
+ const CONFIG_PATH$1 = join(homedir(), ".config", "opencode", "opencode.jsonc");
160
+ const SKILLS_DIR$2 = join(homedir(), ".config", "opencode", "skills");
161
+ const SHARED_SKILLS_DIR$2 = join(homedir(), ".agents", "skills");
162
+ var opencode_default = {
163
+ name: "opencode",
164
+ displayName: "opencode",
165
+ async detect() {
166
+ return existsSync(DB_PATH$1);
167
+ },
168
+ async collect(since, until) {
169
+ const records = [];
170
+ try {
171
+ if (!existsSync(DB_PATH$1)) return records;
172
+ const db = new Database(DB_PATH$1, { readonly: true });
173
+ try {
174
+ const sinceMs = since.getTime();
175
+ const untilMs = until.getTime();
176
+ const rows = db.prepare("SELECT id, session_id, data FROM message WHERE time_created >= ? AND time_created <= ?").all(sinceMs, untilMs);
177
+ for (const row of rows) try {
178
+ const data = JSON.parse(row.data);
179
+ if (data.role !== "assistant") continue;
180
+ const ts = data.time?.created ? new Date(data.time.created) : new Date(sinceMs);
181
+ records.push({
182
+ agent: "opencode",
183
+ model: data.modelID ?? "unknown",
184
+ provider: data.providerID,
185
+ timestamp: ts,
186
+ tokens: {
187
+ input: data.tokens?.input ?? 0,
188
+ output: data.tokens?.output ?? 0,
189
+ cacheRead: data.tokens?.cache?.read ?? 0,
190
+ cacheWrite: data.tokens?.cache?.write ?? 0,
191
+ reasoning: data.tokens?.reasoning ?? 0
192
+ },
193
+ sessionId: row.session_id
194
+ });
195
+ } catch {}
196
+ } finally {
197
+ db.close();
198
+ }
199
+ } catch {}
200
+ return records;
201
+ },
202
+ async config() {
203
+ const cfg = {
204
+ mcpServers: [],
205
+ plugins: [],
206
+ models: [],
207
+ skills: []
208
+ };
209
+ try {
210
+ if (!existsSync(CONFIG_PATH$1)) return cfg;
211
+ const parsed = parse(readFileSync(CONFIG_PATH$1, "utf-8"));
212
+ if (parsed.mcp && typeof parsed.mcp === "object") cfg.mcpServers = Object.keys(parsed.mcp);
213
+ if (Array.isArray(parsed.plugin)) for (const p of parsed.plugin) {
214
+ const raw = typeof p === "string" ? p : String(p);
215
+ const atIdx = raw.indexOf("@");
216
+ const name = atIdx > 0 ? raw.slice(0, atIdx) : raw;
217
+ cfg.plugins.push({
218
+ name,
219
+ skills: [],
220
+ agents: [],
221
+ commands: [],
222
+ sources: []
223
+ });
224
+ }
225
+ if (parsed.provider && typeof parsed.provider === "object") for (const providerKey of Object.keys(parsed.provider)) {
226
+ const provider = parsed.provider[providerKey];
227
+ if (provider.models && typeof provider.models === "object") cfg.models.push(...Object.keys(provider.models));
228
+ }
229
+ } catch {}
230
+ cfg.skills.push(...scanSkillDir(SKILLS_DIR$2), ...scanSkillDir(SHARED_SKILLS_DIR$2));
231
+ return cfg;
232
+ }
233
+ };
234
+ //#endregion
235
+ //#region src/agents/gemini-cli.ts
236
+ const GEMINI_DIR = join(homedir(), ".gemini");
237
+ const TMP_DIR = join(GEMINI_DIR, "tmp");
238
+ const SKILLS_DIR$1 = join(GEMINI_DIR, "skills");
239
+ const SHARED_SKILLS_DIR$1 = join(homedir(), ".agents", "skills");
240
+ var gemini_cli_default = {
241
+ name: "gemini-cli",
242
+ displayName: "Gemini CLI",
243
+ async detect() {
244
+ return existsSync(GEMINI_DIR);
245
+ },
246
+ async collect(since, until) {
247
+ const records = [];
248
+ try {
249
+ if (!existsSync(TMP_DIR)) return records;
250
+ const tmpDirs = readdirSync(TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
251
+ for (const tmpDir of tmpDirs) {
252
+ const chatsDir = join(TMP_DIR, tmpDir.name, "chats");
253
+ if (!existsSync(chatsDir)) continue;
254
+ const sessionFiles = readdirSync(chatsDir, { withFileTypes: true }).filter((f) => f.isFile() && f.name.startsWith("session-") && f.name.endsWith(".json"));
255
+ for (const file of sessionFiles) try {
256
+ const content = readFileSync(join(chatsDir, file.name), "utf-8");
257
+ const session = JSON.parse(content);
258
+ const sessionId = session.sessionId;
259
+ if (!Array.isArray(session.messages)) continue;
260
+ for (const msg of session.messages) {
261
+ if (msg.type !== "gemini") continue;
262
+ const ts = new Date(msg.timestamp);
263
+ if (ts < since || ts >= until) continue;
264
+ records.push({
265
+ agent: "gemini-cli",
266
+ model: msg.model ?? "unknown",
267
+ provider: "google",
268
+ timestamp: ts,
269
+ tokens: {
270
+ input: msg.tokens?.input ?? 0,
271
+ output: msg.tokens?.output ?? 0,
272
+ cacheRead: msg.tokens?.cached ?? 0,
273
+ reasoning: msg.tokens?.thoughts ?? 0
274
+ },
275
+ sessionId
276
+ });
277
+ }
278
+ } catch {}
279
+ }
280
+ } catch {}
281
+ return records;
282
+ },
283
+ async config() {
284
+ const cfg = {
285
+ mcpServers: [],
286
+ plugins: [],
287
+ models: [],
288
+ skills: []
289
+ };
290
+ try {
291
+ const settingsPath = join(GEMINI_DIR, "settings.json");
292
+ if (!existsSync(settingsPath)) return cfg;
293
+ const content = readFileSync(settingsPath, "utf-8");
294
+ const settings = JSON.parse(content);
295
+ if (settings.mcpServers && typeof settings.mcpServers === "object") cfg.mcpServers = Object.keys(settings.mcpServers);
296
+ } catch {}
297
+ cfg.skills.push(...scanSkillDir(SKILLS_DIR$1), ...scanSkillDir(SHARED_SKILLS_DIR$1));
298
+ return cfg;
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/agents/codex.ts
303
+ const CODEX_DIR = join(homedir(), ".codex");
304
+ const DB_PATH = join(CODEX_DIR, "state_5.sqlite");
305
+ const CONFIG_PATH = join(CODEX_DIR, "config.toml");
306
+ const SKILLS_DIR = join(CODEX_DIR, "skills");
307
+ const PLUGINS_CACHE_DIR = join(CODEX_DIR, "plugins", "cache");
308
+ const SHARED_SKILLS_DIR = join(homedir(), ".agents", "skills");
309
+ function parseCodexPlugin(pluginDir) {
310
+ const manifestPath = join(pluginDir, ".codex-plugin", "plugin.json");
311
+ if (!existsSync(manifestPath)) return null;
312
+ try {
313
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
314
+ const info = {
315
+ name: manifest.name || pluginDir.split("/").pop() || "unknown",
316
+ version: manifest.version,
317
+ skills: [],
318
+ agents: [],
319
+ commands: [],
320
+ sources: []
321
+ };
322
+ const skillsRel = manifest.skills;
323
+ const skillsDir = skillsRel ? join(pluginDir, skillsRel) : join(pluginDir, "skills");
324
+ if (existsSync(skillsDir)) info.skills.push(...scanSkillDir(skillsDir));
325
+ return info;
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ function discoverPlugins() {
331
+ if (!existsSync(PLUGINS_CACHE_DIR)) return [];
332
+ const plugins = [];
333
+ try {
334
+ for (const marketplace of readdirSync(PLUGINS_CACHE_DIR, { withFileTypes: true })) {
335
+ if (!marketplace.isDirectory()) continue;
336
+ const marketDir = join(PLUGINS_CACHE_DIR, marketplace.name);
337
+ for (const plugin of readdirSync(marketDir, { withFileTypes: true })) {
338
+ if (!plugin.isDirectory()) continue;
339
+ const pluginDir = join(marketDir, plugin.name);
340
+ const versions = readdirSync(pluginDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
341
+ const latest = versions[versions.length - 1];
342
+ if (!latest) continue;
343
+ const info = parseCodexPlugin(join(pluginDir, latest));
344
+ if (info) plugins.push(info);
345
+ }
346
+ }
347
+ } catch {}
348
+ return plugins;
349
+ }
350
+ //#endregion
351
+ //#region src/agents/registry.ts
352
+ const adapters = [
353
+ claude_code_default,
354
+ opencode_default,
355
+ gemini_cli_default,
356
+ {
357
+ name: "codex",
358
+ displayName: "Codex",
359
+ async detect() {
360
+ return existsSync(CODEX_DIR);
361
+ },
362
+ async collect(since, until) {
363
+ const records = [];
364
+ try {
365
+ if (!existsSync(DB_PATH)) return records;
366
+ const db = new Database(DB_PATH, { readonly: true });
367
+ try {
368
+ const sinceSec = Math.floor(since.getTime() / 1e3);
369
+ const untilSec = Math.floor(until.getTime() / 1e3);
370
+ const rows = db.prepare("SELECT id, model, model_provider, tokens_used, created_at FROM threads WHERE created_at >= ? AND created_at <= ?").all(sinceSec, untilSec);
371
+ for (const row of rows) try {
372
+ records.push({
373
+ agent: "codex",
374
+ model: row.model ?? "unknown",
375
+ provider: row.model_provider,
376
+ timestamp: /* @__PURE__ */ new Date(row.created_at * 1e3),
377
+ tokens: {
378
+ input: 0,
379
+ output: row.tokens_used ?? 0
380
+ },
381
+ sessionId: row.id
382
+ });
383
+ } catch {}
384
+ } finally {
385
+ db.close();
386
+ }
387
+ } catch {}
388
+ return records;
389
+ },
390
+ async config() {
391
+ const cfg = {
392
+ mcpServers: [],
393
+ plugins: [],
394
+ models: [],
395
+ skills: []
396
+ };
397
+ try {
398
+ if (!existsSync(CONFIG_PATH)) return cfg;
399
+ const parsed = parse$1(readFileSync(CONFIG_PATH, "utf-8"));
400
+ if (parsed.mcp_servers && typeof parsed.mcp_servers === "object" && !Array.isArray(parsed.mcp_servers)) cfg.mcpServers = Object.keys(parsed.mcp_servers);
401
+ if (typeof parsed.model === "string" && parsed.model) cfg.models.push(parsed.model);
402
+ if (parsed.provider && typeof parsed.provider === "object" && !Array.isArray(parsed.provider)) for (const providerKey of Object.keys(parsed.provider)) {
403
+ const provider = parsed.provider[providerKey];
404
+ if (provider?.models && typeof provider.models === "object") cfg.models.push(...Object.keys(provider.models));
405
+ }
406
+ } catch {}
407
+ cfg.plugins.push(...discoverPlugins());
408
+ cfg.skills.push(...scanSkillDir(SKILLS_DIR), ...scanSkillDir(SHARED_SKILLS_DIR));
409
+ return cfg;
410
+ }
411
+ }
412
+ ];
413
+ function getAllAdapters() {
414
+ return adapters;
415
+ }
416
+ //#endregion
417
+ //#region src/pricing/models-dev.ts
418
+ const MODELS_DEV_URL = "https://models.dev/api.json";
419
+ const CACHE_DIR = join(homedir(), ".wingman", "cache");
420
+ const CACHE_FILE = join(CACHE_DIR, "models.json");
421
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
422
+ /** Maps model family prefixes to the official AI lab name */
423
+ const FAMILY_TO_LAB = {
424
+ claude: "Anthropic",
425
+ gemini: "Google",
426
+ gemma: "Google",
427
+ gpt: "OpenAI",
428
+ o: "OpenAI",
429
+ dall: "OpenAI",
430
+ sora: "OpenAI",
431
+ deepseek: "DeepSeek",
432
+ qwen: "Alibaba",
433
+ llama: "Meta",
434
+ mistral: "Mistral",
435
+ mixtral: "Mistral",
436
+ codestral: "Mistral",
437
+ devstral: "Mistral",
438
+ pixtral: "Mistral",
439
+ ministral: "Mistral",
440
+ magistral: "Mistral",
441
+ command: "Cohere",
442
+ grok: "xAI",
443
+ phi: "Microsoft",
444
+ nova: "Amazon",
445
+ titan: "Amazon",
446
+ imagen: "Google",
447
+ glm: "Zhipu",
448
+ kimi: "Moonshot",
449
+ minimax: "MiniMax",
450
+ ernie: "Baidu",
451
+ hunyuan: "Tencent",
452
+ jamba: "AI21",
453
+ nemotron: "NVIDIA",
454
+ granite: "IBM"
455
+ };
456
+ /** Derive the official AI lab from a model's family prefix */
457
+ function modelLab(familyOrId) {
458
+ return FAMILY_TO_LAB[familyOrId.split("-")[0]] ?? "AI";
459
+ }
460
+ /**
461
+ * Normalize a model ID for fuzzy matching by stripping preview suffixes,
462
+ * date suffixes (-YYYY-MM-DD or -YYYYMMDD), and trailing version suffixes.
463
+ */
464
+ function normalizeModelId(id) {
465
+ let normalized = id;
466
+ normalized = normalized.replace(/-preview(?:-.*)?$/, "");
467
+ normalized = normalized.replace(/-\d{4}-\d{2}-\d{2}$/, "");
468
+ normalized = normalized.replace(/-\d{8}$/, "");
469
+ normalized = normalized.replace(/[:-]v[\d.]+$/, "");
470
+ normalized = normalized.replace(/:latest$/, "");
471
+ return normalized;
472
+ }
473
+ async function readCache() {
474
+ try {
475
+ const raw = await readFile(CACHE_FILE, "utf-8");
476
+ const envelope = JSON.parse(raw);
477
+ if (Date.now() - envelope.fetchedAt < CACHE_TTL_MS) return envelope.data;
478
+ } catch {}
479
+ return null;
480
+ }
481
+ async function writeCache(data) {
482
+ const envelope = {
483
+ fetchedAt: Date.now(),
484
+ data
485
+ };
486
+ try {
487
+ await mkdir(CACHE_DIR, { recursive: true });
488
+ await writeFile(CACHE_FILE, JSON.stringify(envelope), "utf-8");
489
+ } catch {}
490
+ }
491
+ async function fetchFromApi() {
492
+ const res = await fetch(MODELS_DEV_URL);
493
+ if (!res.ok) throw new Error(`Failed to fetch models.dev: ${res.status} ${res.statusText}`);
494
+ return await res.json();
495
+ }
496
+ /**
497
+ * Fetch model pricing data from models.dev (with 24h disk cache).
498
+ *
499
+ * Returns a Map keyed by modelId. Each value is an array of ModelPricing
500
+ * objects — one per provider that offers the model.
501
+ */
502
+ async function fetchModelPricing() {
503
+ let data = await readCache();
504
+ if (!data) {
505
+ data = await fetchFromApi();
506
+ await writeCache(data);
507
+ }
508
+ const result = /* @__PURE__ */ new Map();
509
+ for (const [provider, providerData] of Object.entries(data)) {
510
+ const models = providerData?.models;
511
+ if (!models || typeof models !== "object") continue;
512
+ for (const [modelId, model] of Object.entries(models)) {
513
+ if (!model?.cost) continue;
514
+ const pricing = {
515
+ modelId,
516
+ provider,
517
+ inputPerMillion: model.cost.input ?? 0,
518
+ outputPerMillion: model.cost.output ?? 0,
519
+ ...model.cost.cache_read != null && { cacheReadPerMillion: model.cost.cache_read },
520
+ ...model.cost.cache_write != null && { cacheWritePerMillion: model.cost.cache_write }
521
+ };
522
+ const existing = result.get(modelId);
523
+ if (existing) existing.push(pricing);
524
+ else result.set(modelId, [pricing]);
525
+ }
526
+ }
527
+ return result;
528
+ }
529
+ /**
530
+ * Fetch model metadata from models.dev (reuses same 24h disk cache).
531
+ *
532
+ * Returns a Map keyed by model ID. Uses the same normalized-ID fallback
533
+ * as the pricing engine for fuzzy matching.
534
+ */
535
+ async function fetchModelInfo() {
536
+ let data = await readCache();
537
+ if (!data) {
538
+ data = await fetchFromApi();
539
+ await writeCache(data);
540
+ }
541
+ const result = /* @__PURE__ */ new Map();
542
+ for (const [providerId, providerData] of Object.entries(data)) {
543
+ const models = providerData?.models;
544
+ if (!models || typeof models !== "object") continue;
545
+ for (const [modelId, model] of Object.entries(models)) {
546
+ const raw = model;
547
+ const family = typeof raw.family === "string" ? raw.family : modelId;
548
+ const info = {
549
+ id: modelId,
550
+ name: raw.name ?? modelId,
551
+ provider: providerId,
552
+ lab: modelLab(family),
553
+ capabilities: Array.isArray(raw.capabilities) ? raw.capabilities : []
554
+ };
555
+ if (typeof raw.family === "string") info.family = raw.family;
556
+ if (typeof raw.release_date === "string") info.releaseDate = raw.release_date;
557
+ if (typeof raw.knowledge === "string") info.knowledge = raw.knowledge;
558
+ const mod = raw.modalities;
559
+ if (mod && Array.isArray(mod.input) && Array.isArray(mod.output)) info.modalities = {
560
+ input: mod.input,
561
+ output: mod.output
562
+ };
563
+ const lim = raw.limits;
564
+ if (lim && typeof lim === "object") {
565
+ const limits = {};
566
+ if (typeof lim.context === "number") limits.context = lim.context;
567
+ if (typeof lim.output === "number") limits.output = lim.output;
568
+ if (limits.context !== void 0 || limits.output !== void 0) info.limits = limits;
569
+ }
570
+ result.set(modelId, info);
571
+ const normalized = normalizeModelId(modelId);
572
+ if (normalized !== modelId && !result.has(normalized)) result.set(normalized, info);
573
+ }
574
+ }
575
+ return result;
576
+ }
577
+ //#endregion
578
+ //#region src/pricing/engine.ts
579
+ /**
580
+ * Create a PricingEngine that resolves model pricing using:
581
+ * 1. Caller-supplied overrides (exact modelId + provider)
582
+ * 2. Exact match from models.dev (modelId + provider)
583
+ * 3. Exact modelId match from models.dev (any provider)
584
+ * 4. Fuzzy (normalized) modelId match from models.dev
585
+ * 5. null
586
+ */
587
+ async function createPricingEngine(overrides) {
588
+ const catalog = await fetchModelPricing();
589
+ const overrideMap = /* @__PURE__ */ new Map();
590
+ if (overrides) for (const o of overrides) overrideMap.set(`${o.modelId}::${o.provider}`, o);
591
+ const normalizedIndex = /* @__PURE__ */ new Map();
592
+ for (const [modelId, pricings] of catalog) {
593
+ const key = normalizeModelId(modelId);
594
+ const existing = normalizedIndex.get(key);
595
+ if (existing) existing.push(...pricings);
596
+ else normalizedIndex.set(key, [...pricings]);
597
+ }
598
+ function resolve(modelId, provider) {
599
+ if (provider) {
600
+ const override = overrideMap.get(`${modelId}::${provider}`);
601
+ if (override) return override;
602
+ }
603
+ const exactEntries = catalog.get(modelId);
604
+ if (exactEntries && provider) {
605
+ const match = exactEntries.find((p) => p.provider === provider);
606
+ if (match) return match;
607
+ }
608
+ if (exactEntries && exactEntries.length > 0) return exactEntries[0];
609
+ const normalized = normalizeModelId(modelId);
610
+ const fuzzyEntries = normalizedIndex.get(normalized);
611
+ if (fuzzyEntries && fuzzyEntries.length > 0) {
612
+ if (provider) {
613
+ const match = fuzzyEntries.find((p) => p.provider === provider);
614
+ if (match) return match;
615
+ }
616
+ return fuzzyEntries[0];
617
+ }
618
+ return null;
619
+ }
620
+ function calculateCost(record) {
621
+ const pricing = resolve(record.model, record.provider);
622
+ if (!pricing) return 0;
623
+ const { tokens } = record;
624
+ const input = tokens.input * pricing.inputPerMillion;
625
+ const output = tokens.output * pricing.outputPerMillion;
626
+ const cacheRead = (tokens.cacheRead ?? 0) * (pricing.cacheReadPerMillion ?? 0);
627
+ const cacheWrite = (tokens.cacheWrite ?? 0) * (pricing.cacheWritePerMillion ?? 0);
628
+ return (input + output + cacheRead + cacheWrite) / 1e6;
629
+ }
630
+ return {
631
+ resolve,
632
+ calculateCost
633
+ };
634
+ }
635
+ //#endregion
636
+ //#region src/inventory.ts
637
+ function buildInventory(agents) {
638
+ const pluginMap = /* @__PURE__ */ new Map();
639
+ const mcpServerSources = /* @__PURE__ */ new Map();
640
+ const skillSources = /* @__PURE__ */ new Map();
641
+ const pluginBundledSkills = /* @__PURE__ */ new Set();
642
+ for (const agent of agents) {
643
+ for (const s of agent.config.mcpServers) {
644
+ if (!mcpServerSources.has(s)) mcpServerSources.set(s, /* @__PURE__ */ new Set());
645
+ mcpServerSources.get(s).add(agent.agent);
646
+ }
647
+ for (const s of agent.config.skills) {
648
+ if (!skillSources.has(s)) skillSources.set(s, /* @__PURE__ */ new Set());
649
+ skillSources.get(s).add(agent.agent);
650
+ }
651
+ for (const plugin of agent.config.plugins) {
652
+ const existing = pluginMap.get(plugin.name);
653
+ if (existing) {
654
+ if (!existing.sources.includes(agent.agent)) existing.sources.push(agent.agent);
655
+ for (const s of plugin.skills) if (!existing.skills.includes(s)) existing.skills.push(s);
656
+ for (const a of plugin.agents) if (!existing.agents.includes(a)) existing.agents.push(a);
657
+ for (const c of plugin.commands) if (!existing.commands.includes(c)) existing.commands.push(c);
658
+ if (!existing.version && plugin.version) existing.version = plugin.version;
659
+ } else pluginMap.set(plugin.name, {
660
+ ...plugin,
661
+ skills: [...plugin.skills],
662
+ agents: [...plugin.agents],
663
+ commands: [...plugin.commands],
664
+ sources: [agent.agent]
665
+ });
666
+ }
667
+ }
668
+ for (const plugin of pluginMap.values()) for (const s of plugin.skills) pluginBundledSkills.add(s);
669
+ const danglingMcp = [...mcpServerSources.entries()].map(([name, sources]) => ({
670
+ name,
671
+ sources: [...sources]
672
+ })).sort((a, b) => a.name.localeCompare(b.name));
673
+ const danglingSkills = [...skillSources.entries()].filter(([name]) => !pluginBundledSkills.has(name)).map(([name, sources]) => ({
674
+ name,
675
+ sources: [...sources]
676
+ })).sort((a, b) => a.name.localeCompare(b.name));
677
+ const plugins = [...pluginMap.values()].sort((a, b) => a.name.localeCompare(b.name));
678
+ for (const p of plugins) {
679
+ p.skills.sort();
680
+ p.agents.sort();
681
+ p.commands.sort();
682
+ }
683
+ return {
684
+ plugins,
685
+ mcpServers: danglingMcp,
686
+ skills: danglingSkills
687
+ };
688
+ }
689
+ //#endregion
690
+ //#region src/aggregator.ts
691
+ function sumTokens(record) {
692
+ const t = record.tokens;
693
+ return t.input + t.output + (t.cacheRead ?? 0) + (t.cacheWrite ?? 0) + (t.reasoning ?? 0);
694
+ }
695
+ function formatDay(date) {
696
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
697
+ }
698
+ function aggregate(records, configs, pricing, since, until) {
699
+ const grouped = /* @__PURE__ */ new Map();
700
+ for (const record of records) {
701
+ let list = grouped.get(record.agent);
702
+ if (!list) {
703
+ list = [];
704
+ grouped.set(record.agent, list);
705
+ }
706
+ list.push(record);
707
+ }
708
+ const agents = [];
709
+ for (const [agent, agentRecords] of grouped) {
710
+ let totalTokens = 0;
711
+ let totalCost = 0;
712
+ let unknownCost = false;
713
+ const sessions = /* @__PURE__ */ new Set();
714
+ const models = {};
715
+ const dailyActivity = {};
716
+ for (const record of agentRecords) {
717
+ const tokens = sumTokens(record);
718
+ const cost = pricing.calculateCost(record);
719
+ if (pricing.resolve(record.model, record.provider) === null) unknownCost = true;
720
+ totalTokens += tokens;
721
+ totalCost += cost;
722
+ if (record.sessionId) sessions.add(record.sessionId);
723
+ if (!models[record.model]) models[record.model] = {
724
+ tokens: 0,
725
+ cost: 0
726
+ };
727
+ models[record.model].tokens += tokens;
728
+ models[record.model].cost += cost;
729
+ const day = formatDay(record.timestamp);
730
+ dailyActivity[day] = (dailyActivity[day] ?? 0) + tokens;
731
+ }
732
+ const entry = configs.get(agent);
733
+ const displayName = entry?.displayName ?? agent;
734
+ const config = entry?.config ?? {
735
+ mcpServers: [],
736
+ plugins: [],
737
+ models: [],
738
+ skills: []
739
+ };
740
+ agents.push({
741
+ agent,
742
+ displayName,
743
+ totalTokens,
744
+ totalCost,
745
+ unknownCost,
746
+ sessionCount: sessions.size,
747
+ models,
748
+ dailyActivity,
749
+ config
750
+ });
751
+ }
752
+ agents.sort((a, b) => b.totalTokens - a.totalTokens);
753
+ let totalInput = 0;
754
+ let totalOutput = 0;
755
+ let totalCacheRead = 0;
756
+ let totalCacheWrite = 0;
757
+ for (const record of records) {
758
+ totalInput += record.tokens.input;
759
+ totalOutput += record.tokens.output;
760
+ totalCacheRead += record.tokens.cacheRead ?? 0;
761
+ totalCacheWrite += record.tokens.cacheWrite ?? 0;
762
+ }
763
+ const totals = {
764
+ tokens: agents.reduce((sum, a) => sum + a.totalTokens, 0),
765
+ inputTokens: totalInput,
766
+ outputTokens: totalOutput,
767
+ cacheReadTokens: totalCacheRead,
768
+ cacheWriteTokens: totalCacheWrite,
769
+ cost: agents.reduce((sum, a) => sum + a.totalCost, 0),
770
+ sessions: agents.reduce((sum, a) => sum + a.sessionCount, 0)
771
+ };
772
+ const modelDailyActivity = {};
773
+ for (const record of records) {
774
+ const day = formatDay(record.timestamp);
775
+ const tokens = sumTokens(record);
776
+ if (!modelDailyActivity[record.model]) modelDailyActivity[record.model] = {};
777
+ modelDailyActivity[record.model][day] = (modelDailyActivity[record.model][day] ?? 0) + tokens;
778
+ }
779
+ const inventory = buildInventory(agents);
780
+ return {
781
+ period: {
782
+ since,
783
+ until
784
+ },
785
+ agents,
786
+ totals,
787
+ modelDailyActivity,
788
+ inventory
789
+ };
790
+ }
791
+ //#endregion
792
+ //#region src/svg/components.ts
793
+ /**
794
+ * Reusable SVG helper functions for rendering card components.
795
+ * All functions are pure and return SVG string fragments.
796
+ */
797
+ const MONO_FONT = `"ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, monospace"`;
798
+ function formatNumber(n) {
799
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
800
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
801
+ return n.toLocaleString("en-US");
802
+ }
803
+ function formatCost$1(n) {
804
+ if (n >= 1e3) return `$${Math.round(n).toLocaleString("en-US")}`;
805
+ return `$${n.toFixed(2)}`;
806
+ }
807
+ function formatDate(d) {
808
+ return `${[
809
+ "Jan",
810
+ "Feb",
811
+ "Mar",
812
+ "Apr",
813
+ "May",
814
+ "Jun",
815
+ "Jul",
816
+ "Aug",
817
+ "Sep",
818
+ "Oct",
819
+ "Nov",
820
+ "Dec"
821
+ ][d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
822
+ }
823
+ function escapeXml(s) {
824
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
825
+ }
826
+ function svgText(x, y, text, opts = {}) {
827
+ return `<text x="${x}" y="${y}" fill="${opts.fill ?? "#e6edf3"}" font-size="${opts.size ?? 14}" font-weight="${opts.weight ?? "normal"}" text-anchor="${opts.anchor ?? "start"}" font-family=${opts.font ?? MONO_FONT}>${escapeXml(text)}</text>`;
828
+ }
829
+ function svgRect(x, y, w, h, opts = {}) {
830
+ const parts = [`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${opts.fill ?? "none"}" rx="${opts.rx ?? 0}"`];
831
+ if (opts.opacity !== void 0) parts.push(` opacity="${opts.opacity}"`);
832
+ if (opts.stroke) parts.push(` stroke="${opts.stroke}" stroke-width="1"`);
833
+ parts.push(`/>`);
834
+ return parts.join("");
835
+ }
836
+ function svgLine(x1, y1, x2, y2, opts = {}) {
837
+ return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${opts.stroke ?? "#21262d"}" stroke-width="${opts.width ?? 1}"/>`;
838
+ }
839
+ function svgCircle(cx, cy, r, opts = {}) {
840
+ return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${opts.fill ?? "#e6edf3"}"/>`;
841
+ }
842
+ /**
843
+ * Render a donut/pie chart with labeled slices.
844
+ * Returns SVG string fragment.
845
+ */
846
+ function svgDonut(cx, cy, radius, innerRadius, slices, opts = {}) {
847
+ const total = slices.reduce((s, sl) => s + sl.value, 0);
848
+ if (total === 0) return "";
849
+ const parts = [];
850
+ let startAngle = -Math.PI / 2;
851
+ for (const slice of slices) {
852
+ const pct = slice.value / total;
853
+ if (pct === 0) continue;
854
+ const endAngle = startAngle + pct * 2 * Math.PI;
855
+ const largeArc = pct > .5 ? 1 : 0;
856
+ const ox1 = cx + radius * Math.cos(startAngle);
857
+ const oy1 = cy + radius * Math.sin(startAngle);
858
+ const ox2 = cx + radius * Math.cos(endAngle);
859
+ const oy2 = cy + radius * Math.sin(endAngle);
860
+ const ix1 = cx + innerRadius * Math.cos(endAngle);
861
+ const iy1 = cy + innerRadius * Math.sin(endAngle);
862
+ const ix2 = cx + innerRadius * Math.cos(startAngle);
863
+ const iy2 = cy + innerRadius * Math.sin(startAngle);
864
+ if (pct >= .9999) {
865
+ const bg = opts.bgFill ?? "#0d1117";
866
+ parts.push(`<circle cx="${cx}" cy="${cy}" r="${radius}" fill="${slice.color}"/>`, `<circle cx="${cx}" cy="${cy}" r="${innerRadius}" fill="${bg}"/>`);
867
+ } else {
868
+ const d = [
869
+ `M ${ox1.toFixed(1)} ${oy1.toFixed(1)}`,
870
+ `A ${radius} ${radius} 0 ${largeArc} 1 ${ox2.toFixed(1)} ${oy2.toFixed(1)}`,
871
+ `L ${ix1.toFixed(1)} ${iy1.toFixed(1)}`,
872
+ `A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${ix2.toFixed(1)} ${iy2.toFixed(1)}`,
873
+ `Z`
874
+ ].join(" ");
875
+ parts.push(`<path d="${d}" fill="${slice.color}"/>`);
876
+ }
877
+ startAngle = endAngle;
878
+ }
879
+ return parts.join("\n");
880
+ }
881
+ function svgPill(x, y, text, opts = {}) {
882
+ const fill = opts.fill ?? "#21262d";
883
+ const textFill = opts.textFill ?? "#8b949e";
884
+ const height = opts.height ?? 20;
885
+ const charWidth = 7;
886
+ const padding = 12;
887
+ const badges = opts.badges ?? [];
888
+ const badgeSize = 6;
889
+ const badgeGap = 3;
890
+ const badgesW = badges.length > 0 ? badges.length * (badgeSize + badgeGap) + 2 : 0;
891
+ const textWidth = text.length * charWidth;
892
+ const pillWidth = textWidth + padding * 2 + badgesW;
893
+ const parts = [svgRect(x, y, pillWidth, height, {
894
+ fill,
895
+ rx: height / 2
896
+ })];
897
+ const badgeCy = y + height / 2;
898
+ let bx = x + padding;
899
+ for (const color of badges) {
900
+ parts.push(svgRect(bx, badgeCy - badgeSize / 2, badgeSize, badgeSize, {
901
+ fill: color,
902
+ rx: 1
903
+ }));
904
+ bx += badgeSize + badgeGap;
905
+ }
906
+ parts.push(svgText(x + padding + badgesW + textWidth / 2, y + height / 2 + 4, text, {
907
+ fill: textFill,
908
+ size: 11,
909
+ anchor: "middle"
910
+ }));
911
+ return {
912
+ svg: parts.join("\n"),
913
+ width: pillWidth
914
+ };
915
+ }
916
+ //#endregion
917
+ //#region src/themes/shared/context.ts
918
+ function createContext(cardWidth, palette) {
919
+ const padX = 24;
920
+ return {
921
+ cardWidth,
922
+ contentWidth: cardWidth - padX * 2,
923
+ padX,
924
+ colors: palette.colors,
925
+ agentColors: palette.agentColors,
926
+ modelColors: palette.modelColors
927
+ };
928
+ }
929
+ //#endregion
930
+ //#region src/themes/shared/helpers.ts
931
+ const MIN_CARD_WIDTH = 660;
932
+ const MAX_CARD_WIDTH = 1200;
933
+ function separator(ctx, y) {
934
+ return svgLine(ctx.padX, y, ctx.cardWidth - ctx.padX, y, { stroke: ctx.colors.separator });
935
+ }
936
+ function shortMonth(d) {
937
+ return [
938
+ "Jan",
939
+ "Feb",
940
+ "Mar",
941
+ "Apr",
942
+ "May",
943
+ "Jun",
944
+ "Jul",
945
+ "Aug",
946
+ "Sep",
947
+ "Oct",
948
+ "Nov",
949
+ "Dec"
950
+ ][d.getMonth()];
951
+ }
952
+ function formatDateRange(since, until) {
953
+ const sameYear = since.getFullYear() === until.getFullYear();
954
+ if (sameYear && since.getMonth() === until.getMonth()) return `${shortMonth(since)} ${since.getDate()} \u2013 ${until.getDate()}, ${until.getFullYear()}`;
955
+ if (sameYear) return `${shortMonth(since)} ${since.getDate()} \u2013 ${shortMonth(until)} ${until.getDate()}, ${until.getFullYear()}`;
956
+ return `${formatDate(since)} \u2013 ${formatDate(until)}`;
957
+ }
958
+ function topModels(data, limit) {
959
+ const map = /* @__PURE__ */ new Map();
960
+ for (const agent of data.agents) for (const [modelId, stats] of Object.entries(agent.models)) {
961
+ const existing = map.get(modelId);
962
+ if (existing) {
963
+ existing.tokens += stats.tokens;
964
+ existing.cost += stats.cost;
965
+ } else map.set(modelId, {
966
+ tokens: stats.tokens,
967
+ cost: stats.cost
968
+ });
969
+ }
970
+ return [...map.entries()].map(([id, s]) => ({
971
+ id,
972
+ ...s
973
+ })).sort((a, b) => b.tokens - a.tokens).slice(0, limit);
974
+ }
975
+ function computePeriodSize(data) {
976
+ const periodStart = new Date(data.period.since);
977
+ periodStart.setHours(0, 0, 0, 0);
978
+ const end = new Date(data.period.until);
979
+ end.setHours(23, 59, 59, 999);
980
+ const numDays = Math.round((end.getTime() - periodStart.getTime()) / 864e5) + 1;
981
+ const MIN_HEATMAP_DAYS = 60;
982
+ const heatmapStart = new Date(periodStart);
983
+ if (numDays < MIN_HEATMAP_DAYS) heatmapStart.setDate(heatmapStart.getDate() - (MIN_HEATMAP_DAYS - numDays));
984
+ heatmapStart.setHours(0, 0, 0, 0);
985
+ const firstDow = (heatmapStart.getDay() + 6) % 7;
986
+ const cursor = new Date(heatmapStart);
987
+ const startTime = cursor.getTime();
988
+ let maxCol = 0;
989
+ while (cursor <= end) {
990
+ const daysSinceStart = Math.round((cursor.getTime() - startTime) / 864e5) + firstDow;
991
+ maxCol = Math.floor(daysSinceStart / 7);
992
+ cursor.setDate(cursor.getDate() + 1);
993
+ }
994
+ return {
995
+ numWeeks: maxCol + 1,
996
+ numDays
997
+ };
998
+ }
999
+ function computeCardWidth(numWeeks, numDays) {
1000
+ const colStep = Math.min(16, Math.max(6, 12)) + 2;
1001
+ const heatmapW = Math.max(numWeeks, Math.ceil(60 / 7) + 1) * colStep - 2;
1002
+ const labelColW = 28;
1003
+ const ratioColW = 38;
1004
+ const gapLR = 8;
1005
+ const padX = 24;
1006
+ if (numDays <= 182) {
1007
+ const needed = padX + heatmapW + gapLR + labelColW + ratioColW + 4 + 80 + padX;
1008
+ return Math.max(MIN_CARD_WIDTH, Math.min(MAX_CARD_WIDTH, needed));
1009
+ }
1010
+ const needed = padX + heatmapW + gapLR + labelColW + ratioColW + padX;
1011
+ return Math.max(MIN_CARD_WIDTH, Math.min(MAX_CARD_WIDTH, needed));
1012
+ }
1013
+ //#endregion
1014
+ //#region src/themes/shared/header.ts
1015
+ function renderHeader(ctx, data, y) {
1016
+ const startY = y;
1017
+ const parts = [];
1018
+ parts.push(svgText(ctx.padX, startY + 30, "Wingman Stats", {
1019
+ fill: ctx.colors.blue,
1020
+ size: 16,
1021
+ weight: "bold"
1022
+ }));
1023
+ parts.push(svgText(ctx.cardWidth - ctx.padX, startY + 30, formatDateRange(data.period.since, data.period.until), {
1024
+ fill: ctx.colors.muted,
1025
+ size: 11,
1026
+ anchor: "end"
1027
+ }));
1028
+ const endY = startY + 48;
1029
+ parts.push(separator(ctx, endY));
1030
+ return {
1031
+ svg: parts.join("\n"),
1032
+ height: endY - startY
1033
+ };
1034
+ }
1035
+ //#endregion
1036
+ //#region src/svg/icons.ts
1037
+ /**
1038
+ * Bootstrap Icons SVG path data (16x16 viewBox).
1039
+ * Source: bootstrap-icons npm package.
1040
+ *
1041
+ * Usage: svgIcon(x, y, ICONS.puzzle, { fill, size })
1042
+ */
1043
+ const ICONS = {
1044
+ puzzle: "M3.112 3.645A1.5 1.5 0 0 1 4.605 2H7a.5.5 0 0 1 .5.5v.382c0 .696-.497 1.182-.872 1.469a.5.5 0 0 0-.115.118l-.012.025L6.5 4.5v.003l.003.01q.005.015.036.053a.9.9 0 0 0 .27.194C7.09 4.9 7.51 5 8 5c.492 0 .912-.1 1.19-.24a.9.9 0 0 0 .271-.194.2.2 0 0 0 .039-.063v-.009l-.012-.025a.5.5 0 0 0-.115-.118c-.375-.287-.872-.773-.872-1.469V2.5A.5.5 0 0 1 9 2h2.395a1.5 1.5 0 0 1 1.493 1.645L12.645 6.5h.237c.195 0 .42-.147.675-.48.21-.274.528-.52.943-.52.568 0 .947.447 1.154.862C15.877 6.807 16 7.387 16 8s-.123 1.193-.346 1.638c-.207.415-.586.862-1.154.862-.415 0-.733-.246-.943-.52-.255-.333-.48-.48-.675-.48h-.237l.243 2.855A1.5 1.5 0 0 1 11.395 14H9a.5.5 0 0 1-.5-.5v-.382c0-.696.497-1.182.872-1.469a.5.5 0 0 0 .115-.118l.012-.025.001-.006v-.003a.2.2 0 0 0-.039-.064.9.9 0 0 0-.27-.193C8.91 11.1 8.49 11 8 11s-.912.1-1.19.24a.9.9 0 0 0-.271.194.2.2 0 0 0-.039.063v.003l.001.006.012.025c.016.027.05.068.115.118.375.287.872.773.872 1.469v.382a.5.5 0 0 1-.5.5H4.605a1.5 1.5 0 0 1-1.493-1.645L3.356 9.5h-.238c-.195 0-.42.147-.675.48-.21.274-.528.52-.943.52-.568 0-.947-.447-1.154-.862C.123 9.193 0 8.613 0 8s.123-1.193.346-1.638C.553 5.947.932 5.5 1.5 5.5c.415 0 .733.246.943.52.255.333.48.48.675.48h.238zM4.605 3a.5.5 0 0 0-.498.55l.001.007.29 3.4A.5.5 0 0 1 3.9 7.5h-.782c-.696 0-1.182-.497-1.469-.872a.5.5 0 0 0-.118-.115l-.025-.012L1.5 6.5h-.003a.2.2 0 0 0-.064.039.9.9 0 0 0-.193.27C1.1 7.09 1 7.51 1 8s.1.912.24 1.19c.07.14.14.225.194.271a.2.2 0 0 0 .063.039H1.5l.006-.001.025-.012a.5.5 0 0 0 .118-.115c.287-.375.773-.872 1.469-.872H3.9a.5.5 0 0 1 .498.542l-.29 3.408a.5.5 0 0 0 .497.55h1.878c-.048-.166-.195-.352-.463-.557-.274-.21-.52-.528-.52-.943 0-.568.447-.947.862-1.154C6.807 10.123 7.387 10 8 10s1.193.123 1.638.346c.415.207.862.586.862 1.154 0 .415-.246.733-.52.943-.268.205-.415.39-.463.557h1.878a.5.5 0 0 0 .498-.55l-.001-.007-.29-3.4A.5.5 0 0 1 12.1 8.5h.782c.696 0 1.182.497 1.469.872.05.065.091.099.118.115l.025.012.006.001h.003a.2.2 0 0 0 .064-.039.9.9 0 0 0 .193-.27c.14-.28.24-.7.24-1.191s-.1-.912-.24-1.19a.9.9 0 0 0-.194-.271.2.2 0 0 0-.063-.039H14.5l-.006.001-.025.012a.5.5 0 0 0-.118.115c-.287.375-.773.872-1.469.872H12.1a.5.5 0 0 1-.498-.543l.29-3.407a.5.5 0 0 0-.497-.55H9.517c.048.166.195.352.463.557.274.21.52.528.52.943 0 .568-.447.947-.862 1.154C9.193 5.877 8.613 6 8 6s-1.193-.123-1.638-.346C5.947 5.447 5.5 5.068 5.5 4.5c0-.415.246-.733.52-.943.268-.205.415-.39.463-.557z",
1045
+ tools: "M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3q0-.405-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708M3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026z",
1046
+ hexagon: "M14 4.577v6.846L8 15l-6-3.577V4.577L8 1zM8.5.134a1 1 0 0 0-1 0l-6 3.577a1 1 0 0 0-.5.866v6.846a1 1 0 0 0 .5.866l6 3.577a1 1 0 0 0 1 0l6-3.577a1 1 0 0 0 .5-.866V4.577a1 1 0 0 0-.5-.866z",
1047
+ hash: "M8.39 12.648a1 1 0 0 0-.015.18c0 .305.21.508.5.508.266 0 .492-.172.555-.477l.554-2.703h1.204c.421 0 .617-.234.617-.547 0-.312-.188-.53-.617-.53h-.985l.516-2.524h1.265c.43 0 .618-.227.618-.547 0-.313-.188-.524-.618-.524h-1.046l.476-2.304a1 1 0 0 0 .016-.164.51.51 0 0 0-.516-.516.54.54 0 0 0-.539.43l-.523 2.554H7.617l.477-2.304c.008-.04.015-.118.015-.164a.51.51 0 0 0-.523-.516.54.54 0 0 0-.531.43L6.53 5.484H5.414c-.43 0-.617.22-.617.532s.187.539.617.539h.906l-.515 2.523H4.609c-.421 0-.609.219-.609.531s.188.547.61.547h.976l-.516 2.492c-.008.04-.015.125-.015.18 0 .305.21.508.5.508.265 0 .492-.172.554-.477l.555-2.703h2.242zm-1-6.109h2.266l-.515 2.563H6.859l.532-2.563z",
1048
+ currencyDollar: "M4 10.781c.148 1.667 1.513 2.85 3.591 3.003V15h1.043v-1.216c2.27-.179 3.678-1.438 3.678-3.3 0-1.59-.947-2.51-2.956-3.028l-.722-.187V3.467c1.122.11 1.879.714 2.07 1.616h1.47c-.166-1.6-1.54-2.748-3.54-2.875V1H7.591v1.233c-1.939.23-3.27 1.472-3.27 3.156 0 1.454.966 2.483 2.661 2.917l.61.162v4.031c-1.149-.17-1.94-.8-2.131-1.718zm3.391-3.836c-1.043-.263-1.6-.825-1.6-1.616 0-.944.704-1.641 1.8-1.828v3.495l-.2-.05zm1.591 1.872c1.287.323 1.852.859 1.852 1.769 0 1.097-.826 1.828-2.2 1.939V8.73z",
1049
+ terminal: ["M6 9a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3A.5.5 0 0 1 6 9M3.854 4.146a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708z", "M2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm12 1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"],
1050
+ people: ["M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1zm-7.978-1L7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002-.014.002zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4m3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0M6.936 9.28a6 6 0 0 0-1.23-.247A7 7 0 0 0 5 9c-4 0-5 3-5 4q0 1 1 1h4.216A2.24 2.24 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816M4.92 10A5.5 5.5 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0m3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4"],
1051
+ trophy: "M2.5.5A.5.5 0 0 1 3 0h10a.5.5 0 0 1 .5.5q0 .807-.034 1.536a3 3 0 1 1-1.133 5.89c-.79 1.865-1.878 2.777-2.833 3.011v2.173l1.425.356c.194.048.377.135.537.255L13.3 15.1a.5.5 0 0 1-.3.9H3a.5.5 0 0 1-.3-.9l1.838-1.379c.16-.12.343-.207.537-.255L6.5 13.11v-2.173c-.955-.234-2.043-1.146-2.833-3.012a3 3 0 1 1-1.132-5.89A33 33 0 0 1 2.5.5m.099 2.54a2 2 0 0 0 .72 3.935c-.333-1.05-.588-2.346-.72-3.935m10.083 3.935a2 2 0 0 0 .72-3.935c-.133 1.59-.388 2.885-.72 3.935M3.504 1q.01.775.056 1.469c.13 2.028.457 3.546.87 4.667C5.294 9.48 6.484 10 7 10a.5.5 0 0 1 .5.5v2.61a1 1 0 0 1-.757.97l-1.426.356a.5.5 0 0 0-.179.085L4.5 15h7l-.638-.479a.5.5 0 0 0-.18-.085l-1.425-.356a1 1 0 0 1-.757-.97V10.5A.5.5 0 0 1 9 10c.516 0 1.706-.52 2.57-2.864.413-1.12.74-2.64.87-4.667q.045-.694.056-1.469z",
1052
+ calendar3: ["M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857z", "M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"],
1053
+ box: "M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464z",
1054
+ stars: "M7.657 6.247c.11-.33.576-.33.686 0l.645 1.937a2.89 2.89 0 0 0 1.829 1.828l1.936.645c.33.11.33.576 0 .686l-1.937.645a2.89 2.89 0 0 0-1.828 1.829l-.645 1.936a.361.361 0 0 1-.686 0l-.645-1.937a2.89 2.89 0 0 0-1.828-1.828l-1.937-.645a.361.361 0 0 1 0-.686l1.937-.645a2.89 2.89 0 0 0 1.828-1.829zM3.794 1.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.58.926 1.097 1.098l1.163.387a.217.217 0 0 1 0 .412l-1.162.387A1.73 1.73 0 0 0 4.593 5.69l-.387 1.162a.217.217 0 0 1-.412 0L3.407 5.69a1.73 1.73 0 0 0-1.097-1.098l-1.163-.387a.217.217 0 0 1 0-.412l1.162-.387a1.73 1.73 0 0 0 1.098-1.097zM10.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.16 1.16 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.16 1.16 0 0 0-.732-.732L9.1 2.137a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732z",
1055
+ lightningCharge: ["M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09zM4.157 8.5H7a.5.5 0 0 1 .478.647L6.11 13.59l5.732-6.09H9a.5.5 0 0 1-.478-.647L9.89 2.41z"]
1056
+ };
1057
+ /**
1058
+ * Render a Bootstrap Icon as an inline SVG <g> element.
1059
+ * The icon's native viewBox is 0 0 16 16; `size` scales it accordingly.
1060
+ * Accepts a single path string or an array of path strings (for multi-path icons).
1061
+ */
1062
+ function svgIcon(x, y, pathData, opts = {}) {
1063
+ const fill = opts.fill ?? "#8b949e";
1064
+ return `<g transform="translate(${x}, ${y}) scale(${(opts.size ?? 14) / 16})">${(Array.isArray(pathData) ? pathData : [pathData]).map((d) => `<path fill="${fill}" d="${d}"/>`).join("")}</g>`;
1065
+ }
1066
+ //#endregion
1067
+ //#region src/themes/shared/stats.ts
1068
+ function renderTopStats(ctx, data, y) {
1069
+ const startY = y;
1070
+ const parts = [];
1071
+ const colWidth = ctx.contentWidth / 3;
1072
+ const col1x = ctx.padX;
1073
+ parts.push(svgIcon(col1x, startY + 11, ICONS.hash, {
1074
+ fill: ctx.colors.secondary,
1075
+ size: 11
1076
+ }));
1077
+ parts.push(svgText(col1x + 14, startY + 22, "TOTAL TOKENS", {
1078
+ fill: ctx.colors.secondary,
1079
+ size: 11
1080
+ }));
1081
+ parts.push(svgText(col1x, startY + 52, formatNumber(data.totals.tokens), {
1082
+ fill: ctx.colors.primary,
1083
+ size: 28,
1084
+ weight: "bold"
1085
+ }));
1086
+ const breakdown = `${formatNumber(data.totals.inputTokens)} in / ${formatNumber(data.totals.outputTokens)} out / ${formatNumber(data.totals.cacheReadTokens)} read / ${formatNumber(data.totals.cacheWriteTokens)} write`;
1087
+ parts.push(svgText(col1x, startY + 66, breakdown, {
1088
+ fill: ctx.colors.secondary,
1089
+ size: 10
1090
+ }));
1091
+ const col2x = ctx.padX + colWidth;
1092
+ parts.push(svgIcon(col2x, startY + 11, ICONS.currencyDollar, {
1093
+ fill: ctx.colors.secondary,
1094
+ size: 11
1095
+ }));
1096
+ parts.push(svgText(col2x + 14, startY + 22, "TOTAL COST", {
1097
+ fill: ctx.colors.secondary,
1098
+ size: 11
1099
+ }));
1100
+ parts.push(svgText(col2x, startY + 52, formatCost$1(data.totals.cost), {
1101
+ fill: ctx.colors.green,
1102
+ size: 28,
1103
+ weight: "bold"
1104
+ }));
1105
+ const col3x = ctx.padX + colWidth * 2;
1106
+ parts.push(svgIcon(col3x, startY + 11, ICONS.terminal, {
1107
+ fill: ctx.colors.secondary,
1108
+ size: 11
1109
+ }));
1110
+ parts.push(svgText(col3x + 14, startY + 22, "SESSIONS", {
1111
+ fill: ctx.colors.secondary,
1112
+ size: 11
1113
+ }));
1114
+ parts.push(svgText(col3x, startY + 52, formatNumber(data.totals.sessions), {
1115
+ fill: ctx.colors.purple,
1116
+ size: 28,
1117
+ weight: "bold"
1118
+ }));
1119
+ const endY = startY + 72;
1120
+ parts.push(separator(ctx, endY));
1121
+ return {
1122
+ svg: parts.join("\n"),
1123
+ height: endY - startY
1124
+ };
1125
+ }
1126
+ //#endregion
1127
+ //#region src/themes/shared/legend.ts
1128
+ function renderLegend(ctx, data, y) {
1129
+ const startY = y + 8;
1130
+ const parts = [];
1131
+ const agents = data.agents.slice(0, 6);
1132
+ const models = topModels(data, 5);
1133
+ const agentRowH = 22;
1134
+ const modelRowH = 22;
1135
+ const agentCount = agents.length;
1136
+ const modelCount = models.length;
1137
+ const squareX = 200;
1138
+ const circleX = ctx.cardWidth - 200;
1139
+ const agentTextX = squareX - 10;
1140
+ const modelTextX = circleX + 14;
1141
+ const totalH = Math.max(agentCount, modelCount) * agentRowH;
1142
+ const agentStartY = startY + (totalH - agentCount * agentRowH) / 2;
1143
+ const modelStartY = startY + (totalH - modelCount * modelRowH) / 2;
1144
+ const modelYMap = /* @__PURE__ */ new Map();
1145
+ models.forEach((m, i) => {
1146
+ modelYMap.set(m.id, modelStartY + i * modelRowH + modelRowH / 2);
1147
+ });
1148
+ let maxPairTokens = 1;
1149
+ for (const agent of agents) for (const [, stats] of Object.entries(agent.models)) if (stats.tokens > maxPairTokens) maxPairTokens = stats.tokens;
1150
+ for (let ai = 0; ai < agents.length; ai++) {
1151
+ const agent = agents[ai];
1152
+ const agentColor = ctx.agentColors[ai % ctx.agentColors.length];
1153
+ const ay = agentStartY + ai * agentRowH + agentRowH / 2;
1154
+ const sx = squareX + 12;
1155
+ for (const [modelId, stats] of Object.entries(agent.models)) {
1156
+ const my = modelYMap.get(modelId);
1157
+ if (my === void 0) continue;
1158
+ const lineW = Math.max(1, stats.tokens / maxPairTokens * 8);
1159
+ const cx = circleX - 6;
1160
+ const midX = (sx + cx) / 2;
1161
+ parts.push(`<path d="M ${sx} ${ay} C ${midX} ${ay}, ${midX} ${my}, ${cx} ${my}" fill="none" stroke="${agentColor}" stroke-width="${lineW.toFixed(1)}" opacity="0.35"/>`);
1162
+ }
1163
+ }
1164
+ agents.forEach((agent, i) => {
1165
+ const cy = agentStartY + i * agentRowH + agentRowH / 2;
1166
+ const color = ctx.agentColors[i % ctx.agentColors.length];
1167
+ parts.push(svgRect(squareX, cy - 5, 10, 10, {
1168
+ fill: color,
1169
+ rx: 2
1170
+ }));
1171
+ parts.push(svgText(agentTextX, cy + 4, escapeXml(agent.displayName), {
1172
+ fill: ctx.colors.primary,
1173
+ size: 11,
1174
+ anchor: "end"
1175
+ }));
1176
+ });
1177
+ models.forEach((model, i) => {
1178
+ const cy = modelStartY + i * modelRowH + modelRowH / 2;
1179
+ const color = ctx.modelColors[i % ctx.modelColors.length];
1180
+ const truncId = model.id.length > 24 ? model.id.slice(0, 24) + "…" : model.id;
1181
+ parts.push(svgCircle(circleX, cy, 5, { fill: color }));
1182
+ parts.push(svgText(modelTextX, cy + 4, truncId, {
1183
+ fill: ctx.colors.primary,
1184
+ size: 11
1185
+ }));
1186
+ });
1187
+ const endY = startY + totalH + 8;
1188
+ parts.push(separator(ctx, endY));
1189
+ return {
1190
+ svg: parts.join("\n"),
1191
+ height: endY - y
1192
+ };
1193
+ }
1194
+ //#endregion
1195
+ //#region src/themes/shared/charts.ts
1196
+ function renderCharts(ctx, data, y) {
1197
+ const startY = y;
1198
+ const parts = [];
1199
+ const agents = data.agents.slice(0, 6);
1200
+ const models = topModels(data, 5);
1201
+ const donutRadius = 60;
1202
+ const donutInner = 38;
1203
+ const donutCx = ctx.padX + 16 + donutRadius;
1204
+ const donutCy = startY + 16 + donutRadius;
1205
+ parts.push(svgIcon(ctx.padX, startY + 5, ICONS.people, {
1206
+ fill: ctx.colors.secondary,
1207
+ size: 11
1208
+ }));
1209
+ parts.push(svgText(ctx.padX + 14, startY + 16, "AGENTS", {
1210
+ fill: ctx.colors.secondary,
1211
+ size: 11
1212
+ }));
1213
+ const slices = agents.map((agent, i) => ({
1214
+ value: agent.totalTokens,
1215
+ color: ctx.agentColors[i % ctx.agentColors.length]
1216
+ }));
1217
+ parts.push(svgDonut(donutCx, donutCy, donutRadius, donutInner, slices, { bgFill: ctx.colors.bg }));
1218
+ parts.push(svgText(donutCx, donutCy - 6, formatNumber(data.totals.tokens), {
1219
+ fill: ctx.colors.primary,
1220
+ size: 14,
1221
+ weight: "bold",
1222
+ anchor: "middle"
1223
+ }));
1224
+ parts.push(svgText(donutCx, donutCy + 10, "tokens", {
1225
+ fill: ctx.colors.secondary,
1226
+ size: 10,
1227
+ anchor: "middle"
1228
+ }));
1229
+ const legendX = donutCx + donutRadius + 16;
1230
+ const legendRowH = 18;
1231
+ const totalTokens = data.totals.tokens || 1;
1232
+ const legendTopY = donutCy - agents.length * legendRowH / 2;
1233
+ agents.forEach((agent, i) => {
1234
+ const ly = legendTopY + i * legendRowH;
1235
+ const color = ctx.agentColors[i % ctx.agentColors.length];
1236
+ const pct = (agent.totalTokens / totalTokens * 100).toFixed(0);
1237
+ parts.push(svgRect(legendX, ly, 8, 8, {
1238
+ fill: color,
1239
+ rx: 2
1240
+ }));
1241
+ parts.push(svgText(legendX + 14, ly + 8, `${pct}% ${formatNumber(agent.totalTokens)} ${formatCost$1(agent.totalCost)}`, {
1242
+ fill: ctx.colors.primary,
1243
+ size: 10
1244
+ }));
1245
+ });
1246
+ const barAreaX = ctx.cardWidth / 2 + 20;
1247
+ const barMaxWidth = ctx.cardWidth - ctx.padX - barAreaX;
1248
+ const maxModelTokens = models.length > 0 ? models[0].tokens : 1;
1249
+ const barH = 18;
1250
+ const barGap = 6;
1251
+ parts.push(svgIcon(barAreaX, startY + 5, ICONS.trophy, {
1252
+ fill: ctx.colors.secondary,
1253
+ size: 11
1254
+ }));
1255
+ parts.push(svgText(barAreaX + 14, startY + 16, "TOP MODELS", {
1256
+ fill: ctx.colors.secondary,
1257
+ size: 11
1258
+ }));
1259
+ models.forEach((model, i) => {
1260
+ const barY = startY + 28 + i * (barH + barGap);
1261
+ const color = ctx.modelColors[i % ctx.modelColors.length];
1262
+ const barW = Math.max(6, Math.sqrt(model.tokens) / Math.sqrt(maxModelTokens) * barMaxWidth);
1263
+ parts.push(svgRect(barAreaX, barY, barW, barH, {
1264
+ fill: color,
1265
+ rx: 4,
1266
+ opacity: .85
1267
+ }));
1268
+ const statsLabel = `${formatNumber(model.tokens)} ${formatCost$1(model.cost)}`;
1269
+ if (barW > statsLabel.length * 6.5 + 12) parts.push(svgText(barAreaX + 8, barY + 13, statsLabel, {
1270
+ fill: ctx.colors.bg,
1271
+ size: 11,
1272
+ weight: "bold"
1273
+ }));
1274
+ else parts.push(svgText(barAreaX + barW + 6, barY + 13, statsLabel, {
1275
+ fill: ctx.colors.primary,
1276
+ size: 11
1277
+ }));
1278
+ });
1279
+ const donutWithLegendH = donutRadius * 2 + 20;
1280
+ const modelsH = models.length * (barH + barGap) + 30;
1281
+ const endY = startY + Math.max(donutWithLegendH, modelsH);
1282
+ parts.push(separator(ctx, endY));
1283
+ return {
1284
+ svg: parts.join("\n"),
1285
+ height: endY - startY
1286
+ };
1287
+ }
1288
+ //#endregion
1289
+ //#region src/themes/shared/heatmap.ts
1290
+ function renderActivityHeatmap(ctx, data, y) {
1291
+ const startY = y;
1292
+ const parts = [];
1293
+ const agents = data.agents.slice(0, 6);
1294
+ const models = topModels(data, 5);
1295
+ const MIN_HEATMAP_DAYS = 60;
1296
+ const periodStart = new Date(data.period.since);
1297
+ periodStart.setHours(0, 0, 0, 0);
1298
+ const periodEnd = new Date(data.period.until);
1299
+ periodEnd.setHours(23, 59, 59, 999);
1300
+ const periodDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / 864e5) + 1;
1301
+ const heatmapStart = new Date(periodStart);
1302
+ if (periodDays < MIN_HEATMAP_DAYS) heatmapStart.setDate(heatmapStart.getDate() - (MIN_HEATMAP_DAYS - periodDays));
1303
+ heatmapStart.setHours(0, 0, 0, 0);
1304
+ const allDays = [];
1305
+ const cursor = new Date(heatmapStart);
1306
+ while (cursor <= periodEnd) {
1307
+ const y2 = cursor.getFullYear();
1308
+ const m2 = String(cursor.getMonth() + 1).padStart(2, "0");
1309
+ const d2 = String(cursor.getDate()).padStart(2, "0");
1310
+ allDays.push(`${y2}-${m2}-${d2}`);
1311
+ cursor.setDate(cursor.getDate() + 1);
1312
+ }
1313
+ if (allDays.length === 0) return {
1314
+ svg: "",
1315
+ height: 0
1316
+ };
1317
+ parts.push(svgIcon(ctx.padX, startY + 5, ICONS.calendar3, {
1318
+ fill: ctx.colors.secondary,
1319
+ size: 11
1320
+ }));
1321
+ parts.push(svgText(ctx.padX + 14, startY + 16, "ACTIVITY", {
1322
+ fill: ctx.colors.secondary,
1323
+ size: 11
1324
+ }));
1325
+ const firstDate = /* @__PURE__ */ new Date(allDays[0] + "T00:00:00");
1326
+ const firstDow = (firstDate.getDay() + 6) % 7;
1327
+ const dayGrid = /* @__PURE__ */ new Map();
1328
+ let maxCol = 0;
1329
+ for (const day of allDays) {
1330
+ const date = /* @__PURE__ */ new Date(day + "T00:00:00");
1331
+ const dow = (date.getDay() + 6) % 7;
1332
+ const daysSinceStart = Math.round((date.getTime() - firstDate.getTime()) / 864e5) + firstDow;
1333
+ const col = Math.floor(daysSinceStart / 7);
1334
+ dayGrid.set(day, {
1335
+ col,
1336
+ row: dow
1337
+ });
1338
+ if (col > maxCol) maxCol = col;
1339
+ }
1340
+ const numWeeks = maxCol + 1;
1341
+ const gridX = ctx.padX;
1342
+ const cellGap = 2;
1343
+ const cellW = Math.min(16, Math.max(6, 12));
1344
+ const colStep = cellW + cellGap;
1345
+ const heatmapW = numWeeks * colStep - cellGap;
1346
+ const gapLR = 8;
1347
+ const isCompact = periodDays > 182;
1348
+ const labelColW = isCompact ? 28 : 28;
1349
+ const ratioColW = 38;
1350
+ const labelX = gridX + heatmapW + gapLR;
1351
+ const labelCenterX = labelX + labelColW / 2;
1352
+ const ratioEndX = labelX + labelColW + ratioColW;
1353
+ const barStartX = ratioEndX + 4;
1354
+ const availableBarW = ctx.cardWidth - ctx.padX - barStartX;
1355
+ const baseBarW = 80;
1356
+ const barChartW = isCompact ? 0 : Math.max(baseBarW, Math.min(baseBarW * 2, availableBarW));
1357
+ const heatLineH = 10;
1358
+ const heatLineGap = 2;
1359
+ const mainCellH = cellW;
1360
+ const mainCellGap = 2;
1361
+ const rowStep = mainCellH + mainCellGap;
1362
+ const agentWeekly = agents.map((agent) => {
1363
+ const weekly = Array(numWeeks).fill(0);
1364
+ for (const [day, tokens] of Object.entries(agent.dailyActivity)) {
1365
+ const pos = dayGrid.get(day);
1366
+ if (pos) weekly[pos.col] += tokens;
1367
+ }
1368
+ return weekly;
1369
+ });
1370
+ const modelWeekly = models.map((model) => {
1371
+ const weekly = Array(numWeeks).fill(0);
1372
+ const activity = data.modelDailyActivity[model.id] ?? {};
1373
+ for (const day of allDays) {
1374
+ const pos = dayGrid.get(day);
1375
+ if (pos) weekly[pos.col] += activity[day] ?? 0;
1376
+ }
1377
+ return weekly;
1378
+ });
1379
+ const dowTotals = Array(7).fill(0);
1380
+ for (const [day] of dayGrid) {
1381
+ const dow = ((/* @__PURE__ */ new Date(day + "T00:00:00")).getDay() + 6) % 7;
1382
+ let total = 0;
1383
+ for (const agent of agents) total += agent.dailyActivity[day] ?? 0;
1384
+ dowTotals[dow] += total;
1385
+ }
1386
+ const maxDowTotal = Math.max(...dowTotals, 1);
1387
+ const totalAllDow = dowTotals.reduce((s, v) => s + v, 0) || 1;
1388
+ let curY = startY + 28;
1389
+ parts.push(svgIcon(labelX, curY + 1, ICONS.people, {
1390
+ fill: ctx.colors.muted,
1391
+ size: 9
1392
+ }));
1393
+ parts.push(svgText(labelX + 12, curY + 9, "Agent", {
1394
+ fill: ctx.colors.muted,
1395
+ size: 9
1396
+ }));
1397
+ for (let w = 0; w < numWeeks; w++) {
1398
+ let bestIdx = 0;
1399
+ let bestVal = 0;
1400
+ agentWeekly.forEach((weekly, i) => {
1401
+ if (weekly[w] > bestVal) {
1402
+ bestVal = weekly[w];
1403
+ bestIdx = i;
1404
+ }
1405
+ });
1406
+ const color = ctx.agentColors[bestIdx % ctx.agentColors.length];
1407
+ const opacity = bestVal === 0 ? .08 : .85;
1408
+ parts.push(svgRect(gridX + w * colStep, curY, cellW, heatLineH, {
1409
+ fill: color,
1410
+ rx: 2,
1411
+ opacity
1412
+ }));
1413
+ }
1414
+ curY += heatLineH + heatLineGap;
1415
+ parts.push(svgIcon(labelX, curY + 1, ICONS.stars, {
1416
+ fill: ctx.colors.muted,
1417
+ size: 9
1418
+ }));
1419
+ parts.push(svgText(labelX + 12, curY + 9, "Model", {
1420
+ fill: ctx.colors.muted,
1421
+ size: 9
1422
+ }));
1423
+ for (let w = 0; w < numWeeks; w++) {
1424
+ let bestIdx = 0;
1425
+ let bestVal = 0;
1426
+ modelWeekly.forEach((weekly, i) => {
1427
+ if (weekly[w] > bestVal) {
1428
+ bestVal = weekly[w];
1429
+ bestIdx = i;
1430
+ }
1431
+ });
1432
+ const color = ctx.modelColors[bestIdx % ctx.modelColors.length];
1433
+ const opacity = bestVal === 0 ? .08 : .85;
1434
+ parts.push(svgRect(gridX + w * colStep, curY, cellW, heatLineH, {
1435
+ fill: color,
1436
+ rx: 2,
1437
+ opacity
1438
+ }));
1439
+ }
1440
+ curY += heatLineH + 6;
1441
+ const dowLabels = [
1442
+ "Mon",
1443
+ "Tue",
1444
+ "Wed",
1445
+ "Thu",
1446
+ "Fri",
1447
+ "Sat",
1448
+ "Sun"
1449
+ ];
1450
+ const heatmapStartY = curY;
1451
+ const dayTotals = /* @__PURE__ */ new Map();
1452
+ for (const day of allDays) {
1453
+ let total = 0;
1454
+ for (const agent of agents) total += agent.dailyActivity[day] ?? 0;
1455
+ dayTotals.set(day, total);
1456
+ }
1457
+ const maxDayTotal = Math.max(...dayTotals.values(), 1);
1458
+ for (let r = 0; r < 7; r++) {
1459
+ const ry = heatmapStartY + r * rowStep;
1460
+ const ratio = (dowTotals[r] / totalAllDow * 100).toFixed(1);
1461
+ parts.push(svgText(labelCenterX, ry + mainCellH / 2 + 3, dowLabels[r], {
1462
+ fill: ctx.colors.muted,
1463
+ size: 9,
1464
+ anchor: "middle"
1465
+ }));
1466
+ parts.push(svgText(ratioEndX, ry + mainCellH / 2 + 3, `${ratio}%`, {
1467
+ fill: ctx.colors.muted,
1468
+ size: 9,
1469
+ anchor: "end"
1470
+ }));
1471
+ if (!isCompact) {
1472
+ const barW = dowTotals[r] / maxDowTotal * barChartW;
1473
+ if (barW > 0) parts.push(svgRect(barStartX, ry + 2, barW, mainCellH - 4, {
1474
+ fill: ctx.colors.green,
1475
+ rx: 3,
1476
+ opacity: .7
1477
+ }));
1478
+ }
1479
+ }
1480
+ for (const [day, { col, row }] of dayGrid) {
1481
+ const cx = gridX + col * colStep;
1482
+ const cy = heatmapStartY + row * rowStep;
1483
+ const totalTokens = dayTotals.get(day) ?? 0;
1484
+ if (totalTokens === 0) {
1485
+ parts.push(svgRect(cx, cy, cellW, mainCellH, {
1486
+ fill: ctx.colors.separator,
1487
+ rx: 3,
1488
+ opacity: .2
1489
+ }));
1490
+ continue;
1491
+ }
1492
+ const strength = .15 + totalTokens / maxDayTotal * .85;
1493
+ parts.push(svgRect(cx, cy, cellW, mainCellH, {
1494
+ fill: ctx.colors.green,
1495
+ rx: 3,
1496
+ opacity: strength
1497
+ }));
1498
+ }
1499
+ curY = heatmapStartY + (7 * rowStep - mainCellGap) + 10;
1500
+ parts.push(svgText(gridX, curY, "less", {
1501
+ fill: ctx.colors.muted,
1502
+ size: 9
1503
+ }));
1504
+ const scaleX = gridX + 28;
1505
+ const scaleOpacities = [
1506
+ .15,
1507
+ .35,
1508
+ .55,
1509
+ .75,
1510
+ 1
1511
+ ];
1512
+ for (let si = 0; si < scaleOpacities.length; si++) parts.push(svgRect(scaleX + si * 13, curY - 8, 10, 10, {
1513
+ fill: ctx.colors.green,
1514
+ rx: 2,
1515
+ opacity: scaleOpacities[si]
1516
+ }));
1517
+ parts.push(svgText(scaleX + scaleOpacities.length * 13 + 4, curY, "more", {
1518
+ fill: ctx.colors.muted,
1519
+ size: 9
1520
+ }));
1521
+ curY += 10;
1522
+ parts.push(separator(ctx, curY));
1523
+ return {
1524
+ svg: parts.join("\n"),
1525
+ height: curY - startY
1526
+ };
1527
+ }
1528
+ //#endregion
1529
+ //#region src/themes/shared/inventory.ts
1530
+ function renderInventory(ctx, data, y) {
1531
+ const inv = data.inventory;
1532
+ const hasPlugins = inv.plugins.length > 0;
1533
+ const hasMcp = inv.mcpServers.length > 0;
1534
+ const hasSkills = inv.skills.length > 0;
1535
+ if (!hasPlugins && !hasMcp && !hasSkills) return {
1536
+ svg: "",
1537
+ height: 0
1538
+ };
1539
+ const agentColorMap = /* @__PURE__ */ new Map();
1540
+ for (let i = 0; i < data.agents.length; i++) agentColorMap.set(data.agents[i].agent, ctx.agentColors[i % ctx.agentColors.length]);
1541
+ const toBadges = (sources) => sources.map((s) => agentColorMap.get(s) ?? ctx.colors.muted);
1542
+ const parts = [];
1543
+ const startY = y;
1544
+ parts.push(svgIcon(ctx.padX, startY + 5, ICONS.box, {
1545
+ fill: ctx.colors.secondary,
1546
+ size: 11
1547
+ }));
1548
+ parts.push(svgText(ctx.padX + 14, startY + 16, "INVENTORY", {
1549
+ fill: ctx.colors.secondary,
1550
+ size: 11
1551
+ }));
1552
+ const pillGap = 6;
1553
+ const maxX = ctx.cardWidth - ctx.padX;
1554
+ const indent = 20;
1555
+ const rowH = 26;
1556
+ let curY = startY + 28;
1557
+ for (const plugin of inv.plugins) {
1558
+ const label = plugin.version ? `${plugin.name} v${plugin.version}` : plugin.name;
1559
+ parts.push(svgIcon(ctx.padX, curY + 3, ICONS.puzzle, {
1560
+ fill: ctx.colors.blue,
1561
+ size: 12
1562
+ }));
1563
+ const pill = svgPill(ctx.padX + 16, curY, label, {
1564
+ fill: "#1f6feb22",
1565
+ textFill: ctx.colors.blue,
1566
+ badges: toBadges(plugin.sources)
1567
+ });
1568
+ parts.push(pill.svg);
1569
+ curY += rowH;
1570
+ const renderPillRow = (label, labelW, items, prefix = "") => {
1571
+ parts.push(svgText(ctx.padX + indent, curY + 12, label, {
1572
+ fill: ctx.colors.muted,
1573
+ size: 9
1574
+ }));
1575
+ let px = ctx.padX + indent + labelW;
1576
+ for (const item of items) {
1577
+ const text = prefix + item;
1578
+ const sp = svgPill(px, curY, text, {
1579
+ fill: ctx.colors.separator,
1580
+ textFill: ctx.colors.secondary
1581
+ });
1582
+ if (px + sp.width > maxX && px > ctx.padX + indent + labelW) {
1583
+ curY += rowH;
1584
+ px = ctx.padX + indent + labelW;
1585
+ const sp2 = svgPill(px, curY, text, {
1586
+ fill: ctx.colors.separator,
1587
+ textFill: ctx.colors.secondary
1588
+ });
1589
+ parts.push(sp2.svg);
1590
+ px += sp2.width + pillGap;
1591
+ } else {
1592
+ parts.push(sp.svg);
1593
+ px += sp.width + pillGap;
1594
+ }
1595
+ }
1596
+ curY += rowH;
1597
+ };
1598
+ if (plugin.skills.length > 0) renderPillRow("skills", 36, plugin.skills);
1599
+ if (plugin.agents.length > 0) renderPillRow("agents", 40, plugin.agents);
1600
+ if (plugin.commands.length > 0) renderPillRow("cmds", 32, plugin.commands, "/");
1601
+ }
1602
+ const renderItemSection = (iconPath, iconColor, title, items) => {
1603
+ parts.push(svgIcon(ctx.padX, curY + 3, iconPath, {
1604
+ fill: iconColor,
1605
+ size: 12
1606
+ }));
1607
+ parts.push(svgText(ctx.padX + 16, curY + 12, title, {
1608
+ fill: ctx.colors.secondary,
1609
+ size: 10
1610
+ }));
1611
+ curY += 18;
1612
+ let px = ctx.padX + indent;
1613
+ for (const item of items) {
1614
+ const badges = toBadges(item.sources);
1615
+ const sp = svgPill(px, curY, item.name, {
1616
+ fill: ctx.colors.separator,
1617
+ textFill: ctx.colors.secondary,
1618
+ badges
1619
+ });
1620
+ if (px + sp.width > maxX && px > ctx.padX + indent) {
1621
+ curY += rowH;
1622
+ px = ctx.padX + indent;
1623
+ const sp2 = svgPill(px, curY, item.name, {
1624
+ fill: ctx.colors.separator,
1625
+ textFill: ctx.colors.secondary,
1626
+ badges
1627
+ });
1628
+ parts.push(sp2.svg);
1629
+ px += sp2.width + pillGap;
1630
+ } else {
1631
+ parts.push(sp.svg);
1632
+ px += sp.width + pillGap;
1633
+ }
1634
+ }
1635
+ curY += rowH;
1636
+ };
1637
+ if (hasMcp) renderItemSection(ICONS.tools, ctx.colors.green, "MCP Servers", inv.mcpServers);
1638
+ if (hasSkills) renderItemSection(ICONS.hexagon, ctx.colors.purple, "Skills", inv.skills);
1639
+ return {
1640
+ svg: parts.join("\n"),
1641
+ height: curY - startY
1642
+ };
1643
+ }
1644
+ //#endregion
1645
+ //#region src/themes/shared/footer.ts
1646
+ function renderFooter(ctx, _data, y) {
1647
+ const startY = y + 8;
1648
+ const parts = [];
1649
+ const label = "Generated by @eat-pray-ai/wingman";
1650
+ const labelW = 33 * 6.5;
1651
+ const labelX = ctx.cardWidth / 2;
1652
+ const iconX = labelX - labelW / 2 - 16;
1653
+ parts.push(svgIcon(iconX, startY + 5, ICONS.lightningCharge, {
1654
+ fill: ctx.colors.muted,
1655
+ size: 12
1656
+ }));
1657
+ parts.push(svgText(labelX, startY + 16, label, {
1658
+ fill: ctx.colors.muted,
1659
+ size: 11,
1660
+ anchor: "middle"
1661
+ }));
1662
+ return {
1663
+ svg: parts.join("\n"),
1664
+ height: startY + 30 - y
1665
+ };
1666
+ }
1667
+ function renderEmpty(ctx, _data) {
1668
+ const height = 120;
1669
+ const parts = [];
1670
+ parts.push(svgRect(0, 0, ctx.cardWidth, height, {
1671
+ fill: ctx.colors.bg,
1672
+ rx: 12,
1673
+ stroke: ctx.colors.border
1674
+ }));
1675
+ parts.push(svgText(ctx.cardWidth / 2, 50, "Wingman Stats", {
1676
+ fill: ctx.colors.blue,
1677
+ size: 16,
1678
+ weight: "bold",
1679
+ anchor: "middle"
1680
+ }));
1681
+ parts.push(svgText(ctx.cardWidth / 2, 80, "No activity in this period", {
1682
+ fill: ctx.colors.secondary,
1683
+ size: 14,
1684
+ anchor: "middle"
1685
+ }));
1686
+ return [
1687
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${ctx.cardWidth}" height="${height}" viewBox="0 0 ${ctx.cardWidth} ${height}">`,
1688
+ parts.join("\n"),
1689
+ `</svg>`
1690
+ ].join("\n");
1691
+ }
1692
+ //#endregion
1693
+ //#region src/themes/shared/sections.ts
1694
+ const ALL_SECTIONS = [
1695
+ {
1696
+ name: "header",
1697
+ render: renderHeader
1698
+ },
1699
+ {
1700
+ name: "stats",
1701
+ render: renderTopStats
1702
+ },
1703
+ {
1704
+ name: "legend",
1705
+ render: renderLegend
1706
+ },
1707
+ {
1708
+ name: "charts",
1709
+ render: renderCharts
1710
+ },
1711
+ {
1712
+ name: "heatmap",
1713
+ render: renderActivityHeatmap
1714
+ },
1715
+ {
1716
+ name: "inventory",
1717
+ render: renderInventory
1718
+ },
1719
+ {
1720
+ name: "footer",
1721
+ render: renderFooter
1722
+ }
1723
+ ];
1724
+ const SECTION_NAMES = ALL_SECTIONS.map((s) => s.name);
1725
+ //#endregion
1726
+ //#region src/themes/github-dark/palette.ts
1727
+ const palette$2 = {
1728
+ colors: {
1729
+ bg: "#0d1117",
1730
+ border: "#30363d",
1731
+ separator: "#21262d",
1732
+ primary: "#e6edf3",
1733
+ secondary: "#8b949e",
1734
+ muted: "#484f58",
1735
+ blue: "#58a6ff",
1736
+ green: "#3fb950",
1737
+ purple: "#d2a8ff",
1738
+ orange: "#f0883e",
1739
+ red: "#f85149"
1740
+ },
1741
+ agentColors: [
1742
+ "#1f6feb",
1743
+ "#3fb950",
1744
+ "#818cf8",
1745
+ "#2dd4bf",
1746
+ "#22d3ee",
1747
+ "#58a6ff"
1748
+ ],
1749
+ modelColors: [
1750
+ "#f0883e",
1751
+ "#ff7b72",
1752
+ "#f472b6",
1753
+ "#ffa657",
1754
+ "#da3633"
1755
+ ]
1756
+ };
1757
+ //#endregion
1758
+ //#region src/themes/github-dark/index.ts
1759
+ function wrapSvg$2(ctx, body, height) {
1760
+ return [
1761
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${ctx.cardWidth}" height="${height}" viewBox="0 0 ${ctx.cardWidth} ${height}">`,
1762
+ svgRect(0, 0, ctx.cardWidth, height, {
1763
+ fill: ctx.colors.bg,
1764
+ rx: 12,
1765
+ stroke: ctx.colors.border
1766
+ }),
1767
+ body,
1768
+ `</svg>`
1769
+ ].join("\n");
1770
+ }
1771
+ function render$2(data, opts) {
1772
+ if (data.agents.length === 0 || data.totals.tokens === 0) return renderEmpty(createContext(660, palette$2), data);
1773
+ const { numWeeks, numDays } = computePeriodSize(data);
1774
+ const ctx = createContext(computeCardWidth(numWeeks, numDays), palette$2);
1775
+ const sections = opts?.sections ? ALL_SECTIONS.filter((s) => opts.sections.includes(s.name)) : ALL_SECTIONS;
1776
+ let y = 0;
1777
+ const parts = [];
1778
+ for (const section of sections) {
1779
+ const result = section.render(ctx, data, y);
1780
+ parts.push(result.svg);
1781
+ y += result.height;
1782
+ }
1783
+ return wrapSvg$2(ctx, parts.join("\n"), y);
1784
+ }
1785
+ var github_dark_default = {
1786
+ name: "github-dark",
1787
+ render: render$2
1788
+ };
1789
+ //#endregion
1790
+ //#region src/themes/github-light/palette.ts
1791
+ const palette$1 = {
1792
+ colors: {
1793
+ bg: "#ffffff",
1794
+ border: "#d0d7de",
1795
+ separator: "#d8dee4",
1796
+ primary: "#1f2328",
1797
+ secondary: "#656d76",
1798
+ muted: "#8b949e",
1799
+ blue: "#0969da",
1800
+ green: "#1a7f37",
1801
+ purple: "#8250df",
1802
+ orange: "#bf8700",
1803
+ red: "#cf222e"
1804
+ },
1805
+ agentColors: [
1806
+ "#0969da",
1807
+ "#1a7f37",
1808
+ "#6639ba",
1809
+ "#0e8a6e",
1810
+ "#0c7a92",
1811
+ "#2563eb"
1812
+ ],
1813
+ modelColors: [
1814
+ "#bf8700",
1815
+ "#cf222e",
1816
+ "#bf3989",
1817
+ "#bc4c00",
1818
+ "#a40e26"
1819
+ ]
1820
+ };
1821
+ //#endregion
1822
+ //#region src/themes/github-light/index.ts
1823
+ function wrapSvg$1(ctx, body, height) {
1824
+ return [
1825
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${ctx.cardWidth}" height="${height}" viewBox="0 0 ${ctx.cardWidth} ${height}">`,
1826
+ svgRect(0, 0, ctx.cardWidth, height, {
1827
+ fill: ctx.colors.bg,
1828
+ rx: 12,
1829
+ stroke: ctx.colors.border
1830
+ }),
1831
+ body,
1832
+ `</svg>`
1833
+ ].join("\n");
1834
+ }
1835
+ function render$1(data, opts) {
1836
+ if (data.agents.length === 0 || data.totals.tokens === 0) return renderEmpty(createContext(660, palette$1), data);
1837
+ const { numWeeks, numDays } = computePeriodSize(data);
1838
+ const ctx = createContext(computeCardWidth(numWeeks, numDays), palette$1);
1839
+ const sections = opts?.sections ? ALL_SECTIONS.filter((s) => opts.sections.includes(s.name)) : ALL_SECTIONS;
1840
+ let y = 0;
1841
+ const parts = [];
1842
+ for (const section of sections) {
1843
+ const result = section.render(ctx, data, y);
1844
+ parts.push(result.svg);
1845
+ y += result.height;
1846
+ }
1847
+ return wrapSvg$1(ctx, parts.join("\n"), y);
1848
+ }
1849
+ var github_light_default = {
1850
+ name: "github-light",
1851
+ render: render$1
1852
+ };
1853
+ //#endregion
1854
+ //#region src/themes/onedark/palette.ts
1855
+ const palette = {
1856
+ colors: {
1857
+ bg: "#282c34",
1858
+ border: "#3e4452",
1859
+ separator: "#2c323c",
1860
+ primary: "#abb2bf",
1861
+ secondary: "#5c6370",
1862
+ muted: "#4b5263",
1863
+ blue: "#61afef",
1864
+ green: "#98c379",
1865
+ purple: "#c678dd",
1866
+ orange: "#d19a66",
1867
+ red: "#e06c75"
1868
+ },
1869
+ agentColors: [
1870
+ "#61afef",
1871
+ "#98c379",
1872
+ "#c678dd",
1873
+ "#56b6c2",
1874
+ "#e5c07b",
1875
+ "#e06c75"
1876
+ ],
1877
+ modelColors: [
1878
+ "#d19a66",
1879
+ "#be5046",
1880
+ "#e06c75",
1881
+ "#e5c07b",
1882
+ "#56b6c2"
1883
+ ]
1884
+ };
1885
+ //#endregion
1886
+ //#region src/themes/onedark/index.ts
1887
+ function wrapSvg(ctx, body, height) {
1888
+ return [
1889
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${ctx.cardWidth}" height="${height}" viewBox="0 0 ${ctx.cardWidth} ${height}">`,
1890
+ svgRect(0, 0, ctx.cardWidth, height, {
1891
+ fill: ctx.colors.bg,
1892
+ rx: 12,
1893
+ stroke: ctx.colors.border
1894
+ }),
1895
+ body,
1896
+ `</svg>`
1897
+ ].join("\n");
1898
+ }
1899
+ function render(data, opts) {
1900
+ if (data.agents.length === 0 || data.totals.tokens === 0) return renderEmpty(createContext(660, palette), data);
1901
+ const { numWeeks, numDays } = computePeriodSize(data);
1902
+ const ctx = createContext(computeCardWidth(numWeeks, numDays), palette);
1903
+ const sections = opts?.sections ? ALL_SECTIONS.filter((s) => opts.sections.includes(s.name)) : ALL_SECTIONS;
1904
+ let y = 0;
1905
+ const parts = [];
1906
+ for (const section of sections) {
1907
+ const result = section.render(ctx, data, y);
1908
+ parts.push(result.svg);
1909
+ y += result.height;
1910
+ }
1911
+ return wrapSvg(ctx, parts.join("\n"), y);
1912
+ }
1913
+ var onedark_default = {
1914
+ name: "onedark",
1915
+ render
1916
+ };
1917
+ //#endregion
1918
+ //#region src/themes/registry.ts
1919
+ const themes = new Map([
1920
+ [github_dark_default.name, github_dark_default],
1921
+ [github_light_default.name, github_light_default],
1922
+ [onedark_default.name, onedark_default]
1923
+ ]);
1924
+ function getTheme(name) {
1925
+ return themes.get(name);
1926
+ }
1927
+ function getAvailableThemes() {
1928
+ return [...themes.keys()];
1929
+ }
1930
+ //#endregion
1931
+ //#region src/resume/renderer.ts
1932
+ /**
1933
+ * Quote a YAML string only when necessary.
1934
+ * Uses single quotes when quoting is needed (just double any embedded ').
1935
+ * Matches rendercv example style: bare strings for simple values.
1936
+ */
1937
+ function yamlValue(s) {
1938
+ if (s.length === 0) return "\"\"";
1939
+ if (/^(true|false|null|yes|no|on|off)$/i.test(s)) return `'${s}'`;
1940
+ if (/^[\s&?|>'"{}\[\]]/.test(s) || /^\*[^*]/.test(s) || /^-\s/.test(s) || s.includes(": ") || s.includes(" #") || s.includes(", ") || s.includes("\n")) return `'${s.replace(/'/g, "''")}'`;
1941
+ return s;
1942
+ }
1943
+ function formatTokens(n) {
1944
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
1945
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
1946
+ return String(n);
1947
+ }
1948
+ function formatCost(n) {
1949
+ return `$${n.toFixed(2)}`;
1950
+ }
1951
+ function formatDateYMD(d) {
1952
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1953
+ }
1954
+ function agentDateRange(agent, fallbackSince, fallbackUntil) {
1955
+ const days = Object.keys(agent.dailyActivity).sort();
1956
+ if (days.length === 0) return {
1957
+ start: formatDateYMD(fallbackSince),
1958
+ end: formatDateYMD(fallbackUntil)
1959
+ };
1960
+ return {
1961
+ start: days[0],
1962
+ end: days[days.length - 1]
1963
+ };
1964
+ }
1965
+ function positionLabel(index, pct) {
1966
+ if (index === 0) return `Primary Agent (${pct}%)`;
1967
+ if (index === 1) return `Secondary Agent (${pct}%)`;
1968
+ return `Supporting Agent (${pct}%)`;
1969
+ }
1970
+ const MIN_MODEL_TOKENS = 3e6;
1971
+ const MIN_MODEL_COST = 1;
1972
+ function isSignificantModel(tokens, cost) {
1973
+ return tokens > MIN_MODEL_TOKENS && cost > MIN_MODEL_COST;
1974
+ }
1975
+ /** Collect all unique models across all agents, sorted by total tokens desc */
1976
+ function collectModels(data) {
1977
+ const map = /* @__PURE__ */ new Map();
1978
+ for (const agent of data.agents) for (const [modelId, stats] of Object.entries(agent.models)) {
1979
+ const existing = map.get(modelId);
1980
+ if (existing) {
1981
+ existing.tokens += stats.tokens;
1982
+ existing.cost += stats.cost;
1983
+ } else map.set(modelId, {
1984
+ tokens: stats.tokens,
1985
+ cost: stats.cost
1986
+ });
1987
+ }
1988
+ return [...map.entries()].map(([modelId, s]) => ({
1989
+ modelId,
1990
+ ...s
1991
+ })).filter((m) => isSignificantModel(m.tokens, m.cost)).sort((a, b) => b.tokens - a.tokens);
1992
+ }
1993
+ function renderSummary(data, indent) {
1994
+ const lines = [];
1995
+ const agentCount = data.agents.length;
1996
+ const totalTokens = formatTokens(data.totals.tokens);
1997
+ const sessions = data.totals.sessions;
1998
+ const since = formatDateYMD(data.period.since);
1999
+ const until = formatDateYMD(data.period.until);
2000
+ const cost = formatCost(data.totals.cost);
2001
+ lines.push(`${indent}summary:`);
2002
+ lines.push(`${indent} - ${yamlValue(`AI agent team of **${agentCount} agents**, processing **${totalTokens} tokens** across **${sessions} sessions** from ${since} to ${until}. Total cost: **${cost}**.`)}`);
2003
+ return lines;
2004
+ }
2005
+ function renderExperience(data, indent) {
2006
+ const lines = [];
2007
+ lines.push(`${indent}experience:`);
2008
+ const totalTokens = data.totals.tokens;
2009
+ let rank = 0;
2010
+ for (let i = 0; i < data.agents.length; i++) {
2011
+ const agent = data.agents[i];
2012
+ const pctNum = totalTokens > 0 ? agent.totalTokens / totalTokens * 100 : 0;
2013
+ if (pctNum <= 1) continue;
2014
+ const pct = pctNum.toFixed(1);
2015
+ const position = positionLabel(rank++, pct);
2016
+ const { start, end } = agentDateRange(agent, data.period.since, data.period.until);
2017
+ lines.push(`${indent} - company: ${yamlValue(agent.displayName)}`);
2018
+ lines.push(`${indent} position: ${yamlValue(position)}`);
2019
+ lines.push(`${indent} start_date: ${yamlValue(start)}`);
2020
+ lines.push(`${indent} end_date: ${yamlValue(end)}`);
2021
+ lines.push(`${indent} highlights:`);
2022
+ lines.push(`${indent} - ${yamlValue(`Processed **${formatTokens(agent.totalTokens)} tokens** across **${agent.sessionCount} sessions**, **${formatCost(agent.totalCost)}** total`)}`);
2023
+ const modelEntries = Object.entries(agent.models).filter(([, s]) => isSignificantModel(s.tokens, s.cost)).sort(([, a], [, b]) => b.tokens - a.tokens);
2024
+ if (modelEntries.length > 0) {
2025
+ const modelParts = modelEntries.map(([id, s]) => `**${id}** (${formatTokens(s.tokens)})`);
2026
+ lines.push(`${indent} - ${yamlValue(`Models: ${modelParts.join(", ")}`)}`);
2027
+ }
2028
+ }
2029
+ return lines;
2030
+ }
2031
+ function renderEducation(data, modelInfo, indent) {
2032
+ const lines = [];
2033
+ const models = collectModels(data);
2034
+ if (models.length === 0) return [];
2035
+ const groups = /* @__PURE__ */ new Map();
2036
+ for (const m of models) {
2037
+ const lab = modelInfo.get(m.modelId)?.lab ?? modelLab(m.modelId);
2038
+ const group = groups.get(lab) ?? [];
2039
+ group.push(m);
2040
+ groups.set(lab, group);
2041
+ }
2042
+ lines.push(`${indent}education:`);
2043
+ for (const [company, groupModels] of groups) {
2044
+ const allInputs = /* @__PURE__ */ new Set();
2045
+ const allOutputs = /* @__PURE__ */ new Set();
2046
+ let earliestKnowledge;
2047
+ let latestRelease;
2048
+ for (const m of groupModels) {
2049
+ const info = modelInfo.get(m.modelId);
2050
+ if (info?.modalities) {
2051
+ for (const i of info.modalities.input) allInputs.add(i);
2052
+ for (const o of info.modalities.output) allOutputs.add(o);
2053
+ }
2054
+ if (info?.knowledge && (!earliestKnowledge || info.knowledge < earliestKnowledge)) earliestKnowledge = info.knowledge;
2055
+ if (info?.releaseDate && (!latestRelease || info.releaseDate > latestRelease)) latestRelease = info.releaseDate;
2056
+ }
2057
+ const area = allInputs.size > 0 ? [...allInputs].join(", ") : "Language Models";
2058
+ const premium = [...groupModels].sort((a, b) => (b.tokens > 0 ? b.cost / b.tokens : 0) - (a.tokens > 0 ? a.cost / a.tokens : 0))[0];
2059
+ const primaryInfo = modelInfo.get(premium.modelId);
2060
+ const degree = primaryInfo ? primaryInfo.name : premium.modelId;
2061
+ lines.push(`${indent} - institution: ${yamlValue(company)}`);
2062
+ lines.push(`${indent} area: ${yamlValue(area)}`);
2063
+ lines.push(`${indent} degree: ${yamlValue(degree)}`);
2064
+ if (earliestKnowledge) lines.push(`${indent} start_date: ${yamlValue(earliestKnowledge)}`);
2065
+ if (latestRelease) lines.push(`${indent} end_date: ${yamlValue(latestRelease)}`);
2066
+ else lines.push(`${indent} end_date: present`);
2067
+ lines.push(`${indent} highlights:`);
2068
+ for (const m of groupModels) {
2069
+ const info = modelInfo.get(m.modelId);
2070
+ const name = info ? info.name : m.modelId;
2071
+ lines.push(`${indent} - ${yamlValue(`**${name}**: ${formatTokens(m.tokens)} tokens, ${formatCost(m.cost)}`)}`);
2072
+ }
2073
+ }
2074
+ return lines;
2075
+ }
2076
+ function renderTechnologies(data, indent) {
2077
+ const lines = [];
2078
+ const categories = [];
2079
+ if (data.inventory.plugins.length > 0) {
2080
+ const items = data.inventory.plugins.map((p) => p.version ? `${p.name} v${p.version}` : p.name);
2081
+ categories.push({
2082
+ label: "Plugins",
2083
+ items
2084
+ });
2085
+ }
2086
+ if (data.inventory.mcpServers.length > 0) categories.push({
2087
+ label: "MCP Servers",
2088
+ items: data.inventory.mcpServers.map((s) => s.name)
2089
+ });
2090
+ if (data.inventory.skills.length > 0) categories.push({
2091
+ label: "Skills",
2092
+ items: data.inventory.skills.map((s) => s.name)
2093
+ });
2094
+ if (categories.length === 0) return [];
2095
+ lines.push(`${indent}technologies:`);
2096
+ for (const cat of categories) {
2097
+ lines.push(`${indent} - label: ${yamlValue(cat.label)}`);
2098
+ lines.push(`${indent} details: ${yamlValue(cat.items.join(", "))}`);
2099
+ }
2100
+ return lines;
2101
+ }
2102
+ function generateResumeYaml(data, modelInfo, opts) {
2103
+ const { name, headline } = opts;
2104
+ const indent = " ";
2105
+ const lines = [];
2106
+ lines.push("cv:");
2107
+ lines.push(` name: ${yamlValue(name)}`);
2108
+ lines.push(` headline: ${yamlValue(headline)}`);
2109
+ lines.push(" sections:");
2110
+ lines.push(...renderSummary(data, indent));
2111
+ lines.push(...renderExperience(data, indent));
2112
+ lines.push(...renderEducation(data, modelInfo, indent));
2113
+ lines.push(...renderTechnologies(data, indent));
2114
+ return lines.join("\n") + "\n";
2115
+ }
2116
+ //#endregion
2117
+ //#region src/cli.ts
2118
+ const parseIntArg = (v) => parseInt(v, 10);
2119
+ const program = new Command();
2120
+ program.name("wingman").description("Showcase your AI pair usage — SVG cards, resumes, and more").version("0.1.0");
2121
+ program.command("card").description("Generate an SVG stats card from local AI agent data").option("-o, --output <path>", "output file path", "wingman.svg").option("-t, --theme <name>", `theme name (${getAvailableThemes().join(", ")})`, "github-dark").option("--agents <names>", `comma-separated agent filter (${getAllAdapters().map((a) => a.name).join(", ")})`).option("--since <date>", "start date (YYYY-MM-DD)").option("--until <date>", "end date (YYYY-MM-DD)").option("--days <n>", "last N days", parseIntArg, 90).option("--sections <names>", `comma-separated sections to include (${SECTION_NAMES.join(", ")})`).action(async (opts) => {
2122
+ const until = opts.until ? /* @__PURE__ */ new Date(opts.until + "T23:59:59.999") : /* @__PURE__ */ new Date();
2123
+ let since;
2124
+ if (opts.since) since = /* @__PURE__ */ new Date(opts.since + "T00:00:00");
2125
+ else since = /* @__PURE__ */ new Date(until.getTime() - opts.days * 24 * 60 * 60 * 1e3);
2126
+ const theme = getTheme(opts.theme);
2127
+ if (!theme) {
2128
+ console.error(`Unknown theme "${opts.theme}". Available: ${getAvailableThemes().join(", ")}`);
2129
+ process.exit(1);
2130
+ }
2131
+ const sectionsFilter = opts.sections ? opts.sections.split(",").map((s) => s.trim()) : void 0;
2132
+ if (sectionsFilter) {
2133
+ const invalid = sectionsFilter.filter((s) => !SECTION_NAMES.includes(s));
2134
+ if (invalid.length > 0) {
2135
+ console.error(`Unknown section(s): ${invalid.join(", ")}. Available: ${SECTION_NAMES.join(", ")}`);
2136
+ process.exit(1);
2137
+ }
2138
+ }
2139
+ const agentFilter = opts.agents ? new Set(opts.agents.split(",").map((s) => s.trim())) : null;
2140
+ let adapters = getAllAdapters();
2141
+ if (agentFilter) adapters = adapters.filter((a) => agentFilter.has(a.name));
2142
+ console.log("šŸ” Detecting agents...");
2143
+ const detected = [];
2144
+ for (const adapter of adapters) if (await adapter.detect()) {
2145
+ detected.push(adapter);
2146
+ console.log(` āœ“ ${adapter.displayName}`);
2147
+ }
2148
+ if (detected.length === 0) {
2149
+ console.error("No AI agents detected on this machine.");
2150
+ process.exit(1);
2151
+ }
2152
+ console.log("\nšŸ“Š Collecting usage data...");
2153
+ const allRecords = [];
2154
+ const configsMap = /* @__PURE__ */ new Map();
2155
+ const pricingOverrides = [];
2156
+ for (const adapter of detected) try {
2157
+ const records = await adapter.collect(since, until);
2158
+ allRecords.push(...records);
2159
+ const config = await adapter.config();
2160
+ configsMap.set(adapter.name, {
2161
+ displayName: adapter.displayName,
2162
+ config
2163
+ });
2164
+ console.log(` ${adapter.displayName}: ${records.length} records`);
2165
+ } catch (err) {
2166
+ console.warn(` ⚠ ${adapter.displayName}: ${err.message}`);
2167
+ }
2168
+ console.log("\nšŸ’° Loading pricing data...");
2169
+ const pricing = await createPricingEngine(pricingOverrides);
2170
+ console.log("šŸŽØ Rendering SVG...");
2171
+ const data = aggregate(allRecords, configsMap, pricing, since, until);
2172
+ const svg = theme.render(data, { sections: sectionsFilter });
2173
+ const outputPath = resolve(opts.output);
2174
+ writeFileSync(outputPath, svg, "utf-8");
2175
+ console.log(`\nāœ… Saved to ${outputPath}`);
2176
+ });
2177
+ program.command("resume").description("Generate a rendercv-compatible YAML resume from AI agent usage stats").option("--name <name>", "resume name", "Wingman").option("--headline <text>", "resume headline", "AI pair for everything").option("-o, --output <path>", "output file path", "resume.yaml").option("--agents <names>", `comma-separated agent filter (${getAllAdapters().map((a) => a.name).join(", ")})`).option("--since <date>", "start date (YYYY-MM-DD)").option("--until <date>", "end date (YYYY-MM-DD)").option("--days <n>", "last N days", parseIntArg, 180).action(async (opts) => {
2178
+ const until = opts.until ? /* @__PURE__ */ new Date(opts.until + "T23:59:59.999") : /* @__PURE__ */ new Date();
2179
+ let since;
2180
+ if (opts.since) since = /* @__PURE__ */ new Date(opts.since + "T00:00:00");
2181
+ else since = /* @__PURE__ */ new Date(until.getTime() - opts.days * 24 * 60 * 60 * 1e3);
2182
+ const agentFilter = opts.agents ? new Set(opts.agents.split(",").map((s) => s.trim())) : null;
2183
+ let adapters = getAllAdapters();
2184
+ if (agentFilter) adapters = adapters.filter((a) => agentFilter.has(a.name));
2185
+ console.log("šŸ” Detecting agents...");
2186
+ const detected = [];
2187
+ for (const adapter of adapters) if (await adapter.detect()) {
2188
+ detected.push(adapter);
2189
+ console.log(` āœ“ ${adapter.displayName}`);
2190
+ }
2191
+ if (detected.length === 0) {
2192
+ console.error("No AI agents detected on this machine.");
2193
+ process.exit(1);
2194
+ }
2195
+ console.log("\nšŸ“Š Collecting usage data...");
2196
+ const allRecords = [];
2197
+ const configsMap = /* @__PURE__ */ new Map();
2198
+ const pricingOverrides = [];
2199
+ for (const adapter of detected) try {
2200
+ const records = await adapter.collect(since, until);
2201
+ allRecords.push(...records);
2202
+ const config = await adapter.config();
2203
+ configsMap.set(adapter.name, {
2204
+ displayName: adapter.displayName,
2205
+ config
2206
+ });
2207
+ console.log(` ${adapter.displayName}: ${records.length} records`);
2208
+ } catch (err) {
2209
+ console.warn(` ⚠ ${adapter.displayName}: ${err.message}`);
2210
+ }
2211
+ console.log("\nšŸ’° Loading pricing data...");
2212
+ const pricing = await createPricingEngine(pricingOverrides);
2213
+ console.log("šŸ“‹ Loading model metadata...");
2214
+ const modelInfo = await fetchModelInfo();
2215
+ console.log("šŸ“ Generating resume YAML...");
2216
+ const yaml = generateResumeYaml(aggregate(allRecords, configsMap, pricing, since, until), modelInfo, {
2217
+ name: opts.name,
2218
+ headline: opts.headline
2219
+ });
2220
+ const outputPath = resolve(opts.output);
2221
+ writeFileSync(outputPath, yaml, "utf-8");
2222
+ console.log(`\nāœ… Saved to ${outputPath}`);
2223
+ console.log(`šŸ“„ Render your resume at https://rendercv.com/`);
2224
+ });
2225
+ program.parse();
2226
+ //#endregion
2227
+ export {};
2228
+
2229
+ //# sourceMappingURL=cli.mjs.map