@sooneocean/claude-hud 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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +20 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/commands/configure.md +361 -0
- package/commands/export.md +43 -0
- package/commands/health.md +61 -0
- package/commands/setup.md +287 -0
- package/commands/theme.md +31 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.d.ts.map +1 -0
- package/dist/alert.js +53 -0
- package/dist/alert.js.map +1 -0
- package/dist/burn-rate.d.ts +4 -0
- package/dist/burn-rate.d.ts.map +1 -0
- package/dist/burn-rate.js +36 -0
- package/dist/burn-rate.js.map +1 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +47 -0
- package/dist/cache.js.map +1 -0
- package/dist/claude-config-dir.d.ts +4 -0
- package/dist/claude-config-dir.d.ts.map +1 -0
- package/dist/claude-config-dir.js +24 -0
- package/dist/claude-config-dir.js.map +1 -0
- package/dist/config-io.d.ts +6 -0
- package/dist/config-io.d.ts.map +1 -0
- package/dist/config-io.js +27 -0
- package/dist/config-io.js.map +1 -0
- package/dist/config-reader.d.ts +8 -0
- package/dist/config-reader.d.ts.map +1 -0
- package/dist/config-reader.js +204 -0
- package/dist/config-reader.js.map +1 -0
- package/dist/config.d.ts +94 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +358 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/cost-tracker.d.ts +9 -0
- package/dist/cost-tracker.d.ts.map +1 -0
- package/dist/cost-tracker.js +46 -0
- package/dist/cost-tracker.js.map +1 -0
- package/dist/debug.d.ts +6 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +15 -0
- package/dist/debug.js.map +1 -0
- package/dist/extra-cmd.d.ts +20 -0
- package/dist/extra-cmd.d.ts.map +1 -0
- package/dist/extra-cmd.js +112 -0
- package/dist/extra-cmd.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +94 -0
- package/dist/git.js.map +1 -0
- package/dist/health-check.d.ts +12 -0
- package/dist/health-check.d.ts.map +1 -0
- package/dist/health-check.js +37 -0
- package/dist/health-check.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/agent-teams-provider.d.ts +10 -0
- package/dist/providers/agent-teams-provider.d.ts.map +1 -0
- package/dist/providers/agent-teams-provider.js +57 -0
- package/dist/providers/agent-teams-provider.js.map +1 -0
- package/dist/providers/agw-provider.d.ts +10 -0
- package/dist/providers/agw-provider.d.ts.map +1 -0
- package/dist/providers/agw-provider.js +49 -0
- package/dist/providers/agw-provider.js.map +1 -0
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +25 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/render/agents-line.d.ts +3 -0
- package/dist/render/agents-line.d.ts.map +1 -0
- package/dist/render/agents-line.js +40 -0
- package/dist/render/agents-line.js.map +1 -0
- package/dist/render/alert-line.d.ts +3 -0
- package/dist/render/alert-line.d.ts.map +1 -0
- package/dist/render/alert-line.js +11 -0
- package/dist/render/alert-line.js.map +1 -0
- package/dist/render/colors.d.ts +39 -0
- package/dist/render/colors.d.ts.map +1 -0
- package/dist/render/colors.js +109 -0
- package/dist/render/colors.js.map +1 -0
- package/dist/render/framework-line.d.ts +3 -0
- package/dist/render/framework-line.d.ts.map +1 -0
- package/dist/render/framework-line.js +32 -0
- package/dist/render/framework-line.js.map +1 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +435 -0
- package/dist/render/index.js.map +1 -0
- package/dist/render/lines/environment.d.ts +3 -0
- package/dist/render/lines/environment.d.ts.map +1 -0
- package/dist/render/lines/environment.js +30 -0
- package/dist/render/lines/environment.js.map +1 -0
- package/dist/render/lines/identity.d.ts +3 -0
- package/dist/render/lines/identity.d.ts.map +1 -0
- package/dist/render/lines/identity.js +93 -0
- package/dist/render/lines/identity.js.map +1 -0
- package/dist/render/lines/index.d.ts +5 -0
- package/dist/render/lines/index.d.ts.map +1 -0
- package/dist/render/lines/index.js +5 -0
- package/dist/render/lines/index.js.map +1 -0
- package/dist/render/lines/project.d.ts +3 -0
- package/dist/render/lines/project.d.ts.map +1 -0
- package/dist/render/lines/project.js +100 -0
- package/dist/render/lines/project.js.map +1 -0
- package/dist/render/lines/usage.d.ts +3 -0
- package/dist/render/lines/usage.d.ts.map +1 -0
- package/dist/render/lines/usage.js +65 -0
- package/dist/render/lines/usage.js.map +1 -0
- package/dist/render/session-line.d.ts +7 -0
- package/dist/render/session-line.d.ts.map +1 -0
- package/dist/render/session-line.js +227 -0
- package/dist/render/session-line.js.map +1 -0
- package/dist/render/todos-line.d.ts +3 -0
- package/dist/render/todos-line.d.ts.map +1 -0
- package/dist/render/todos-line.js +29 -0
- package/dist/render/todos-line.js.map +1 -0
- package/dist/render/tools-line.d.ts +3 -0
- package/dist/render/tools-line.d.ts.map +1 -0
- package/dist/render/tools-line.js +45 -0
- package/dist/render/tools-line.js.map +1 -0
- package/dist/session-history.d.ts +15 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +46 -0
- package/dist/session-history.js.map +1 -0
- package/dist/session-stats.d.ts +11 -0
- package/dist/session-stats.d.ts.map +1 -0
- package/dist/session-stats.js +48 -0
- package/dist/session-stats.js.map +1 -0
- package/dist/speed-tracker.d.ts +7 -0
- package/dist/speed-tracker.d.ts.map +1 -0
- package/dist/speed-tracker.js +34 -0
- package/dist/speed-tracker.js.map +1 -0
- package/dist/stdin.d.ts +9 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +142 -0
- package/dist/stdin.js.map +1 -0
- package/dist/themes.d.ts +10 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +81 -0
- package/dist/themes.js.map +1 -0
- package/dist/transcript.d.ts +3 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +221 -0
- package/dist/transcript.js.map +1 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-api.d.ts +62 -0
- package/dist/usage-api.d.ts.map +1 -0
- package/dist/usage-api.js +908 -0
- package/dist/usage-api.js.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +75 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/terminal.d.ts +5 -0
- package/dist/utils/terminal.d.ts.map +1 -0
- package/dist/utils/terminal.js +42 -0
- package/dist/utils/terminal.js.map +1 -0
- package/package.json +36 -0
- package/src/alert.ts +75 -0
- package/src/burn-rate.ts +45 -0
- package/src/cache.ts +57 -0
- package/src/claude-config-dir.ts +27 -0
- package/src/config-io.ts +26 -0
- package/src/config-reader.ts +236 -0
- package/src/config.ts +496 -0
- package/src/constants.ts +10 -0
- package/src/cost-tracker.ts +53 -0
- package/src/debug.ts +16 -0
- package/src/extra-cmd.ts +125 -0
- package/src/git.ts +126 -0
- package/src/health-check.ts +50 -0
- package/src/index.ts +234 -0
- package/src/providers/agent-teams-provider.ts +56 -0
- package/src/providers/agw-provider.ts +47 -0
- package/src/providers/index.ts +27 -0
- package/src/render/agents-line.ts +51 -0
- package/src/render/alert-line.ts +11 -0
- package/src/render/colors.ts +145 -0
- package/src/render/framework-line.ts +34 -0
- package/src/render/index.ts +512 -0
- package/src/render/lines/environment.ts +41 -0
- package/src/render/lines/identity.ts +109 -0
- package/src/render/lines/index.ts +4 -0
- package/src/render/lines/project.ts +113 -0
- package/src/render/lines/usage.ts +79 -0
- package/src/render/session-line.ts +253 -0
- package/src/render/todos-line.ts +35 -0
- package/src/render/tools-line.ts +58 -0
- package/src/session-history.ts +62 -0
- package/src/session-stats.ts +65 -0
- package/src/speed-tracker.ts +51 -0
- package/src/stdin.ts +169 -0
- package/src/themes.ts +90 -0
- package/src/transcript.ts +268 -0
- package/src/types.ts +146 -0
- package/src/usage-api.ts +1090 -0
- package/src/utils/format.ts +79 -0
- package/src/utils/terminal.ts +46 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { getHudPluginDir } from './claude-config-dir.js';
|
|
5
|
+
import type { AlertAction } from './types.js';
|
|
6
|
+
import { getTheme } from './themes.js';
|
|
7
|
+
|
|
8
|
+
export type LineLayoutType = 'compact' | 'expanded';
|
|
9
|
+
|
|
10
|
+
export type AutocompactBufferMode = 'enabled' | 'disabled';
|
|
11
|
+
export type ContextValueMode = 'percent' | 'tokens' | 'remaining';
|
|
12
|
+
export type HudElement = 'project' | 'context' | 'usage' | 'environment' | 'framework' | 'tools' | 'agents' | 'todos' | 'alert';
|
|
13
|
+
export type HudColorName =
|
|
14
|
+
| 'red'
|
|
15
|
+
| 'green'
|
|
16
|
+
| 'yellow'
|
|
17
|
+
| 'magenta'
|
|
18
|
+
| 'cyan'
|
|
19
|
+
| 'brightBlue'
|
|
20
|
+
| 'brightMagenta';
|
|
21
|
+
|
|
22
|
+
/** A color value: named preset, 256-color index (0-255), or hex string (#rrggbb). */
|
|
23
|
+
export type HudColorValue = HudColorName | number | string;
|
|
24
|
+
|
|
25
|
+
export interface HudColorOverrides {
|
|
26
|
+
context: HudColorValue;
|
|
27
|
+
usage: HudColorValue;
|
|
28
|
+
warning: HudColorValue;
|
|
29
|
+
usageWarning: HudColorValue;
|
|
30
|
+
critical: HudColorValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_ELEMENT_ORDER: HudElement[] = [
|
|
34
|
+
'project', 'context', 'usage', 'environment', 'framework', 'tools', 'agents', 'todos', 'alert',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const KNOWN_ELEMENTS = new Set<HudElement>(DEFAULT_ELEMENT_ORDER);
|
|
38
|
+
|
|
39
|
+
export interface HudConfig {
|
|
40
|
+
lineLayout: LineLayoutType;
|
|
41
|
+
showSeparators: boolean;
|
|
42
|
+
pathLevels: 1 | 2 | 3;
|
|
43
|
+
elementOrder: HudElement[];
|
|
44
|
+
gitStatus: {
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
showDirty: boolean;
|
|
47
|
+
showAheadBehind: boolean;
|
|
48
|
+
showFileStats: boolean;
|
|
49
|
+
};
|
|
50
|
+
display: {
|
|
51
|
+
showModel: boolean;
|
|
52
|
+
showProject: boolean;
|
|
53
|
+
showContextBar: boolean;
|
|
54
|
+
contextValue: ContextValueMode;
|
|
55
|
+
showConfigCounts: boolean;
|
|
56
|
+
showDuration: boolean;
|
|
57
|
+
showSpeed: boolean;
|
|
58
|
+
showTokenBreakdown: boolean;
|
|
59
|
+
showUsage: boolean;
|
|
60
|
+
usageBarEnabled: boolean;
|
|
61
|
+
showTools: boolean;
|
|
62
|
+
showAgents: boolean;
|
|
63
|
+
showTodos: boolean;
|
|
64
|
+
showSessionName: boolean;
|
|
65
|
+
autocompactBuffer: AutocompactBufferMode;
|
|
66
|
+
usageThreshold: number;
|
|
67
|
+
sevenDayThreshold: number;
|
|
68
|
+
environmentThreshold: number;
|
|
69
|
+
customLine: string;
|
|
70
|
+
showFrameworks: boolean;
|
|
71
|
+
showBurnRate: boolean;
|
|
72
|
+
showAlerts: boolean;
|
|
73
|
+
activityIndicator: boolean;
|
|
74
|
+
treePrefixes: boolean;
|
|
75
|
+
mergeToolsAgents: boolean;
|
|
76
|
+
barStyle: 'classic' | 'modern';
|
|
77
|
+
showCost: boolean;
|
|
78
|
+
showNotifications: boolean;
|
|
79
|
+
};
|
|
80
|
+
theme: string;
|
|
81
|
+
usage: {
|
|
82
|
+
cacheTtlSeconds: number;
|
|
83
|
+
failureCacheTtlSeconds: number;
|
|
84
|
+
};
|
|
85
|
+
colors: HudColorOverrides;
|
|
86
|
+
frameworks: {
|
|
87
|
+
agw: { enabled: boolean; endpoint: string };
|
|
88
|
+
agentTeams: { enabled: boolean };
|
|
89
|
+
};
|
|
90
|
+
alerts: {
|
|
91
|
+
context: { warningThreshold: number; criticalThreshold: number; actions: AlertAction };
|
|
92
|
+
usage5h: { warningThreshold: number; criticalThreshold: number; actions: AlertAction };
|
|
93
|
+
usage7d: { warningThreshold: number; actions: AlertAction };
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const DEFAULT_CONFIG: HudConfig = {
|
|
98
|
+
lineLayout: 'expanded',
|
|
99
|
+
showSeparators: false,
|
|
100
|
+
pathLevels: 1,
|
|
101
|
+
elementOrder: [...DEFAULT_ELEMENT_ORDER],
|
|
102
|
+
gitStatus: {
|
|
103
|
+
enabled: true,
|
|
104
|
+
showDirty: true,
|
|
105
|
+
showAheadBehind: false,
|
|
106
|
+
showFileStats: false,
|
|
107
|
+
},
|
|
108
|
+
display: {
|
|
109
|
+
showModel: true,
|
|
110
|
+
showProject: true,
|
|
111
|
+
showContextBar: true,
|
|
112
|
+
contextValue: 'percent',
|
|
113
|
+
showConfigCounts: false,
|
|
114
|
+
showDuration: false,
|
|
115
|
+
showSpeed: false,
|
|
116
|
+
showTokenBreakdown: true,
|
|
117
|
+
showUsage: true,
|
|
118
|
+
usageBarEnabled: true,
|
|
119
|
+
showTools: false,
|
|
120
|
+
showAgents: false,
|
|
121
|
+
showTodos: false,
|
|
122
|
+
showSessionName: false,
|
|
123
|
+
autocompactBuffer: 'enabled',
|
|
124
|
+
usageThreshold: 0,
|
|
125
|
+
sevenDayThreshold: 80,
|
|
126
|
+
environmentThreshold: 0,
|
|
127
|
+
customLine: '',
|
|
128
|
+
showFrameworks: false,
|
|
129
|
+
showBurnRate: false,
|
|
130
|
+
showAlerts: true,
|
|
131
|
+
activityIndicator: true,
|
|
132
|
+
treePrefixes: true,
|
|
133
|
+
mergeToolsAgents: true,
|
|
134
|
+
barStyle: 'classic' as const,
|
|
135
|
+
showCost: false,
|
|
136
|
+
showNotifications: false,
|
|
137
|
+
},
|
|
138
|
+
theme: 'default',
|
|
139
|
+
usage: {
|
|
140
|
+
cacheTtlSeconds: 60,
|
|
141
|
+
failureCacheTtlSeconds: 15,
|
|
142
|
+
},
|
|
143
|
+
colors: {
|
|
144
|
+
context: 'green',
|
|
145
|
+
usage: 'brightBlue',
|
|
146
|
+
warning: 'yellow',
|
|
147
|
+
usageWarning: 'brightMagenta',
|
|
148
|
+
critical: 'red',
|
|
149
|
+
},
|
|
150
|
+
frameworks: {
|
|
151
|
+
agw: { enabled: true, endpoint: 'http://localhost:3000' },
|
|
152
|
+
agentTeams: { enabled: true },
|
|
153
|
+
},
|
|
154
|
+
alerts: {
|
|
155
|
+
context: { warningThreshold: 70, criticalThreshold: 85, actions: { visual: true, bell: false, predict: true } },
|
|
156
|
+
usage5h: { warningThreshold: 70, criticalThreshold: 90, actions: { visual: true, bell: true, predict: true } },
|
|
157
|
+
usage7d: { warningThreshold: 80, actions: { visual: true, bell: false, predict: true } },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export function getConfigPath(): string {
|
|
162
|
+
const homeDir = os.homedir();
|
|
163
|
+
return path.join(getHudPluginDir(homeDir), 'config.json');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validatePathLevels(value: unknown): value is 1 | 2 | 3 {
|
|
167
|
+
return value === 1 || value === 2 || value === 3;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateLineLayout(value: unknown): value is LineLayoutType {
|
|
171
|
+
return value === 'compact' || value === 'expanded';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validateAutocompactBuffer(value: unknown): value is AutocompactBufferMode {
|
|
175
|
+
return value === 'enabled' || value === 'disabled';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateContextValue(value: unknown): value is ContextValueMode {
|
|
179
|
+
return value === 'percent' || value === 'tokens' || value === 'remaining';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateColorName(value: unknown): value is HudColorName {
|
|
183
|
+
return value === 'red'
|
|
184
|
+
|| value === 'green'
|
|
185
|
+
|| value === 'yellow'
|
|
186
|
+
|| value === 'magenta'
|
|
187
|
+
|| value === 'cyan'
|
|
188
|
+
|| value === 'brightBlue'
|
|
189
|
+
|| value === 'brightMagenta';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
193
|
+
|
|
194
|
+
function validateColorValue(value: unknown): value is HudColorValue {
|
|
195
|
+
if (validateColorName(value)) return true;
|
|
196
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255) return true;
|
|
197
|
+
if (typeof value === 'string' && HEX_COLOR_PATTERN.test(value)) return true;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateElementOrder(value: unknown): HudElement[] {
|
|
202
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
203
|
+
return [...DEFAULT_ELEMENT_ORDER];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const seen = new Set<HudElement>();
|
|
207
|
+
const elementOrder: HudElement[] = [];
|
|
208
|
+
|
|
209
|
+
for (const item of value) {
|
|
210
|
+
if (typeof item !== 'string' || !KNOWN_ELEMENTS.has(item as HudElement)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const element = item as HudElement;
|
|
215
|
+
if (seen.has(element)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
seen.add(element);
|
|
220
|
+
elementOrder.push(element);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return elementOrder.length > 0 ? elementOrder : [...DEFAULT_ELEMENT_ORDER];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface LegacyConfig {
|
|
227
|
+
layout?: 'default' | 'separators' | Record<string, unknown>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function migrateConfig(userConfig: Partial<HudConfig> & LegacyConfig): Partial<HudConfig> {
|
|
231
|
+
const migrated = { ...userConfig } as Partial<HudConfig> & LegacyConfig;
|
|
232
|
+
|
|
233
|
+
if ('layout' in userConfig && !('lineLayout' in userConfig)) {
|
|
234
|
+
if (typeof userConfig.layout === 'string') {
|
|
235
|
+
// Legacy string migration (v0.0.x → v0.1.x)
|
|
236
|
+
if (userConfig.layout === 'separators') {
|
|
237
|
+
migrated.lineLayout = 'compact';
|
|
238
|
+
migrated.showSeparators = true;
|
|
239
|
+
} else {
|
|
240
|
+
migrated.lineLayout = 'compact';
|
|
241
|
+
migrated.showSeparators = false;
|
|
242
|
+
}
|
|
243
|
+
} else if (typeof userConfig.layout === 'object' && userConfig.layout !== null) {
|
|
244
|
+
// Object layout written by third-party tools — extract nested fields
|
|
245
|
+
const obj = userConfig.layout as Record<string, unknown>;
|
|
246
|
+
if (typeof obj.lineLayout === 'string') migrated.lineLayout = obj.lineLayout as LineLayoutType;
|
|
247
|
+
if (typeof obj.showSeparators === 'boolean') migrated.showSeparators = obj.showSeparators;
|
|
248
|
+
if (typeof obj.pathLevels === 'number') migrated.pathLevels = obj.pathLevels as 1 | 2 | 3;
|
|
249
|
+
}
|
|
250
|
+
delete migrated.layout;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return migrated;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function validateThreshold(value: unknown, max = 100): number {
|
|
257
|
+
if (typeof value !== 'number') return 0;
|
|
258
|
+
return Math.max(0, Math.min(max, value));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validatePositiveInt(value: unknown, defaultValue: number): number {
|
|
262
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) return defaultValue;
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function mergeConfig(userConfig: Partial<HudConfig>): HudConfig {
|
|
267
|
+
const migrated = migrateConfig(userConfig);
|
|
268
|
+
|
|
269
|
+
const lineLayout = validateLineLayout(migrated.lineLayout)
|
|
270
|
+
? migrated.lineLayout
|
|
271
|
+
: DEFAULT_CONFIG.lineLayout;
|
|
272
|
+
|
|
273
|
+
const showSeparators = typeof migrated.showSeparators === 'boolean'
|
|
274
|
+
? migrated.showSeparators
|
|
275
|
+
: DEFAULT_CONFIG.showSeparators;
|
|
276
|
+
|
|
277
|
+
const pathLevels = validatePathLevels(migrated.pathLevels)
|
|
278
|
+
? migrated.pathLevels
|
|
279
|
+
: DEFAULT_CONFIG.pathLevels;
|
|
280
|
+
|
|
281
|
+
const elementOrder = validateElementOrder(migrated.elementOrder);
|
|
282
|
+
|
|
283
|
+
const gitStatus = {
|
|
284
|
+
enabled: typeof migrated.gitStatus?.enabled === 'boolean'
|
|
285
|
+
? migrated.gitStatus.enabled
|
|
286
|
+
: DEFAULT_CONFIG.gitStatus.enabled,
|
|
287
|
+
showDirty: typeof migrated.gitStatus?.showDirty === 'boolean'
|
|
288
|
+
? migrated.gitStatus.showDirty
|
|
289
|
+
: DEFAULT_CONFIG.gitStatus.showDirty,
|
|
290
|
+
showAheadBehind: typeof migrated.gitStatus?.showAheadBehind === 'boolean'
|
|
291
|
+
? migrated.gitStatus.showAheadBehind
|
|
292
|
+
: DEFAULT_CONFIG.gitStatus.showAheadBehind,
|
|
293
|
+
showFileStats: typeof migrated.gitStatus?.showFileStats === 'boolean'
|
|
294
|
+
? migrated.gitStatus.showFileStats
|
|
295
|
+
: DEFAULT_CONFIG.gitStatus.showFileStats,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const display = {
|
|
299
|
+
showModel: typeof migrated.display?.showModel === 'boolean'
|
|
300
|
+
? migrated.display.showModel
|
|
301
|
+
: DEFAULT_CONFIG.display.showModel,
|
|
302
|
+
showProject: typeof migrated.display?.showProject === 'boolean'
|
|
303
|
+
? migrated.display.showProject
|
|
304
|
+
: DEFAULT_CONFIG.display.showProject,
|
|
305
|
+
showContextBar: typeof migrated.display?.showContextBar === 'boolean'
|
|
306
|
+
? migrated.display.showContextBar
|
|
307
|
+
: DEFAULT_CONFIG.display.showContextBar,
|
|
308
|
+
contextValue: validateContextValue(migrated.display?.contextValue)
|
|
309
|
+
? migrated.display.contextValue
|
|
310
|
+
: DEFAULT_CONFIG.display.contextValue,
|
|
311
|
+
showConfigCounts: typeof migrated.display?.showConfigCounts === 'boolean'
|
|
312
|
+
? migrated.display.showConfigCounts
|
|
313
|
+
: DEFAULT_CONFIG.display.showConfigCounts,
|
|
314
|
+
showDuration: typeof migrated.display?.showDuration === 'boolean'
|
|
315
|
+
? migrated.display.showDuration
|
|
316
|
+
: DEFAULT_CONFIG.display.showDuration,
|
|
317
|
+
showSpeed: typeof migrated.display?.showSpeed === 'boolean'
|
|
318
|
+
? migrated.display.showSpeed
|
|
319
|
+
: DEFAULT_CONFIG.display.showSpeed,
|
|
320
|
+
showTokenBreakdown: typeof migrated.display?.showTokenBreakdown === 'boolean'
|
|
321
|
+
? migrated.display.showTokenBreakdown
|
|
322
|
+
: DEFAULT_CONFIG.display.showTokenBreakdown,
|
|
323
|
+
showUsage: typeof migrated.display?.showUsage === 'boolean'
|
|
324
|
+
? migrated.display.showUsage
|
|
325
|
+
: DEFAULT_CONFIG.display.showUsage,
|
|
326
|
+
usageBarEnabled: typeof migrated.display?.usageBarEnabled === 'boolean'
|
|
327
|
+
? migrated.display.usageBarEnabled
|
|
328
|
+
: DEFAULT_CONFIG.display.usageBarEnabled,
|
|
329
|
+
showTools: typeof migrated.display?.showTools === 'boolean'
|
|
330
|
+
? migrated.display.showTools
|
|
331
|
+
: DEFAULT_CONFIG.display.showTools,
|
|
332
|
+
showAgents: typeof migrated.display?.showAgents === 'boolean'
|
|
333
|
+
? migrated.display.showAgents
|
|
334
|
+
: DEFAULT_CONFIG.display.showAgents,
|
|
335
|
+
showTodos: typeof migrated.display?.showTodos === 'boolean'
|
|
336
|
+
? migrated.display.showTodos
|
|
337
|
+
: DEFAULT_CONFIG.display.showTodos,
|
|
338
|
+
showSessionName: typeof migrated.display?.showSessionName === 'boolean'
|
|
339
|
+
? migrated.display.showSessionName
|
|
340
|
+
: DEFAULT_CONFIG.display.showSessionName,
|
|
341
|
+
autocompactBuffer: validateAutocompactBuffer(migrated.display?.autocompactBuffer)
|
|
342
|
+
? migrated.display.autocompactBuffer
|
|
343
|
+
: DEFAULT_CONFIG.display.autocompactBuffer,
|
|
344
|
+
usageThreshold: validateThreshold(migrated.display?.usageThreshold, 100),
|
|
345
|
+
sevenDayThreshold: validateThreshold(migrated.display?.sevenDayThreshold, 100),
|
|
346
|
+
environmentThreshold: validateThreshold(migrated.display?.environmentThreshold, 100),
|
|
347
|
+
customLine: typeof migrated.display?.customLine === 'string'
|
|
348
|
+
? migrated.display.customLine.slice(0, 80)
|
|
349
|
+
: DEFAULT_CONFIG.display.customLine,
|
|
350
|
+
showFrameworks: typeof migrated.display?.showFrameworks === 'boolean'
|
|
351
|
+
? migrated.display.showFrameworks
|
|
352
|
+
: DEFAULT_CONFIG.display.showFrameworks,
|
|
353
|
+
showBurnRate: typeof migrated.display?.showBurnRate === 'boolean'
|
|
354
|
+
? migrated.display.showBurnRate
|
|
355
|
+
: DEFAULT_CONFIG.display.showBurnRate,
|
|
356
|
+
showAlerts: typeof migrated.display?.showAlerts === 'boolean'
|
|
357
|
+
? migrated.display.showAlerts
|
|
358
|
+
: DEFAULT_CONFIG.display.showAlerts,
|
|
359
|
+
activityIndicator: typeof migrated.display?.activityIndicator === 'boolean'
|
|
360
|
+
? migrated.display.activityIndicator
|
|
361
|
+
: DEFAULT_CONFIG.display.activityIndicator,
|
|
362
|
+
treePrefixes: typeof migrated.display?.treePrefixes === 'boolean'
|
|
363
|
+
? migrated.display.treePrefixes
|
|
364
|
+
: DEFAULT_CONFIG.display.treePrefixes,
|
|
365
|
+
mergeToolsAgents: typeof migrated.display?.mergeToolsAgents === 'boolean'
|
|
366
|
+
? migrated.display.mergeToolsAgents
|
|
367
|
+
: DEFAULT_CONFIG.display.mergeToolsAgents,
|
|
368
|
+
barStyle: (migrated.display?.barStyle === 'classic' || migrated.display?.barStyle === 'modern')
|
|
369
|
+
? migrated.display.barStyle
|
|
370
|
+
: DEFAULT_CONFIG.display.barStyle,
|
|
371
|
+
showCost: typeof migrated.display?.showCost === 'boolean'
|
|
372
|
+
? migrated.display.showCost
|
|
373
|
+
: DEFAULT_CONFIG.display.showCost,
|
|
374
|
+
showNotifications: typeof migrated.display?.showNotifications === 'boolean'
|
|
375
|
+
? migrated.display.showNotifications
|
|
376
|
+
: DEFAULT_CONFIG.display.showNotifications,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const usage = {
|
|
380
|
+
cacheTtlSeconds: validatePositiveInt(
|
|
381
|
+
migrated.usage?.cacheTtlSeconds,
|
|
382
|
+
DEFAULT_CONFIG.usage.cacheTtlSeconds
|
|
383
|
+
),
|
|
384
|
+
failureCacheTtlSeconds: validatePositiveInt(
|
|
385
|
+
migrated.usage?.failureCacheTtlSeconds,
|
|
386
|
+
DEFAULT_CONFIG.usage.failureCacheTtlSeconds
|
|
387
|
+
),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const theme = typeof migrated.theme === 'string' ? migrated.theme : DEFAULT_CONFIG.theme;
|
|
391
|
+
|
|
392
|
+
// Start with default colors
|
|
393
|
+
const defaultColors = { ...DEFAULT_CONFIG.colors };
|
|
394
|
+
|
|
395
|
+
// Apply theme colors as base (if a valid theme is set)
|
|
396
|
+
const resolvedTheme = getTheme(theme);
|
|
397
|
+
const themeColors = resolvedTheme ? { ...resolvedTheme.colors } : defaultColors;
|
|
398
|
+
|
|
399
|
+
// User's explicit color overrides take precedence over theme
|
|
400
|
+
const colors = {
|
|
401
|
+
context: validateColorValue(migrated.colors?.context)
|
|
402
|
+
? migrated.colors.context
|
|
403
|
+
: themeColors.context,
|
|
404
|
+
usage: validateColorValue(migrated.colors?.usage)
|
|
405
|
+
? migrated.colors.usage
|
|
406
|
+
: themeColors.usage,
|
|
407
|
+
warning: validateColorValue(migrated.colors?.warning)
|
|
408
|
+
? migrated.colors.warning
|
|
409
|
+
: themeColors.warning,
|
|
410
|
+
usageWarning: validateColorValue(migrated.colors?.usageWarning)
|
|
411
|
+
? migrated.colors.usageWarning
|
|
412
|
+
: themeColors.usageWarning,
|
|
413
|
+
critical: validateColorValue(migrated.colors?.critical)
|
|
414
|
+
? migrated.colors.critical
|
|
415
|
+
: themeColors.critical,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const frameworks = {
|
|
419
|
+
agw: {
|
|
420
|
+
enabled: typeof migrated.frameworks?.agw?.enabled === 'boolean'
|
|
421
|
+
? migrated.frameworks.agw.enabled
|
|
422
|
+
: DEFAULT_CONFIG.frameworks.agw.enabled,
|
|
423
|
+
endpoint: typeof migrated.frameworks?.agw?.endpoint === 'string'
|
|
424
|
+
? migrated.frameworks.agw.endpoint
|
|
425
|
+
: DEFAULT_CONFIG.frameworks.agw.endpoint,
|
|
426
|
+
},
|
|
427
|
+
agentTeams: {
|
|
428
|
+
enabled: typeof migrated.frameworks?.agentTeams?.enabled === 'boolean'
|
|
429
|
+
? migrated.frameworks.agentTeams.enabled
|
|
430
|
+
: DEFAULT_CONFIG.frameworks.agentTeams.enabled,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
function mergeAlertThreshold(value: unknown, defaultValue: number): number {
|
|
435
|
+
if (typeof value === 'number' && value >= 0 && value <= 100) return value;
|
|
436
|
+
return defaultValue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function mergeAlertActions(userActions: Partial<AlertAction> | undefined, defaultActions: AlertAction): AlertAction {
|
|
440
|
+
return {
|
|
441
|
+
visual: typeof userActions?.visual === 'boolean' ? userActions.visual : defaultActions.visual,
|
|
442
|
+
bell: typeof userActions?.bell === 'boolean' ? userActions.bell : defaultActions.bell,
|
|
443
|
+
predict: typeof userActions?.predict === 'boolean' ? userActions.predict : defaultActions.predict,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const alerts = {
|
|
448
|
+
context: {
|
|
449
|
+
warningThreshold: mergeAlertThreshold(
|
|
450
|
+
migrated.alerts?.context?.warningThreshold,
|
|
451
|
+
DEFAULT_CONFIG.alerts.context.warningThreshold
|
|
452
|
+
),
|
|
453
|
+
criticalThreshold: mergeAlertThreshold(
|
|
454
|
+
migrated.alerts?.context?.criticalThreshold,
|
|
455
|
+
DEFAULT_CONFIG.alerts.context.criticalThreshold
|
|
456
|
+
),
|
|
457
|
+
actions: mergeAlertActions(migrated.alerts?.context?.actions, DEFAULT_CONFIG.alerts.context.actions),
|
|
458
|
+
},
|
|
459
|
+
usage5h: {
|
|
460
|
+
warningThreshold: mergeAlertThreshold(
|
|
461
|
+
migrated.alerts?.usage5h?.warningThreshold,
|
|
462
|
+
DEFAULT_CONFIG.alerts.usage5h.warningThreshold
|
|
463
|
+
),
|
|
464
|
+
criticalThreshold: mergeAlertThreshold(
|
|
465
|
+
migrated.alerts?.usage5h?.criticalThreshold,
|
|
466
|
+
DEFAULT_CONFIG.alerts.usage5h.criticalThreshold
|
|
467
|
+
),
|
|
468
|
+
actions: mergeAlertActions(migrated.alerts?.usage5h?.actions, DEFAULT_CONFIG.alerts.usage5h.actions),
|
|
469
|
+
},
|
|
470
|
+
usage7d: {
|
|
471
|
+
warningThreshold: mergeAlertThreshold(
|
|
472
|
+
migrated.alerts?.usage7d?.warningThreshold,
|
|
473
|
+
DEFAULT_CONFIG.alerts.usage7d.warningThreshold
|
|
474
|
+
),
|
|
475
|
+
actions: mergeAlertActions(migrated.alerts?.usage7d?.actions, DEFAULT_CONFIG.alerts.usage7d.actions),
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
return { lineLayout, showSeparators, pathLevels, elementOrder, gitStatus, display, theme, usage, colors, frameworks, alerts };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function loadConfig(): Promise<HudConfig> {
|
|
483
|
+
const configPath = getConfigPath();
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
if (!fs.existsSync(configPath)) {
|
|
487
|
+
return DEFAULT_CONFIG;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
491
|
+
const userConfig = JSON.parse(content) as Partial<HudConfig>;
|
|
492
|
+
return mergeConfig(userConfig);
|
|
493
|
+
} catch {
|
|
494
|
+
return DEFAULT_CONFIG;
|
|
495
|
+
}
|
|
496
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autocompact buffer percentage.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This value is applied as a percentage of Claude Code's reported
|
|
5
|
+
* context window size. The `33k/200k` example is just the 200k-window case.
|
|
6
|
+
* It is empirically derived from current Claude Code `/context` output, is
|
|
7
|
+
* not officially documented by Anthropic, and may need adjustment if users
|
|
8
|
+
* report mismatches in future Claude Code versions.
|
|
9
|
+
*/
|
|
10
|
+
export const AUTOCOMPACT_BUFFER_PERCENT = 0.165;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { StdinData } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface CostEstimate {
|
|
4
|
+
sessionCost: number; // USD
|
|
5
|
+
inputCostPer1M: number;
|
|
6
|
+
outputCostPer1M: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Pricing per 1M tokens (approximate, as of 2026)
|
|
10
|
+
const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
11
|
+
'opus': { input: 15, output: 75 },
|
|
12
|
+
'sonnet': { input: 3, output: 15 },
|
|
13
|
+
'haiku': { input: 0.25, output: 1.25 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function getModelTier(modelName: string): string {
|
|
17
|
+
const lower = modelName.toLowerCase();
|
|
18
|
+
if (lower.includes('opus')) return 'opus';
|
|
19
|
+
if (lower.includes('sonnet')) return 'sonnet';
|
|
20
|
+
if (lower.includes('haiku')) return 'haiku';
|
|
21
|
+
return 'sonnet'; // default
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function estimateCost(stdin: StdinData, _cacheDir: string): CostEstimate | null {
|
|
25
|
+
const modelName = stdin.model?.display_name || stdin.model?.id || '';
|
|
26
|
+
if (!modelName) return null;
|
|
27
|
+
|
|
28
|
+
const usage = stdin.context_window?.current_usage;
|
|
29
|
+
if (!usage) return null;
|
|
30
|
+
|
|
31
|
+
const tier = getModelTier(modelName);
|
|
32
|
+
const pricing = MODEL_PRICING[tier];
|
|
33
|
+
if (!pricing) return null;
|
|
34
|
+
|
|
35
|
+
const inputTokens = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
36
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
37
|
+
|
|
38
|
+
const inputCost = (inputTokens / 1_000_000) * pricing.input;
|
|
39
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.output;
|
|
40
|
+
const sessionCost = inputCost + outputCost;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
sessionCost,
|
|
44
|
+
inputCostPer1M: pricing.input,
|
|
45
|
+
outputCostPer1M: pricing.output,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatCost(cost: number): string {
|
|
50
|
+
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
|
51
|
+
if (cost < 1) return `$${cost.toFixed(2)}`;
|
|
52
|
+
return `$${cost.toFixed(2)}`;
|
|
53
|
+
}
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Shared debug logging utility
|
|
2
|
+
// Enable via: DEBUG=claude-hud or DEBUG=*
|
|
3
|
+
|
|
4
|
+
const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG === '*';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a namespaced debug logger
|
|
8
|
+
* @param namespace - Tag for log messages (e.g., 'config', 'usage')
|
|
9
|
+
*/
|
|
10
|
+
export function createDebug(namespace: string) {
|
|
11
|
+
return function debug(msg: string, ...args: unknown[]): void {
|
|
12
|
+
if (DEBUG) {
|
|
13
|
+
console.error(`[claude-hud:${namespace}] ${msg}`, ...args);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/extra-cmd.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
const MAX_BUFFER = 10 * 1024; // 10KB - plenty for a label
|
|
7
|
+
const MAX_LABEL_LENGTH = 50;
|
|
8
|
+
const TIMEOUT_MS = 3000;
|
|
9
|
+
|
|
10
|
+
const isDebug = process.env.DEBUG?.includes('claude-hud') ?? false;
|
|
11
|
+
|
|
12
|
+
const SHELL_ESCAPE_RE = /[\x00-\x1f\x7f`$\\]/g;
|
|
13
|
+
const CONSECUTIVE_SPACES_RE = /\s{2,}/g;
|
|
14
|
+
const LEADING_TRAILING_SPACES_RE = /^\s+|\s+$/g;
|
|
15
|
+
|
|
16
|
+
// ANSI/control sequence patterns used in sanitize()
|
|
17
|
+
const CSI_SEQUENCE_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
18
|
+
const OSC_SEQUENCE_RE = /\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g;
|
|
19
|
+
const C1_ESCAPE_RE = /\x1B[@-Z\\-_]/g;
|
|
20
|
+
const CONTROL_CHARS_RE = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
21
|
+
const BIDI_CHARS_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069\u206A-\u206F]/g;
|
|
22
|
+
|
|
23
|
+
function debug(message: string): void {
|
|
24
|
+
if (isDebug) {
|
|
25
|
+
console.error(`[claude-hud:extra-cmd] ${message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ExtraLabel {
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sanitize output to prevent terminal escape injection.
|
|
35
|
+
* Strips ANSI escapes, OSC sequences, control characters, and bidi controls.
|
|
36
|
+
*/
|
|
37
|
+
export function sanitize(input: string): string {
|
|
38
|
+
return input
|
|
39
|
+
.replace(CSI_SEQUENCE_RE, '') // CSI sequences
|
|
40
|
+
.replace(OSC_SEQUENCE_RE, '') // OSC sequences
|
|
41
|
+
.replace(C1_ESCAPE_RE, '') // 7-bit C1 / ESC Fe
|
|
42
|
+
.replace(CONTROL_CHARS_RE, '') // C0/C1 controls
|
|
43
|
+
.replace(BIDI_CHARS_RE, ''); // bidi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse --extra-cmd argument from process.argv
|
|
48
|
+
* Supports both: --extra-cmd "command" and --extra-cmd="command"
|
|
49
|
+
*/
|
|
50
|
+
export function parseExtraCmdArg(argv: string[] = process.argv): string | null {
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
|
|
54
|
+
// Handle --extra-cmd=value syntax
|
|
55
|
+
if (arg.startsWith('--extra-cmd=')) {
|
|
56
|
+
const value = arg.slice('--extra-cmd='.length);
|
|
57
|
+
if (value === '') {
|
|
58
|
+
debug('Warning: --extra-cmd value is empty, ignoring');
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle --extra-cmd value syntax
|
|
65
|
+
if (arg === '--extra-cmd') {
|
|
66
|
+
if (i + 1 >= argv.length) {
|
|
67
|
+
debug('Warning: --extra-cmd specified but no value provided');
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const value = argv[i + 1];
|
|
71
|
+
if (value === '') {
|
|
72
|
+
debug('Warning: --extra-cmd value is empty, ignoring');
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute a command and parse JSON output expecting { label: string }
|
|
84
|
+
* Returns null on any error (timeout, parse failure, missing label)
|
|
85
|
+
*
|
|
86
|
+
* SECURITY NOTE: The cmd parameter is sourced exclusively from CLI arguments
|
|
87
|
+
* (--extra-cmd) typed by the user. Since the user controls their own shell,
|
|
88
|
+
* shell injection is not a concern here - it's intentional user input.
|
|
89
|
+
*/
|
|
90
|
+
export async function runExtraCmd(cmd: string, timeout: number = TIMEOUT_MS): Promise<string | null> {
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await execAsync(cmd, {
|
|
93
|
+
timeout,
|
|
94
|
+
maxBuffer: MAX_BUFFER,
|
|
95
|
+
});
|
|
96
|
+
const data: unknown = JSON.parse(stdout.trim());
|
|
97
|
+
if (
|
|
98
|
+
typeof data === 'object' &&
|
|
99
|
+
data !== null &&
|
|
100
|
+
'label' in data &&
|
|
101
|
+
typeof (data as ExtraLabel).label === 'string'
|
|
102
|
+
) {
|
|
103
|
+
let label = sanitize((data as ExtraLabel).label);
|
|
104
|
+
if (label.length > MAX_LABEL_LENGTH) {
|
|
105
|
+
label = label.slice(0, MAX_LABEL_LENGTH - 1) + '…';
|
|
106
|
+
}
|
|
107
|
+
return label;
|
|
108
|
+
}
|
|
109
|
+
debug(`Command output missing 'label' field or invalid type: ${JSON.stringify(data)}`);
|
|
110
|
+
return null;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err instanceof Error) {
|
|
113
|
+
if (err.message.includes('TIMEOUT') || err.message.includes('killed')) {
|
|
114
|
+
debug(`Command timed out after ${timeout}ms: ${cmd}`);
|
|
115
|
+
} else if (err instanceof SyntaxError) {
|
|
116
|
+
debug(`Failed to parse JSON output: ${err.message}`);
|
|
117
|
+
} else {
|
|
118
|
+
debug(`Command failed: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
debug(`Command failed with unknown error`);
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|