@pi-unipi/info-screen 0.1.1
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/config.ts +171 -0
- package/core-groups.ts +482 -0
- package/index.ts +191 -0
- package/package.json +50 -0
- package/registry.ts +183 -0
- package/settings/settings-tui.ts +287 -0
- package/tui/info-overlay.ts +406 -0
- package/types.ts +73 -0
- package/usage-parser.ts +308 -0
package/config.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/info-screen — Config system
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes info-screen settings in ~/.pi/agent/settings.json
|
|
5
|
+
* under the "unipi.info" key.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import type { InfoScreenSettings, GroupSettings } from "./types.js";
|
|
12
|
+
import { DEFAULT_SETTINGS } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** Settings path */
|
|
15
|
+
const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
16
|
+
|
|
17
|
+
/** Settings key within settings.json */
|
|
18
|
+
const SETTINGS_KEY = "unipi";
|
|
19
|
+
|
|
20
|
+
/** Cached settings */
|
|
21
|
+
let cachedSettings: InfoScreenSettings | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if value is a plain object.
|
|
25
|
+
*/
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read the full settings file.
|
|
32
|
+
*/
|
|
33
|
+
function readSettingsFile(): Record<string, unknown> {
|
|
34
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
37
|
+
return isRecord(parsed) ? parsed : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write the full settings file.
|
|
45
|
+
*/
|
|
46
|
+
function writeSettingsFile(data: Record<string, unknown>): void {
|
|
47
|
+
const dir = require("node:path").dirname(SETTINGS_PATH);
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get info-screen settings from settings.json.
|
|
56
|
+
*/
|
|
57
|
+
export function getInfoSettings(): InfoScreenSettings {
|
|
58
|
+
if (cachedSettings) return cachedSettings;
|
|
59
|
+
|
|
60
|
+
const settings = readSettingsFile();
|
|
61
|
+
const unipi = settings[SETTINGS_KEY];
|
|
62
|
+
|
|
63
|
+
if (!isRecord(unipi) || !isRecord(unipi.info)) {
|
|
64
|
+
cachedSettings = { ...DEFAULT_SETTINGS };
|
|
65
|
+
return cachedSettings;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const info = unipi.info as Record<string, unknown>;
|
|
69
|
+
|
|
70
|
+
cachedSettings = {
|
|
71
|
+
showOnBoot: typeof info.showOnBoot === "boolean" ? info.showOnBoot : DEFAULT_SETTINGS.showOnBoot,
|
|
72
|
+
bootTimeoutMs: typeof info.bootTimeoutMs === "number" ? info.bootTimeoutMs : DEFAULT_SETTINGS.bootTimeoutMs,
|
|
73
|
+
groups: isRecord(info.groups) ? parseGroupSettings(info.groups) : {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return cachedSettings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse group settings from raw object.
|
|
81
|
+
*/
|
|
82
|
+
function parseGroupSettings(raw: Record<string, unknown>): Record<string, GroupSettings> {
|
|
83
|
+
const result: Record<string, GroupSettings> = {};
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
86
|
+
if (!isRecord(value)) continue;
|
|
87
|
+
|
|
88
|
+
result[key] = {
|
|
89
|
+
show: typeof value.show === "boolean" ? value.show : true,
|
|
90
|
+
stats: isRecord(value.stats) ? parseStatSettings(value.stats) : undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse stat settings from raw object.
|
|
99
|
+
*/
|
|
100
|
+
function parseStatSettings(raw: Record<string, unknown>): Record<string, boolean> {
|
|
101
|
+
const result: Record<string, boolean> = {};
|
|
102
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
103
|
+
if (typeof value === "boolean") {
|
|
104
|
+
result[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Save info-screen settings to settings.json.
|
|
112
|
+
*/
|
|
113
|
+
export function saveInfoSettings(settings: InfoScreenSettings): void {
|
|
114
|
+
const file = readSettingsFile();
|
|
115
|
+
|
|
116
|
+
if (!isRecord(file[SETTINGS_KEY])) {
|
|
117
|
+
file[SETTINGS_KEY] = {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
(file[SETTINGS_KEY] as Record<string, unknown>).info = {
|
|
121
|
+
showOnBoot: settings.showOnBoot,
|
|
122
|
+
bootTimeoutMs: settings.bootTimeoutMs,
|
|
123
|
+
groups: settings.groups,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
writeSettingsFile(file);
|
|
127
|
+
cachedSettings = settings;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get settings for a specific group.
|
|
132
|
+
*/
|
|
133
|
+
export function getGroupSettings(groupId: string): GroupSettings {
|
|
134
|
+
const settings = getInfoSettings();
|
|
135
|
+
return settings.groups[groupId] ?? { show: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update settings for a specific group.
|
|
140
|
+
*/
|
|
141
|
+
export function setGroupSettings(groupId: string, groupSettings: GroupSettings): void {
|
|
142
|
+
const settings = getInfoSettings();
|
|
143
|
+
settings.groups[groupId] = groupSettings;
|
|
144
|
+
saveInfoSettings(settings);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a group is enabled.
|
|
149
|
+
*/
|
|
150
|
+
export function isGroupEnabled(groupId: string): boolean {
|
|
151
|
+
const settings = getInfoSettings();
|
|
152
|
+
if (!(groupId in settings.groups)) return true; // Default to enabled
|
|
153
|
+
return settings.groups[groupId].show;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if a stat within a group is enabled.
|
|
158
|
+
*/
|
|
159
|
+
export function isStatEnabled(groupId: string, statId: string): boolean {
|
|
160
|
+
const groupSettings = getGroupSettings(groupId);
|
|
161
|
+
if (!groupSettings.stats) return true; // Default to enabled
|
|
162
|
+
if (!(statId in groupSettings.stats)) return true;
|
|
163
|
+
return groupSettings.stats[statId];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear cached settings (for testing or reload).
|
|
168
|
+
*/
|
|
169
|
+
export function clearSettingsCache(): void {
|
|
170
|
+
cachedSettings = null;
|
|
171
|
+
}
|
package/core-groups.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/info-screen — Core group registrations
|
|
3
|
+
*
|
|
4
|
+
* Registers the 5 core groups: Overview, Usage, Tools, Extensions, Skills.
|
|
5
|
+
* These are always available (subject to config visibility).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
9
|
+
import { join, basename } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { infoRegistry } from "./registry.js";
|
|
12
|
+
import { parseUsageStats, formatTokens, formatCost } from "./usage-parser.js";
|
|
13
|
+
import type { InfoGroup } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get package version from package.json.
|
|
17
|
+
*/
|
|
18
|
+
function getPackageVersion(packageDir: string): string {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = join(packageDir, "package.json");
|
|
21
|
+
if (!existsSync(pkgPath)) return "0.0.0";
|
|
22
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
23
|
+
return pkg?.version ?? "0.0.0";
|
|
24
|
+
} catch {
|
|
25
|
+
return "0.0.0";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get pi version from its package.json.
|
|
31
|
+
*/
|
|
32
|
+
function getPiVersion(): string {
|
|
33
|
+
// Try to find pi's package.json in various locations
|
|
34
|
+
const possiblePaths = [
|
|
35
|
+
// Global npm install
|
|
36
|
+
join(homedir(), ".local", "share", "mise", "installs", "node", "24.14.1", "lib", "node_modules", "@mariozechner", "pi-coding-agent", "package.json"),
|
|
37
|
+
// Alternative locations
|
|
38
|
+
join(homedir(), ".local", "share", "mise", "installs", "node", "lib", "node_modules", "@mariozechner", "pi-coding-agent", "package.json"),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const pkgPath of possiblePaths) {
|
|
42
|
+
try {
|
|
43
|
+
if (existsSync(pkgPath)) {
|
|
44
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
return pkg?.version ?? "unknown";
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Continue to next path
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: try to run pi --version
|
|
53
|
+
try {
|
|
54
|
+
const { execSync } = require("node:child_process");
|
|
55
|
+
const version = execSync("pi --version 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
56
|
+
// Extract version number from output like "pi v0.42.4"
|
|
57
|
+
const match = version.match(/v([\d.]+)/);
|
|
58
|
+
if (match) return match[1];
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return "unknown";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Discover loaded extensions by scanning filesystem.
|
|
68
|
+
*/
|
|
69
|
+
function discoverExtensions(): Array<{ name: string; source: string; version: string }> {
|
|
70
|
+
const extensions: Array<{ name: string; source: string; version: string }> = [];
|
|
71
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
|
|
74
|
+
// Check settings.json for package extensions
|
|
75
|
+
const settingsPaths = [
|
|
76
|
+
join(homeDir, ".pi", "agent", "settings.json"),
|
|
77
|
+
join(cwd, ".pi", "settings.json"),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const counted = new Set<string>();
|
|
81
|
+
|
|
82
|
+
for (const settingsPath of settingsPaths) {
|
|
83
|
+
if (!existsSync(settingsPath)) continue;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
87
|
+
if (typeof settings !== "object" || settings === null) continue;
|
|
88
|
+
|
|
89
|
+
const packages = settings.packages;
|
|
90
|
+
if (!Array.isArray(packages)) continue;
|
|
91
|
+
|
|
92
|
+
for (const pkg of packages) {
|
|
93
|
+
let source: string | undefined;
|
|
94
|
+
let extensionsFilter: string[] | undefined;
|
|
95
|
+
|
|
96
|
+
if (typeof pkg === "string") {
|
|
97
|
+
source = pkg;
|
|
98
|
+
} else if (typeof pkg === "object" && pkg !== null) {
|
|
99
|
+
source = pkg.source;
|
|
100
|
+
extensionsFilter = pkg.extensions;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!source) continue;
|
|
104
|
+
|
|
105
|
+
// Extract package name from source
|
|
106
|
+
let name = source;
|
|
107
|
+
if (source.startsWith("npm:")) {
|
|
108
|
+
const npmPkg = source.slice(4);
|
|
109
|
+
// Handle scoped packages like @scope/name
|
|
110
|
+
if (npmPkg.startsWith("@")) {
|
|
111
|
+
// @scope/name -> name
|
|
112
|
+
const parts = npmPkg.split("/");
|
|
113
|
+
name = parts.length > 1 ? parts[1] : npmPkg;
|
|
114
|
+
} else {
|
|
115
|
+
name = npmPkg.split("@")[0];
|
|
116
|
+
}
|
|
117
|
+
} else if (source.startsWith("git:")) {
|
|
118
|
+
name = source.split("/").pop()?.replace(/\.git$/, "") ?? source;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Skip empty names
|
|
122
|
+
if (!name || name.trim() === "") continue;
|
|
123
|
+
if (counted.has(name)) continue;
|
|
124
|
+
counted.add(name);
|
|
125
|
+
|
|
126
|
+
extensions.push({
|
|
127
|
+
name,
|
|
128
|
+
source: source.startsWith("npm:") ? "npm" : source.startsWith("git:") ? "git" : "local",
|
|
129
|
+
version: "latest",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Skip malformed settings
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check extension directories
|
|
138
|
+
const extensionDirs = [
|
|
139
|
+
join(homeDir, ".pi", "agent", "extensions"),
|
|
140
|
+
join(cwd, ".pi", "extensions"),
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
for (const dir of extensionDirs) {
|
|
144
|
+
if (!existsSync(dir)) continue;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const name = entry.name;
|
|
150
|
+
if (counted.has(name)) continue;
|
|
151
|
+
|
|
152
|
+
if (entry.isFile() && name.endsWith(".ts")) {
|
|
153
|
+
counted.add(name.replace(".ts", ""));
|
|
154
|
+
extensions.push({
|
|
155
|
+
name: name.replace(".ts", ""),
|
|
156
|
+
source: "local",
|
|
157
|
+
version: "local",
|
|
158
|
+
});
|
|
159
|
+
} else if (entry.isDirectory()) {
|
|
160
|
+
counted.add(name);
|
|
161
|
+
extensions.push({
|
|
162
|
+
name,
|
|
163
|
+
source: "local",
|
|
164
|
+
version: "local",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Skip unreadable directories
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return extensions;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Discover loaded skills by scanning filesystem.
|
|
178
|
+
*/
|
|
179
|
+
function discoverSkills(): Array<{ name: string; source: string }> {
|
|
180
|
+
const skills: Array<{ name: string; source: string }> = [];
|
|
181
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
|
|
184
|
+
// Skill directories to scan
|
|
185
|
+
const skillDirs = [
|
|
186
|
+
join(homeDir, ".pi", "agent", "skills"),
|
|
187
|
+
join(cwd, ".pi", "skills"),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const counted = new Set<string>();
|
|
191
|
+
|
|
192
|
+
for (const dir of skillDirs) {
|
|
193
|
+
if (!existsSync(dir)) continue;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (!entry.isDirectory()) continue;
|
|
199
|
+
|
|
200
|
+
const name = entry.name;
|
|
201
|
+
if (counted.has(name)) continue;
|
|
202
|
+
|
|
203
|
+
// Check if it has a SKILL.md
|
|
204
|
+
const skillPath = join(dir, name, "SKILL.md");
|
|
205
|
+
if (existsSync(skillPath)) {
|
|
206
|
+
counted.add(name);
|
|
207
|
+
skills.push({
|
|
208
|
+
name,
|
|
209
|
+
source: dir.includes(homeDir) ? "global" : "project",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Skip unreadable directories
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return skills;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Track announced modules.
|
|
223
|
+
*/
|
|
224
|
+
const announcedModules: Array<{ name: string; version: string }> = [];
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Track registered tools.
|
|
228
|
+
*/
|
|
229
|
+
const registeredTools: Array<{ name: string; source: string }> = [];
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Add a module to the announced list.
|
|
233
|
+
*/
|
|
234
|
+
export function trackModule(name: string, version: string): void {
|
|
235
|
+
if (!announcedModules.find((m) => m.name === name)) {
|
|
236
|
+
announcedModules.push({ name, version });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get list of announced modules.
|
|
242
|
+
*/
|
|
243
|
+
export function getAnnouncedModules(): Array<{ name: string; version: string }> {
|
|
244
|
+
return [...announcedModules];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Track a registered tool.
|
|
249
|
+
*/
|
|
250
|
+
export function trackTool(name: string, source: string): void {
|
|
251
|
+
if (!registeredTools.find((t) => t.name === name)) {
|
|
252
|
+
registeredTools.push({ name, source });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get list of registered tools.
|
|
258
|
+
*/
|
|
259
|
+
export function getRegisteredTools(): Array<{ name: string; source: string }> {
|
|
260
|
+
return [...registeredTools];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Register all core groups.
|
|
265
|
+
*/
|
|
266
|
+
export function registerCoreGroups(): void {
|
|
267
|
+
// 1. Overview group
|
|
268
|
+
infoRegistry.registerGroup({
|
|
269
|
+
id: "overview",
|
|
270
|
+
name: "Overview",
|
|
271
|
+
icon: "📊",
|
|
272
|
+
priority: 10,
|
|
273
|
+
config: {
|
|
274
|
+
showByDefault: true,
|
|
275
|
+
stats: [
|
|
276
|
+
{ id: "version", label: "Pi Version", show: true },
|
|
277
|
+
{ id: "cwd", label: "Working Directory", show: true },
|
|
278
|
+
{ id: "modules", label: "Active Modules", show: true },
|
|
279
|
+
{ id: "uptime", label: "Session Uptime", show: true },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
dataProvider: async () => {
|
|
283
|
+
const cwd = process.cwd();
|
|
284
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
285
|
+
const shortCwd = cwd.startsWith(homeDir) ? `~${cwd.slice(homeDir.length)}` : cwd;
|
|
286
|
+
|
|
287
|
+
const modules = getAnnouncedModules();
|
|
288
|
+
const moduleNames = modules.map((m) => m.name.replace(/^@[^/]+\//, ""));
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
version: { value: getPiVersion(), detail: "pi" },
|
|
292
|
+
cwd: { value: shortCwd },
|
|
293
|
+
modules: {
|
|
294
|
+
value: String(modules.length),
|
|
295
|
+
detail: moduleNames.slice(0, 4).join(", ") + (moduleNames.length > 4 ? ` +${moduleNames.length - 4} more` : ""),
|
|
296
|
+
},
|
|
297
|
+
uptime: { value: formatUptime(process.uptime()) },
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 2. Usage group
|
|
303
|
+
infoRegistry.registerGroup({
|
|
304
|
+
id: "usage",
|
|
305
|
+
name: "Usage",
|
|
306
|
+
icon: "💰",
|
|
307
|
+
priority: 20,
|
|
308
|
+
config: {
|
|
309
|
+
showByDefault: true,
|
|
310
|
+
stats: [
|
|
311
|
+
{ id: "tokensToday", label: "Tokens Today", show: true },
|
|
312
|
+
{ id: "tokensWeek", label: "Tokens This Week", show: true },
|
|
313
|
+
{ id: "tokensMonth", label: "Tokens This Month", show: true },
|
|
314
|
+
{ id: "costToday", label: "Cost Today", show: true },
|
|
315
|
+
{ id: "costAllTime", label: "Cost All Time", show: true },
|
|
316
|
+
{ id: "topModelToday", label: "Top Model Today", show: true },
|
|
317
|
+
{ id: "topModelWeek", label: "Top Model Week", show: true },
|
|
318
|
+
{ id: "topModelMonth", label: "Top Model Month", show: true },
|
|
319
|
+
{ id: "sessions", label: "Total Sessions", show: true },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
dataProvider: async () => {
|
|
323
|
+
const stats = parseUsageStats();
|
|
324
|
+
|
|
325
|
+
// Find top model for each period
|
|
326
|
+
const findTopModel = (modelStats: Record<string, { tokens: number; cost: number; sessions: number }> | undefined) => {
|
|
327
|
+
if (!modelStats) return { name: "none", cost: 0 };
|
|
328
|
+
let topName = "none";
|
|
329
|
+
let topCost = 0;
|
|
330
|
+
for (const [model, data] of Object.entries(modelStats)) {
|
|
331
|
+
if (data.cost > topCost) {
|
|
332
|
+
topCost = data.cost;
|
|
333
|
+
topName = model;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Strip "Claude " prefix for brevity
|
|
337
|
+
if (topName.startsWith("Claude ")) {
|
|
338
|
+
topName = topName.slice(7);
|
|
339
|
+
}
|
|
340
|
+
return { name: topName, cost: topCost };
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const topToday = findTopModel(stats.byModelToday);
|
|
344
|
+
const topWeek = findTopModel(stats.byModelWeek);
|
|
345
|
+
const topMonth = findTopModel(stats.byModelMonth);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
tokensToday: { value: formatTokens(stats.tokens.today) },
|
|
349
|
+
tokensWeek: { value: formatTokens(stats.tokens.week) },
|
|
350
|
+
tokensMonth: { value: formatTokens(stats.tokens.month) },
|
|
351
|
+
costToday: { value: formatCost(stats.cost.today) },
|
|
352
|
+
costAllTime: { value: formatCost(stats.cost.allTime) },
|
|
353
|
+
topModelToday: { value: topToday.name, detail: formatCost(topToday.cost) },
|
|
354
|
+
topModelWeek: { value: topWeek.name, detail: formatCost(topWeek.cost) },
|
|
355
|
+
topModelMonth: { value: topMonth.name, detail: formatCost(topMonth.cost) },
|
|
356
|
+
sessions: { value: String(stats.sessionCount) },
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// 3. Tools group
|
|
362
|
+
infoRegistry.registerGroup({
|
|
363
|
+
id: "tools",
|
|
364
|
+
name: "Tools",
|
|
365
|
+
icon: "🔧",
|
|
366
|
+
priority: 30,
|
|
367
|
+
config: {
|
|
368
|
+
showByDefault: true,
|
|
369
|
+
stats: [
|
|
370
|
+
{ id: "total", label: "Total Tools", show: true },
|
|
371
|
+
{ id: "builtin", label: "Built-in", show: true },
|
|
372
|
+
{ id: "registered", label: "Registered", show: true },
|
|
373
|
+
{ id: "list", label: "Tools", show: true },
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
dataProvider: async () => {
|
|
377
|
+
const tools = getRegisteredTools();
|
|
378
|
+
const builtin = tools.filter((t) => t.source === "builtin");
|
|
379
|
+
const custom = tools.filter((t) => t.source === "registered");
|
|
380
|
+
|
|
381
|
+
// Build tool list - show all
|
|
382
|
+
const toolNames = tools.map((t) => `${t.name}`);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
total: { value: String(tools.length) },
|
|
386
|
+
builtin: { value: String(builtin.length) },
|
|
387
|
+
registered: { value: String(custom.length) },
|
|
388
|
+
list: {
|
|
389
|
+
value: toolNames.length > 0 ? toolNames[0] : "none",
|
|
390
|
+
detail: toolNames.length > 1 ? toolNames.slice(1).join("\n") : undefined,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// 4. Extensions group
|
|
397
|
+
infoRegistry.registerGroup({
|
|
398
|
+
id: "extensions",
|
|
399
|
+
name: "Extensions",
|
|
400
|
+
icon: "📦",
|
|
401
|
+
priority: 40,
|
|
402
|
+
config: {
|
|
403
|
+
showByDefault: true,
|
|
404
|
+
stats: [
|
|
405
|
+
{ id: "count", label: "Total Extensions", show: true },
|
|
406
|
+
{ id: "list", label: "Extensions", show: true },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
dataProvider: async () => {
|
|
410
|
+
const extensions = discoverExtensions();
|
|
411
|
+
const bySource: Record<string, number> = {};
|
|
412
|
+
for (const ext of extensions) {
|
|
413
|
+
bySource[ext.source] = (bySource[ext.source] ?? 0) + 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const breakdown = Object.entries(bySource)
|
|
417
|
+
.map(([src, count]) => `${count} ${src}`)
|
|
418
|
+
.join(", ");
|
|
419
|
+
|
|
420
|
+
// Build multi-line list - show all
|
|
421
|
+
const listLines: string[] = [];
|
|
422
|
+
for (const ext of extensions) {
|
|
423
|
+
listLines.push(`${ext.name} (${ext.source})`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
count: { value: String(extensions.length), detail: breakdown || "none" },
|
|
428
|
+
list: {
|
|
429
|
+
value: listLines.length > 0 ? listLines[0] : "none",
|
|
430
|
+
detail: listLines.length > 1 ? listLines.slice(1).join("\n") : undefined,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// 5. Skills group
|
|
437
|
+
infoRegistry.registerGroup({
|
|
438
|
+
id: "skills",
|
|
439
|
+
name: "Skills",
|
|
440
|
+
icon: "🎯",
|
|
441
|
+
priority: 50,
|
|
442
|
+
config: {
|
|
443
|
+
showByDefault: true,
|
|
444
|
+
stats: [
|
|
445
|
+
{ id: "count", label: "Total Skills", show: true },
|
|
446
|
+
{ id: "global", label: "Global Skills", show: true },
|
|
447
|
+
{ id: "project", label: "Project Skills", show: true },
|
|
448
|
+
{ id: "list", label: "Skills", show: true },
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
dataProvider: async () => {
|
|
452
|
+
const skills = discoverSkills();
|
|
453
|
+
const global = skills.filter((s) => s.source === "global");
|
|
454
|
+
const project = skills.filter((s) => s.source === "project");
|
|
455
|
+
|
|
456
|
+
// Build skill list - show all
|
|
457
|
+
const skillNames = skills.map((s) => `${s.name} (${s.source})`);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
count: { value: String(skills.length) },
|
|
461
|
+
global: { value: String(global.length) },
|
|
462
|
+
project: { value: String(project.length) },
|
|
463
|
+
list: {
|
|
464
|
+
value: skillNames.length > 0 ? skillNames[0] : "none",
|
|
465
|
+
detail: skillNames.length > 1 ? skillNames.slice(1).join("\n") : undefined,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Format uptime for display.
|
|
474
|
+
*/
|
|
475
|
+
function formatUptime(seconds: number): string {
|
|
476
|
+
const hours = Math.floor(seconds / 3600);
|
|
477
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
478
|
+
|
|
479
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
480
|
+
if (minutes > 0) return `${minutes}m`;
|
|
481
|
+
return `${Math.floor(seconds)}s`;
|
|
482
|
+
}
|