claude-scope 0.1.8 → 0.2.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/dist/claude-scope.cjs +764 -0
- package/package.json +9 -6
- package/dist/constants.d.ts +0 -56
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -57
- package/dist/constants.js.map +0 -1
- package/dist/core/renderer.d.ts +0 -51
- package/dist/core/renderer.d.ts.map +0 -1
- package/dist/core/renderer.js +0 -75
- package/dist/core/renderer.js.map +0 -1
- package/dist/core/types.d.ts +0 -56
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -5
- package/dist/core/types.js.map +0 -1
- package/dist/core/widget-registry.d.ts +0 -40
- package/dist/core/widget-registry.d.ts.map +0 -1
- package/dist/core/widget-registry.js +0 -75
- package/dist/core/widget-registry.js.map +0 -1
- package/dist/core/widget-types.d.ts +0 -30
- package/dist/core/widget-types.d.ts.map +0 -1
- package/dist/core/widget-types.js +0 -30
- package/dist/core/widget-types.js.map +0 -1
- package/dist/data/stdin-provider.d.ts +0 -44
- package/dist/data/stdin-provider.d.ts.map +0 -1
- package/dist/data/stdin-provider.js +0 -72
- package/dist/data/stdin-provider.js.map +0 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers/git-provider.d.ts +0 -71
- package/dist/providers/git-provider.d.ts.map +0 -1
- package/dist/providers/git-provider.js +0 -73
- package/dist/providers/git-provider.js.map +0 -1
- package/dist/schemas/stdin-schema.d.ts +0 -84
- package/dist/schemas/stdin-schema.d.ts.map +0 -1
- package/dist/schemas/stdin-schema.js +0 -48
- package/dist/schemas/stdin-schema.js.map +0 -1
- package/dist/types.d.ts +0 -31
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -8
- package/dist/types.js.map +0 -1
- package/dist/ui/utils/colors.d.ts +0 -52
- package/dist/ui/utils/colors.d.ts.map +0 -1
- package/dist/ui/utils/colors.js +0 -54
- package/dist/ui/utils/colors.js.map +0 -1
- package/dist/ui/utils/formatters.d.ts +0 -56
- package/dist/ui/utils/formatters.d.ts.map +0 -1
- package/dist/ui/utils/formatters.js +0 -114
- package/dist/ui/utils/formatters.js.map +0 -1
- package/dist/validation/combinators.d.ts +0 -10
- package/dist/validation/combinators.d.ts.map +0 -1
- package/dist/validation/combinators.js +0 -49
- package/dist/validation/combinators.js.map +0 -1
- package/dist/validation/core.d.ts +0 -30
- package/dist/validation/core.d.ts.map +0 -1
- package/dist/validation/core.js +0 -2
- package/dist/validation/core.js.map +0 -1
- package/dist/validation/index.d.ts +0 -4
- package/dist/validation/index.d.ts.map +0 -1
- package/dist/validation/index.js +0 -4
- package/dist/validation/index.js.map +0 -1
- package/dist/validation/result.d.ts +0 -5
- package/dist/validation/result.d.ts.map +0 -1
- package/dist/validation/result.js +0 -11
- package/dist/validation/result.js.map +0 -1
- package/dist/validation/validators.d.ts +0 -7
- package/dist/validation/validators.d.ts.map +0 -1
- package/dist/validation/validators.js +0 -41
- package/dist/validation/validators.js.map +0 -1
- package/dist/widgets/context-widget.d.ts +0 -13
- package/dist/widgets/context-widget.d.ts.map +0 -1
- package/dist/widgets/context-widget.js +0 -31
- package/dist/widgets/context-widget.js.map +0 -1
- package/dist/widgets/core/stdin-data-widget.d.ts +0 -93
- package/dist/widgets/core/stdin-data-widget.d.ts.map +0 -1
- package/dist/widgets/core/stdin-data-widget.js +0 -84
- package/dist/widgets/core/stdin-data-widget.js.map +0 -1
- package/dist/widgets/cost-widget.d.ts +0 -13
- package/dist/widgets/cost-widget.d.ts.map +0 -1
- package/dist/widgets/cost-widget.js +0 -18
- package/dist/widgets/cost-widget.js.map +0 -1
- package/dist/widgets/duration-widget.d.ts +0 -13
- package/dist/widgets/duration-widget.d.ts.map +0 -1
- package/dist/widgets/duration-widget.js +0 -18
- package/dist/widgets/duration-widget.js.map +0 -1
- package/dist/widgets/git/git-changes-widget.d.ts +0 -38
- package/dist/widgets/git/git-changes-widget.d.ts.map +0 -1
- package/dist/widgets/git/git-changes-widget.js +0 -91
- package/dist/widgets/git/git-changes-widget.js.map +0 -1
- package/dist/widgets/git/git-widget.d.ts +0 -37
- package/dist/widgets/git/git-widget.d.ts.map +0 -1
- package/dist/widgets/git/git-widget.js +0 -67
- package/dist/widgets/git/git-widget.js.map +0 -1
- package/dist/widgets/model-widget.d.ts +0 -13
- package/dist/widgets/model-widget.d.ts.map +0 -1
- package/dist/widgets/model-widget.js +0 -15
- package/dist/widgets/model-widget.js.map +0 -1
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
main: () => main
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/core/widget-registry.ts
|
|
29
|
+
var WidgetRegistry = class {
|
|
30
|
+
widgets = /* @__PURE__ */ new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Register a widget
|
|
33
|
+
*/
|
|
34
|
+
async register(widget, context) {
|
|
35
|
+
if (this.widgets.has(widget.id)) {
|
|
36
|
+
throw new Error(`Widget with id '${widget.id}' already registered`);
|
|
37
|
+
}
|
|
38
|
+
if (context) {
|
|
39
|
+
await widget.initialize(context);
|
|
40
|
+
}
|
|
41
|
+
this.widgets.set(widget.id, widget);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a widget
|
|
45
|
+
*/
|
|
46
|
+
async unregister(id) {
|
|
47
|
+
const widget = this.widgets.get(id);
|
|
48
|
+
if (!widget) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (widget.cleanup) {
|
|
53
|
+
await widget.cleanup();
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
this.widgets.delete(id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get a widget by id
|
|
61
|
+
*/
|
|
62
|
+
get(id) {
|
|
63
|
+
return this.widgets.get(id);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if widget is registered
|
|
67
|
+
*/
|
|
68
|
+
has(id) {
|
|
69
|
+
return this.widgets.has(id);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get all registered widgets
|
|
73
|
+
*/
|
|
74
|
+
getAll() {
|
|
75
|
+
return Array.from(this.widgets.values());
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get only enabled widgets
|
|
79
|
+
*/
|
|
80
|
+
getEnabledWidgets() {
|
|
81
|
+
return this.getAll().filter((w) => w.isEnabled());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clear all widgets
|
|
85
|
+
*/
|
|
86
|
+
async clear() {
|
|
87
|
+
for (const widget of this.widgets.values()) {
|
|
88
|
+
if (widget.cleanup) {
|
|
89
|
+
await widget.cleanup();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.widgets.clear();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/constants.ts
|
|
97
|
+
var TIME = {
|
|
98
|
+
/** Milliseconds per second */
|
|
99
|
+
MS_PER_SECOND: 1e3,
|
|
100
|
+
/** Seconds per minute */
|
|
101
|
+
SECONDS_PER_MINUTE: 60,
|
|
102
|
+
/** Seconds per hour */
|
|
103
|
+
SECONDS_PER_HOUR: 3600
|
|
104
|
+
};
|
|
105
|
+
var COST_THRESHOLDS = {
|
|
106
|
+
/** Below this value, show 4 decimal places ($0.0012) */
|
|
107
|
+
SMALL: 0.01,
|
|
108
|
+
/** Above this value, show no decimal places ($123) */
|
|
109
|
+
LARGE: 100
|
|
110
|
+
};
|
|
111
|
+
var CONTEXT_THRESHOLDS = {
|
|
112
|
+
/** Below this: green (low usage) */
|
|
113
|
+
LOW_MEDIUM: 50,
|
|
114
|
+
/** Below this: yellow (medium usage), above: red (high usage) */
|
|
115
|
+
MEDIUM_HIGH: 80
|
|
116
|
+
};
|
|
117
|
+
var DEFAULTS = {
|
|
118
|
+
/** Default separator between widgets */
|
|
119
|
+
SEPARATOR: " ",
|
|
120
|
+
/** Default width for progress bars in characters */
|
|
121
|
+
PROGRESS_BAR_WIDTH: 20
|
|
122
|
+
};
|
|
123
|
+
var ANSI_COLORS = {
|
|
124
|
+
/** Green color */
|
|
125
|
+
GREEN: "\x1B[32m",
|
|
126
|
+
/** Yellow color */
|
|
127
|
+
YELLOW: "\x1B[33m",
|
|
128
|
+
/** Red color */
|
|
129
|
+
RED: "\x1B[31m",
|
|
130
|
+
/** Reset color */
|
|
131
|
+
RESET: "\x1B[0m"
|
|
132
|
+
};
|
|
133
|
+
var DEFAULT_PROGRESS_BAR_WIDTH = DEFAULTS.PROGRESS_BAR_WIDTH;
|
|
134
|
+
|
|
135
|
+
// src/core/renderer.ts
|
|
136
|
+
var Renderer = class {
|
|
137
|
+
separator;
|
|
138
|
+
onError;
|
|
139
|
+
showErrors;
|
|
140
|
+
constructor(options = {}) {
|
|
141
|
+
this.separator = options.separator ?? DEFAULTS.SEPARATOR;
|
|
142
|
+
this.onError = options.onError;
|
|
143
|
+
this.showErrors = options.showErrors ?? false;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Render widgets into a single line with error boundaries
|
|
147
|
+
*
|
|
148
|
+
* Widgets that throw errors are logged (via onError callback) and skipped,
|
|
149
|
+
* allowing other widgets to continue rendering.
|
|
150
|
+
*
|
|
151
|
+
* @param widgets - Array of widgets to render
|
|
152
|
+
* @param context - Render context with width and timestamp
|
|
153
|
+
* @returns Combined widget outputs separated by separator
|
|
154
|
+
*/
|
|
155
|
+
async render(widgets, context) {
|
|
156
|
+
const outputs = [];
|
|
157
|
+
for (const widget of widgets) {
|
|
158
|
+
if (!widget.isEnabled()) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const output = await widget.render(context);
|
|
163
|
+
if (output !== null) {
|
|
164
|
+
outputs.push(output);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.handleError(error, widget);
|
|
168
|
+
if (this.showErrors) {
|
|
169
|
+
outputs.push(`${widget.id}:<err>`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return outputs.join(this.separator);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Set custom separator
|
|
177
|
+
*/
|
|
178
|
+
setSeparator(separator) {
|
|
179
|
+
this.separator = separator;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Handle widget render errors
|
|
183
|
+
*
|
|
184
|
+
* Calls the onError callback if provided, otherwise logs to console.warn
|
|
185
|
+
*/
|
|
186
|
+
handleError(error, widget) {
|
|
187
|
+
if (this.onError) {
|
|
188
|
+
this.onError(error, widget);
|
|
189
|
+
} else {
|
|
190
|
+
console.warn(`[Widget ${widget.id}] ${error.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/core/widget-types.ts
|
|
196
|
+
function createWidgetMetadata(name, description, version = "1.0.0", author = "claude-scope") {
|
|
197
|
+
return {
|
|
198
|
+
name,
|
|
199
|
+
description,
|
|
200
|
+
version,
|
|
201
|
+
author
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/providers/git-provider.ts
|
|
206
|
+
var import_node_child_process = require("node:child_process");
|
|
207
|
+
var import_node_util = require("node:util");
|
|
208
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
209
|
+
var NativeGit = class {
|
|
210
|
+
cwd;
|
|
211
|
+
constructor(cwd) {
|
|
212
|
+
this.cwd = cwd;
|
|
213
|
+
}
|
|
214
|
+
async status() {
|
|
215
|
+
try {
|
|
216
|
+
const { stdout } = await execFileAsync("git", ["status", "--branch", "--short"], {
|
|
217
|
+
cwd: this.cwd
|
|
218
|
+
});
|
|
219
|
+
const match = stdout.match(/^##\s+(\S+)/m);
|
|
220
|
+
const current = match ? match[1] : null;
|
|
221
|
+
return { current };
|
|
222
|
+
} catch {
|
|
223
|
+
return { current: null };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async diffSummary(options) {
|
|
227
|
+
const args = ["diff", "--shortstat"];
|
|
228
|
+
if (options) {
|
|
229
|
+
args.push(...options);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
233
|
+
cwd: this.cwd
|
|
234
|
+
});
|
|
235
|
+
const insertionMatch = stdout.match(/(\d+)\s+insertion/);
|
|
236
|
+
const deletionMatch = stdout.match(/(\d+)\s+deletion/);
|
|
237
|
+
const insertions = insertionMatch ? parseInt(insertionMatch[1], 10) : 0;
|
|
238
|
+
const deletions = deletionMatch ? parseInt(deletionMatch[1], 10) : 0;
|
|
239
|
+
const files = insertions > 0 || deletions > 0 ? [{ file: "(total)", insertions, deletions }] : [];
|
|
240
|
+
return { files };
|
|
241
|
+
} catch {
|
|
242
|
+
return { files: [] };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function createGit(cwd) {
|
|
247
|
+
return new NativeGit(cwd);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/widgets/git/git-widget.ts
|
|
251
|
+
var GitWidget = class {
|
|
252
|
+
id = "git";
|
|
253
|
+
metadata = createWidgetMetadata(
|
|
254
|
+
"Git Widget",
|
|
255
|
+
"Displays current git branch"
|
|
256
|
+
);
|
|
257
|
+
gitFactory;
|
|
258
|
+
git = null;
|
|
259
|
+
enabled = true;
|
|
260
|
+
cwd = null;
|
|
261
|
+
/**
|
|
262
|
+
* @param gitFactory - Optional factory function for creating IGit instances
|
|
263
|
+
* If not provided, uses default createGit (production)
|
|
264
|
+
* Tests can inject MockGit factory here
|
|
265
|
+
*/
|
|
266
|
+
constructor(gitFactory) {
|
|
267
|
+
this.gitFactory = gitFactory || createGit;
|
|
268
|
+
}
|
|
269
|
+
async initialize(context) {
|
|
270
|
+
this.enabled = context.config?.enabled !== false;
|
|
271
|
+
}
|
|
272
|
+
async render(context) {
|
|
273
|
+
if (!this.enabled || !this.git || !this.cwd) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const status = await this.git.status();
|
|
278
|
+
const branch = status.current || null;
|
|
279
|
+
if (!branch) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return ` ${branch}`;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async update(data) {
|
|
288
|
+
if (data.cwd !== this.cwd) {
|
|
289
|
+
this.cwd = data.cwd;
|
|
290
|
+
this.git = this.gitFactory(data.cwd);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
isEnabled() {
|
|
294
|
+
return this.enabled;
|
|
295
|
+
}
|
|
296
|
+
async cleanup() {
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/widgets/core/stdin-data-widget.ts
|
|
301
|
+
var StdinDataWidget = class {
|
|
302
|
+
/**
|
|
303
|
+
* Stored stdin data from last update
|
|
304
|
+
*/
|
|
305
|
+
data = null;
|
|
306
|
+
/**
|
|
307
|
+
* Widget enabled state
|
|
308
|
+
*/
|
|
309
|
+
enabled = true;
|
|
310
|
+
/**
|
|
311
|
+
* Initialize widget with context
|
|
312
|
+
* @param context - Widget initialization context
|
|
313
|
+
*/
|
|
314
|
+
async initialize(context) {
|
|
315
|
+
this.enabled = context.config?.enabled !== false;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Update widget with new stdin data
|
|
319
|
+
* @param data - Stdin data from Claude Code
|
|
320
|
+
*/
|
|
321
|
+
async update(data) {
|
|
322
|
+
this.data = data;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get stored stdin data
|
|
326
|
+
* @returns Stored stdin data
|
|
327
|
+
* @throws Error if data has not been initialized (update not called)
|
|
328
|
+
*/
|
|
329
|
+
getData() {
|
|
330
|
+
if (!this.data) {
|
|
331
|
+
throw new Error(`Widget ${this.id} data not initialized. Call update() first.`);
|
|
332
|
+
}
|
|
333
|
+
return this.data;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check if widget is enabled
|
|
337
|
+
* @returns true if widget should render
|
|
338
|
+
*/
|
|
339
|
+
isEnabled() {
|
|
340
|
+
return this.enabled;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Template method - final, subclasses implement renderWithData()
|
|
344
|
+
*
|
|
345
|
+
* Handles null data checks and calls renderWithData() hook.
|
|
346
|
+
*
|
|
347
|
+
* @param context - Render context
|
|
348
|
+
* @returns Rendered string, or null if widget should not display
|
|
349
|
+
*/
|
|
350
|
+
async render(context) {
|
|
351
|
+
if (!this.data || !this.enabled) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return this.renderWithData(this.data, context);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// src/widgets/model-widget.ts
|
|
359
|
+
var ModelWidget = class extends StdinDataWidget {
|
|
360
|
+
id = "model";
|
|
361
|
+
metadata = createWidgetMetadata(
|
|
362
|
+
"Model",
|
|
363
|
+
"Displays the current Claude model name"
|
|
364
|
+
);
|
|
365
|
+
renderWithData(data, context) {
|
|
366
|
+
return data.model.display_name;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/ui/utils/formatters.ts
|
|
371
|
+
function formatDuration(ms) {
|
|
372
|
+
if (ms <= 0) return "0s";
|
|
373
|
+
const seconds = Math.floor(ms / TIME.MS_PER_SECOND);
|
|
374
|
+
const hours = Math.floor(seconds / TIME.SECONDS_PER_HOUR);
|
|
375
|
+
const minutes = Math.floor(seconds % TIME.SECONDS_PER_HOUR / TIME.SECONDS_PER_MINUTE);
|
|
376
|
+
const secs = seconds % TIME.SECONDS_PER_MINUTE;
|
|
377
|
+
const parts = [];
|
|
378
|
+
if (hours > 0) {
|
|
379
|
+
parts.push(`${hours}h`);
|
|
380
|
+
parts.push(`${minutes}m`);
|
|
381
|
+
parts.push(`${secs}s`);
|
|
382
|
+
} else if (minutes > 0) {
|
|
383
|
+
parts.push(`${minutes}m`);
|
|
384
|
+
parts.push(`${secs}s`);
|
|
385
|
+
} else {
|
|
386
|
+
parts.push(`${secs}s`);
|
|
387
|
+
}
|
|
388
|
+
return parts.join(" ");
|
|
389
|
+
}
|
|
390
|
+
function formatCostUSD(usd) {
|
|
391
|
+
const absUsd = Math.abs(usd);
|
|
392
|
+
if (usd < 0) {
|
|
393
|
+
return `$${usd.toFixed(2)}`;
|
|
394
|
+
} else if (absUsd < COST_THRESHOLDS.SMALL) {
|
|
395
|
+
return `$${usd.toFixed(4)}`;
|
|
396
|
+
} else if (absUsd < COST_THRESHOLDS.LARGE) {
|
|
397
|
+
return `$${usd.toFixed(2)}`;
|
|
398
|
+
} else {
|
|
399
|
+
return `$${Math.floor(usd).toFixed(0)}`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function progressBar(percent, width = DEFAULTS.PROGRESS_BAR_WIDTH) {
|
|
403
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
404
|
+
const filled = Math.round(clampedPercent / 100 * width);
|
|
405
|
+
const empty = width - filled;
|
|
406
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
407
|
+
}
|
|
408
|
+
function getContextColor(percent) {
|
|
409
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
410
|
+
if (clampedPercent < CONTEXT_THRESHOLDS.LOW_MEDIUM) {
|
|
411
|
+
return ANSI_COLORS.GREEN;
|
|
412
|
+
} else if (clampedPercent < CONTEXT_THRESHOLDS.MEDIUM_HIGH) {
|
|
413
|
+
return ANSI_COLORS.YELLOW;
|
|
414
|
+
} else {
|
|
415
|
+
return ANSI_COLORS.RED;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function colorize(text, color) {
|
|
419
|
+
return `${color}${text}${ANSI_COLORS.RESET}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/widgets/context-widget.ts
|
|
423
|
+
var ContextWidget = class extends StdinDataWidget {
|
|
424
|
+
id = "context";
|
|
425
|
+
metadata = createWidgetMetadata(
|
|
426
|
+
"Context",
|
|
427
|
+
"Displays context window usage with progress bar"
|
|
428
|
+
);
|
|
429
|
+
renderWithData(data, context) {
|
|
430
|
+
const { current_usage, context_window_size } = data.context_window;
|
|
431
|
+
if (!current_usage) return null;
|
|
432
|
+
const used = current_usage.input_tokens + current_usage.cache_creation_input_tokens + current_usage.output_tokens;
|
|
433
|
+
const percent = Math.round(used / context_window_size * 100);
|
|
434
|
+
const bar = progressBar(percent, DEFAULTS.PROGRESS_BAR_WIDTH);
|
|
435
|
+
const color = getContextColor(percent);
|
|
436
|
+
return colorize(`[${bar}] ${percent}%`, color);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/widgets/cost-widget.ts
|
|
441
|
+
var CostWidget = class extends StdinDataWidget {
|
|
442
|
+
id = "cost";
|
|
443
|
+
metadata = createWidgetMetadata(
|
|
444
|
+
"Cost",
|
|
445
|
+
"Displays session cost in USD"
|
|
446
|
+
);
|
|
447
|
+
renderWithData(data, context) {
|
|
448
|
+
if (!data.cost || data.cost.total_cost_usd === void 0) return null;
|
|
449
|
+
return formatCostUSD(data.cost.total_cost_usd);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/widgets/duration-widget.ts
|
|
454
|
+
var DurationWidget = class extends StdinDataWidget {
|
|
455
|
+
id = "duration";
|
|
456
|
+
metadata = createWidgetMetadata(
|
|
457
|
+
"Duration",
|
|
458
|
+
"Displays elapsed session time"
|
|
459
|
+
);
|
|
460
|
+
renderWithData(data, context) {
|
|
461
|
+
if (!data.cost || data.cost.total_duration_ms === void 0) return null;
|
|
462
|
+
return formatDuration(data.cost.total_duration_ms);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/widgets/git/git-changes-widget.ts
|
|
467
|
+
var GitChangesWidget = class {
|
|
468
|
+
id = "git-changes";
|
|
469
|
+
metadata = createWidgetMetadata(
|
|
470
|
+
"Git Changes",
|
|
471
|
+
"Displays git diff statistics"
|
|
472
|
+
);
|
|
473
|
+
gitFactory;
|
|
474
|
+
git = null;
|
|
475
|
+
enabled = true;
|
|
476
|
+
cwd = null;
|
|
477
|
+
/**
|
|
478
|
+
* @param gitFactory - Optional factory function for creating IGit instances
|
|
479
|
+
* If not provided, uses default createGit (production)
|
|
480
|
+
* Tests can inject MockGit factory here
|
|
481
|
+
*/
|
|
482
|
+
constructor(gitFactory) {
|
|
483
|
+
this.gitFactory = gitFactory || createGit;
|
|
484
|
+
}
|
|
485
|
+
async initialize(context) {
|
|
486
|
+
this.enabled = context.config?.enabled !== false;
|
|
487
|
+
}
|
|
488
|
+
async update(data) {
|
|
489
|
+
if (data.cwd !== this.cwd) {
|
|
490
|
+
this.cwd = data.cwd;
|
|
491
|
+
this.git = this.gitFactory(data.cwd);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async render(context) {
|
|
495
|
+
if (!this.enabled || !this.git || !this.cwd) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
let changes;
|
|
499
|
+
try {
|
|
500
|
+
const summary = await this.git.diffSummary(["--shortstat"]);
|
|
501
|
+
let insertions = 0;
|
|
502
|
+
let deletions = 0;
|
|
503
|
+
if (summary.files && summary.files.length > 0) {
|
|
504
|
+
for (const file of summary.files) {
|
|
505
|
+
if (typeof file.insertions === "number") {
|
|
506
|
+
insertions += file.insertions;
|
|
507
|
+
}
|
|
508
|
+
if (typeof file.deletions === "number") {
|
|
509
|
+
deletions += file.deletions;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (insertions === 0 && deletions === 0) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
changes = { insertions, deletions };
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
if (!changes) return null;
|
|
521
|
+
if (changes.insertions === 0 && changes.deletions === 0) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const parts = [];
|
|
525
|
+
if (changes.insertions > 0) parts.push(`+${changes.insertions}`);
|
|
526
|
+
if (changes.deletions > 0) parts.push(`-${changes.deletions}`);
|
|
527
|
+
return parts.join(",");
|
|
528
|
+
}
|
|
529
|
+
isEnabled() {
|
|
530
|
+
return this.enabled;
|
|
531
|
+
}
|
|
532
|
+
async cleanup() {
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/validation/result.ts
|
|
537
|
+
function success(data) {
|
|
538
|
+
return { success: true, data };
|
|
539
|
+
}
|
|
540
|
+
function failure(path, message, value) {
|
|
541
|
+
return { success: false, error: { path, message, value } };
|
|
542
|
+
}
|
|
543
|
+
function formatError(error) {
|
|
544
|
+
const path = error.path.length > 0 ? error.path.join(".") : "root";
|
|
545
|
+
return `${path}: ${error.message}`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/validation/validators.ts
|
|
549
|
+
function string() {
|
|
550
|
+
return {
|
|
551
|
+
validate(value) {
|
|
552
|
+
if (typeof value === "string") return success(value);
|
|
553
|
+
return failure([], "Expected string", value);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function number() {
|
|
558
|
+
return {
|
|
559
|
+
validate(value) {
|
|
560
|
+
if (typeof value === "number" && !Number.isNaN(value)) return success(value);
|
|
561
|
+
return failure([], "Expected number", value);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function literal(expected) {
|
|
566
|
+
return {
|
|
567
|
+
validate(value) {
|
|
568
|
+
if (value === expected) return success(expected);
|
|
569
|
+
return failure([], `Expected '${expected}'`, value);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/validation/combinators.ts
|
|
575
|
+
function object(shape) {
|
|
576
|
+
return {
|
|
577
|
+
validate(value) {
|
|
578
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
579
|
+
return failure([], "Expected object", value);
|
|
580
|
+
}
|
|
581
|
+
const result = {};
|
|
582
|
+
for (const [key, validator] of Object.entries(shape)) {
|
|
583
|
+
const fieldValue = value[key];
|
|
584
|
+
const validationResult = validator.validate(fieldValue);
|
|
585
|
+
if (!validationResult.success) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: { ...validationResult.error, path: [key, ...validationResult.error.path] }
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
result[key] = validationResult.data;
|
|
592
|
+
}
|
|
593
|
+
return success(result);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function optional(validator) {
|
|
598
|
+
return {
|
|
599
|
+
validate(value) {
|
|
600
|
+
if (value === void 0) return success(void 0);
|
|
601
|
+
return validator.validate(value);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function nullable(validator) {
|
|
606
|
+
return {
|
|
607
|
+
validate(value) {
|
|
608
|
+
if (value === null) return success(null);
|
|
609
|
+
return validator.validate(value);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/schemas/stdin-schema.ts
|
|
615
|
+
var ContextUsageSchema = object({
|
|
616
|
+
input_tokens: number(),
|
|
617
|
+
output_tokens: number(),
|
|
618
|
+
cache_creation_input_tokens: number(),
|
|
619
|
+
cache_read_input_tokens: number()
|
|
620
|
+
});
|
|
621
|
+
var CostInfoSchema = object({
|
|
622
|
+
total_cost_usd: optional(number()),
|
|
623
|
+
total_duration_ms: optional(number()),
|
|
624
|
+
total_api_duration_ms: optional(number()),
|
|
625
|
+
total_lines_added: optional(number()),
|
|
626
|
+
total_lines_removed: optional(number())
|
|
627
|
+
});
|
|
628
|
+
var ContextWindowSchema = object({
|
|
629
|
+
total_input_tokens: number(),
|
|
630
|
+
total_output_tokens: number(),
|
|
631
|
+
context_window_size: number(),
|
|
632
|
+
current_usage: nullable(ContextUsageSchema)
|
|
633
|
+
});
|
|
634
|
+
var ModelInfoSchema = object({
|
|
635
|
+
id: string(),
|
|
636
|
+
display_name: string()
|
|
637
|
+
});
|
|
638
|
+
var WorkspaceSchema = object({
|
|
639
|
+
current_dir: string(),
|
|
640
|
+
project_dir: string()
|
|
641
|
+
});
|
|
642
|
+
var OutputStyleSchema = object({
|
|
643
|
+
name: string()
|
|
644
|
+
});
|
|
645
|
+
var StdinDataSchema = object({
|
|
646
|
+
hook_event_name: literal("Status"),
|
|
647
|
+
session_id: string(),
|
|
648
|
+
transcript_path: string(),
|
|
649
|
+
cwd: string(),
|
|
650
|
+
model: ModelInfoSchema,
|
|
651
|
+
workspace: WorkspaceSchema,
|
|
652
|
+
version: string(),
|
|
653
|
+
output_style: OutputStyleSchema,
|
|
654
|
+
cost: optional(CostInfoSchema),
|
|
655
|
+
context_window: ContextWindowSchema
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// src/data/stdin-provider.ts
|
|
659
|
+
var StdinParseError = class extends Error {
|
|
660
|
+
constructor(message) {
|
|
661
|
+
super(message);
|
|
662
|
+
this.name = "StdinParseError";
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var StdinValidationError = class extends Error {
|
|
666
|
+
constructor(message) {
|
|
667
|
+
super(message);
|
|
668
|
+
this.name = "StdinValidationError";
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var StdinProvider = class {
|
|
672
|
+
/**
|
|
673
|
+
* Parse and validate JSON string from stdin
|
|
674
|
+
* @param input JSON string to parse
|
|
675
|
+
* @returns Validated StdinData object
|
|
676
|
+
* @throws StdinParseError if JSON is malformed
|
|
677
|
+
* @throws StdinValidationError if data doesn't match schema
|
|
678
|
+
*/
|
|
679
|
+
async parse(input) {
|
|
680
|
+
if (!input || input.trim().length === 0) {
|
|
681
|
+
throw new StdinParseError("stdin data is empty");
|
|
682
|
+
}
|
|
683
|
+
let data;
|
|
684
|
+
try {
|
|
685
|
+
data = JSON.parse(input);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
throw new StdinParseError(`Invalid JSON: ${error.message}`);
|
|
688
|
+
}
|
|
689
|
+
const result = StdinDataSchema.validate(data);
|
|
690
|
+
if (!result.success) {
|
|
691
|
+
throw new StdinValidationError(
|
|
692
|
+
`Validation failed: ${formatError(result.error)}`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return result.data;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Safe parse that returns result instead of throwing
|
|
699
|
+
* Useful for testing and optional validation
|
|
700
|
+
* @param input JSON string to parse
|
|
701
|
+
* @returns Result object with success flag
|
|
702
|
+
*/
|
|
703
|
+
async safeParse(input) {
|
|
704
|
+
try {
|
|
705
|
+
const data = await this.parse(input);
|
|
706
|
+
return { success: true, data };
|
|
707
|
+
} catch (error) {
|
|
708
|
+
return { success: false, error: error.message };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// src/index.ts
|
|
714
|
+
async function readStdin() {
|
|
715
|
+
const chunks = [];
|
|
716
|
+
for await (const chunk of process.stdin) {
|
|
717
|
+
chunks.push(chunk);
|
|
718
|
+
}
|
|
719
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
720
|
+
}
|
|
721
|
+
async function main() {
|
|
722
|
+
try {
|
|
723
|
+
const stdin = await readStdin();
|
|
724
|
+
if (!stdin || stdin.trim().length === 0) {
|
|
725
|
+
return "";
|
|
726
|
+
}
|
|
727
|
+
const provider = new StdinProvider();
|
|
728
|
+
const stdinData = await provider.parse(stdin);
|
|
729
|
+
const registry = new WidgetRegistry();
|
|
730
|
+
await registry.register(new ModelWidget());
|
|
731
|
+
await registry.register(new ContextWidget());
|
|
732
|
+
await registry.register(new CostWidget());
|
|
733
|
+
await registry.register(new DurationWidget());
|
|
734
|
+
await registry.register(new GitWidget());
|
|
735
|
+
await registry.register(new GitChangesWidget());
|
|
736
|
+
const renderer = new Renderer({
|
|
737
|
+
separator: " \u2502 ",
|
|
738
|
+
onError: (error, widget) => {
|
|
739
|
+
},
|
|
740
|
+
showErrors: false
|
|
741
|
+
});
|
|
742
|
+
for (const widget of registry.getAll()) {
|
|
743
|
+
await widget.update(stdinData);
|
|
744
|
+
}
|
|
745
|
+
const output = await renderer.render(
|
|
746
|
+
registry.getEnabledWidgets(),
|
|
747
|
+
{ width: 80, timestamp: Date.now() }
|
|
748
|
+
);
|
|
749
|
+
return output || "";
|
|
750
|
+
} catch (error) {
|
|
751
|
+
return "";
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
main().then((output) => {
|
|
755
|
+
if (output) {
|
|
756
|
+
console.log(output);
|
|
757
|
+
}
|
|
758
|
+
}).catch(() => {
|
|
759
|
+
process.exit(0);
|
|
760
|
+
});
|
|
761
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
762
|
+
0 && (module.exports = {
|
|
763
|
+
main
|
|
764
|
+
});
|