@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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/codeql.yml +103 -0
- package/.github/workflows/publish.yml +23 -0
- package/.github/workflows/release.yml +52 -0
- package/.github/workflows/test.yml +32 -0
- package/.idea/workspace.xml +125 -0
- package/AGENTS.md +34 -0
- package/README.md +145 -0
- package/bin/wingman.ts +2 -0
- package/dist/bin/wingman.mjs +3 -0
- package/dist/src/cli.mjs +2229 -0
- package/dist/src/cli.mjs.map +1 -0
- package/docs/AGENTS.md +172 -0
- package/docs/resume.yaml +68 -0
- package/docs/wingman.pdf +2544 -1
- package/docs/wingman.png +0 -0
- package/docs/wingman.svg +376 -0
- package/package.json +51 -0
- package/scripts/generate-demo.ts +265 -0
- package/src/AGENTS.md +50 -0
- package/src/agents/AGENTS.md +52 -0
- package/src/agents/claude-code.ts +160 -0
- package/src/agents/codex.ts +174 -0
- package/src/agents/gemini-cli.ts +100 -0
- package/src/agents/opencode.ts +117 -0
- package/src/agents/registry.ts +12 -0
- package/src/agents/skills.ts +51 -0
- package/src/aggregator.ts +142 -0
- package/src/cli.ts +194 -0
- package/src/inventory.ts +84 -0
- package/src/pricing/AGENTS.md +36 -0
- package/src/pricing/__tests__/model-info.test.ts +135 -0
- package/src/pricing/engine.ts +86 -0
- package/src/pricing/models-dev.ts +253 -0
- package/src/resume/AGENTS.md +34 -0
- package/src/resume/__tests__/renderer.test.ts +286 -0
- package/src/resume/renderer.ts +286 -0
- package/src/svg/AGENTS.md +22 -0
- package/src/svg/components.ts +266 -0
- package/src/svg/icons.ts +83 -0
- package/src/themes/AGENTS.md +60 -0
- package/src/themes/__tests__/themes.test.ts +187 -0
- package/src/themes/github-dark/index.ts +46 -0
- package/src/themes/github-dark/palette.ts +19 -0
- package/src/themes/github-light/index.ts +46 -0
- package/src/themes/github-light/palette.ts +19 -0
- package/src/themes/onedark/index.ts +46 -0
- package/src/themes/onedark/palette.ts +19 -0
- package/src/themes/registry.ts +18 -0
- package/src/themes/shared/charts.ts +112 -0
- package/src/themes/shared/context.ts +47 -0
- package/src/themes/shared/footer.ts +52 -0
- package/src/themes/shared/header.ts +29 -0
- package/src/themes/shared/heatmap.ts +229 -0
- package/src/themes/shared/helpers.ts +103 -0
- package/src/themes/shared/inventory.ts +105 -0
- package/src/themes/shared/legend.ts +91 -0
- package/src/themes/shared/sections.ts +20 -0
- package/src/themes/shared/stats.ts +60 -0
- package/src/types.ts +106 -0
- package/tsconfig.json +18 -0
- package/tsdown.config.ts +10 -0
package/dist/src/cli.mjs
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|