claude-scope 0.2.4 → 0.3.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 +301 -29
- package/package.json +2 -1
package/dist/claude-scope.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/index.ts
|
|
@@ -143,34 +153,51 @@ var Renderer = class {
|
|
|
143
153
|
this.showErrors = options.showErrors ?? false;
|
|
144
154
|
}
|
|
145
155
|
/**
|
|
146
|
-
* Render widgets into
|
|
156
|
+
* Render widgets into multiple lines with error boundaries
|
|
147
157
|
*
|
|
148
|
-
* Widgets
|
|
149
|
-
*
|
|
158
|
+
* Widgets are grouped by their metadata.line property and rendered
|
|
159
|
+
* on separate lines. Widgets that throw errors are logged (via onError
|
|
160
|
+
* callback) and skipped, allowing other widgets to continue rendering.
|
|
150
161
|
*
|
|
151
162
|
* @param widgets - Array of widgets to render
|
|
152
163
|
* @param context - Render context with width and timestamp
|
|
153
|
-
* @returns
|
|
164
|
+
* @returns Array of rendered lines (one per line number)
|
|
154
165
|
*/
|
|
155
166
|
async render(widgets, context) {
|
|
156
|
-
const
|
|
167
|
+
const lineMap = /* @__PURE__ */ new Map();
|
|
157
168
|
for (const widget of widgets) {
|
|
158
169
|
if (!widget.isEnabled()) {
|
|
159
170
|
continue;
|
|
160
171
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
const line = widget.metadata.line ?? 0;
|
|
173
|
+
if (!lineMap.has(line)) {
|
|
174
|
+
lineMap.set(line, []);
|
|
175
|
+
}
|
|
176
|
+
lineMap.get(line).push(widget);
|
|
177
|
+
}
|
|
178
|
+
const lines = [];
|
|
179
|
+
const sortedLines = Array.from(lineMap.entries()).sort((a, b) => a[0] - b[0]);
|
|
180
|
+
for (const [, widgetsForLine] of sortedLines) {
|
|
181
|
+
const outputs = [];
|
|
182
|
+
for (const widget of widgetsForLine) {
|
|
183
|
+
try {
|
|
184
|
+
const output = await widget.render(context);
|
|
185
|
+
if (output !== null) {
|
|
186
|
+
outputs.push(output);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
this.handleError(error, widget);
|
|
190
|
+
if (this.showErrors) {
|
|
191
|
+
outputs.push(`${widget.id}:<err>`);
|
|
192
|
+
}
|
|
170
193
|
}
|
|
171
194
|
}
|
|
195
|
+
const line = outputs.join(this.separator);
|
|
196
|
+
if (line) {
|
|
197
|
+
lines.push(line);
|
|
198
|
+
}
|
|
172
199
|
}
|
|
173
|
-
return
|
|
200
|
+
return lines;
|
|
174
201
|
}
|
|
175
202
|
/**
|
|
176
203
|
* Set custom separator
|
|
@@ -193,12 +220,13 @@ var Renderer = class {
|
|
|
193
220
|
};
|
|
194
221
|
|
|
195
222
|
// src/core/widget-types.ts
|
|
196
|
-
function createWidgetMetadata(name, description, version = "1.0.0", author = "claude-scope") {
|
|
223
|
+
function createWidgetMetadata(name, description, version = "1.0.0", author = "claude-scope", line = 0) {
|
|
197
224
|
return {
|
|
198
225
|
name,
|
|
199
226
|
description,
|
|
200
227
|
version,
|
|
201
|
-
author
|
|
228
|
+
author,
|
|
229
|
+
line
|
|
202
230
|
};
|
|
203
231
|
}
|
|
204
232
|
|
|
@@ -252,7 +280,11 @@ var GitWidget = class {
|
|
|
252
280
|
id = "git";
|
|
253
281
|
metadata = createWidgetMetadata(
|
|
254
282
|
"Git Widget",
|
|
255
|
-
"Displays current git branch"
|
|
283
|
+
"Displays current git branch",
|
|
284
|
+
"1.0.0",
|
|
285
|
+
"claude-scope",
|
|
286
|
+
0
|
|
287
|
+
// First line
|
|
256
288
|
);
|
|
257
289
|
gitFactory;
|
|
258
290
|
git = null;
|
|
@@ -360,7 +392,11 @@ var ModelWidget = class extends StdinDataWidget {
|
|
|
360
392
|
id = "model";
|
|
361
393
|
metadata = createWidgetMetadata(
|
|
362
394
|
"Model",
|
|
363
|
-
"Displays the current Claude model name"
|
|
395
|
+
"Displays the current Claude model name",
|
|
396
|
+
"1.0.0",
|
|
397
|
+
"claude-scope",
|
|
398
|
+
0
|
|
399
|
+
// First line
|
|
364
400
|
);
|
|
365
401
|
renderWithData(data, context) {
|
|
366
402
|
return data.model.display_name;
|
|
@@ -424,7 +460,11 @@ var ContextWidget = class extends StdinDataWidget {
|
|
|
424
460
|
id = "context";
|
|
425
461
|
metadata = createWidgetMetadata(
|
|
426
462
|
"Context",
|
|
427
|
-
"Displays context window usage with progress bar"
|
|
463
|
+
"Displays context window usage with progress bar",
|
|
464
|
+
"1.0.0",
|
|
465
|
+
"claude-scope",
|
|
466
|
+
0
|
|
467
|
+
// First line
|
|
428
468
|
);
|
|
429
469
|
renderWithData(data, context) {
|
|
430
470
|
const { current_usage, context_window_size } = data.context_window;
|
|
@@ -442,7 +482,11 @@ var CostWidget = class extends StdinDataWidget {
|
|
|
442
482
|
id = "cost";
|
|
443
483
|
metadata = createWidgetMetadata(
|
|
444
484
|
"Cost",
|
|
445
|
-
"Displays session cost in USD"
|
|
485
|
+
"Displays session cost in USD",
|
|
486
|
+
"1.0.0",
|
|
487
|
+
"claude-scope",
|
|
488
|
+
0
|
|
489
|
+
// First line
|
|
446
490
|
);
|
|
447
491
|
renderWithData(data, context) {
|
|
448
492
|
if (!data.cost || data.cost.total_cost_usd === void 0) return null;
|
|
@@ -450,12 +494,36 @@ var CostWidget = class extends StdinDataWidget {
|
|
|
450
494
|
}
|
|
451
495
|
};
|
|
452
496
|
|
|
497
|
+
// src/widgets/lines-widget.ts
|
|
498
|
+
var LinesWidget = class extends StdinDataWidget {
|
|
499
|
+
id = "lines";
|
|
500
|
+
metadata = createWidgetMetadata(
|
|
501
|
+
"Lines",
|
|
502
|
+
"Displays lines added/removed in session",
|
|
503
|
+
"1.0.0",
|
|
504
|
+
"claude-scope",
|
|
505
|
+
0
|
|
506
|
+
// First line
|
|
507
|
+
);
|
|
508
|
+
renderWithData(data, context) {
|
|
509
|
+
const added = data.cost?.total_lines_added ?? 0;
|
|
510
|
+
const removed = data.cost?.total_lines_removed ?? 0;
|
|
511
|
+
const addedStr = colorize(`+${added}`, ANSI_COLORS.GREEN);
|
|
512
|
+
const removedStr = colorize(`-${removed}`, ANSI_COLORS.RED);
|
|
513
|
+
return `${addedStr}/${removedStr}`;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
453
517
|
// src/widgets/duration-widget.ts
|
|
454
518
|
var DurationWidget = class extends StdinDataWidget {
|
|
455
519
|
id = "duration";
|
|
456
520
|
metadata = createWidgetMetadata(
|
|
457
521
|
"Duration",
|
|
458
|
-
"Displays elapsed session time"
|
|
522
|
+
"Displays elapsed session time",
|
|
523
|
+
"1.0.0",
|
|
524
|
+
"claude-scope",
|
|
525
|
+
0
|
|
526
|
+
// First line
|
|
459
527
|
);
|
|
460
528
|
renderWithData(data, context) {
|
|
461
529
|
if (!data.cost || data.cost.total_duration_ms === void 0) return null;
|
|
@@ -468,7 +536,11 @@ var GitChangesWidget = class {
|
|
|
468
536
|
id = "git-changes";
|
|
469
537
|
metadata = createWidgetMetadata(
|
|
470
538
|
"Git Changes",
|
|
471
|
-
"Displays git diff statistics"
|
|
539
|
+
"Displays git diff statistics",
|
|
540
|
+
"1.0.0",
|
|
541
|
+
"claude-scope",
|
|
542
|
+
0
|
|
543
|
+
// First line
|
|
472
544
|
);
|
|
473
545
|
gitFactory;
|
|
474
546
|
git = null;
|
|
@@ -533,16 +605,214 @@ var GitChangesWidget = class {
|
|
|
533
605
|
}
|
|
534
606
|
};
|
|
535
607
|
|
|
608
|
+
// src/providers/config-provider.ts
|
|
609
|
+
var fs = __toESM(require("fs/promises"), 1);
|
|
610
|
+
var path = __toESM(require("path"), 1);
|
|
611
|
+
var os = __toESM(require("os"), 1);
|
|
612
|
+
var ConfigProvider = class {
|
|
613
|
+
cachedCounts;
|
|
614
|
+
lastScan = 0;
|
|
615
|
+
cacheInterval = 5e3;
|
|
616
|
+
// 5 seconds
|
|
617
|
+
/**
|
|
618
|
+
* Get config counts with hybrid caching
|
|
619
|
+
* Scans filesystem if cache is stale (>5 seconds)
|
|
620
|
+
*/
|
|
621
|
+
async getConfigs(options = {}) {
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
if (this.cachedCounts && now - this.lastScan < this.cacheInterval) {
|
|
624
|
+
return this.cachedCounts;
|
|
625
|
+
}
|
|
626
|
+
this.cachedCounts = await this.scanConfigs(options);
|
|
627
|
+
this.lastScan = now;
|
|
628
|
+
return this.cachedCounts;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Scan filesystem for Claude Code configurations
|
|
632
|
+
*/
|
|
633
|
+
async scanConfigs(options) {
|
|
634
|
+
let claudeMdCount = 0;
|
|
635
|
+
let rulesCount = 0;
|
|
636
|
+
let mcpCount = 0;
|
|
637
|
+
let hooksCount = 0;
|
|
638
|
+
const homeDir = os.homedir();
|
|
639
|
+
const claudeDir = path.join(homeDir, ".claude");
|
|
640
|
+
const cwd = options.cwd;
|
|
641
|
+
if (await this.fileExists(path.join(claudeDir, "CLAUDE.md"))) {
|
|
642
|
+
claudeMdCount++;
|
|
643
|
+
}
|
|
644
|
+
rulesCount += await this.countRulesInDir(path.join(claudeDir, "rules"));
|
|
645
|
+
const userSettings = path.join(claudeDir, "settings.json");
|
|
646
|
+
const userSettingsData = await this.readJsonFile(userSettings);
|
|
647
|
+
if (userSettingsData) {
|
|
648
|
+
mcpCount += this.countMcpServers(userSettingsData);
|
|
649
|
+
hooksCount += this.countHooks(userSettingsData);
|
|
650
|
+
}
|
|
651
|
+
const userClaudeJson = path.join(homeDir, ".claude.json");
|
|
652
|
+
const userClaudeData = await this.readJsonFile(userClaudeJson);
|
|
653
|
+
if (userClaudeData) {
|
|
654
|
+
const userMcpCount = this.countMcpServers(userClaudeData);
|
|
655
|
+
mcpCount += Math.max(0, userMcpCount - this.countMcpServers(userSettingsData || {}));
|
|
656
|
+
}
|
|
657
|
+
if (cwd) {
|
|
658
|
+
if (await this.fileExists(path.join(cwd, "CLAUDE.md"))) {
|
|
659
|
+
claudeMdCount++;
|
|
660
|
+
}
|
|
661
|
+
if (await this.fileExists(path.join(cwd, "CLAUDE.local.md"))) {
|
|
662
|
+
claudeMdCount++;
|
|
663
|
+
}
|
|
664
|
+
if (await this.fileExists(path.join(cwd, ".claude", "CLAUDE.md"))) {
|
|
665
|
+
claudeMdCount++;
|
|
666
|
+
}
|
|
667
|
+
if (await this.fileExists(path.join(cwd, ".claude", "CLAUDE.local.md"))) {
|
|
668
|
+
claudeMdCount++;
|
|
669
|
+
}
|
|
670
|
+
rulesCount += await this.countRulesInDir(path.join(cwd, ".claude", "rules"));
|
|
671
|
+
const mcpJson = path.join(cwd, ".mcp.json");
|
|
672
|
+
const mcpData = await this.readJsonFile(mcpJson);
|
|
673
|
+
if (mcpData) {
|
|
674
|
+
mcpCount += this.countMcpServers(mcpData);
|
|
675
|
+
}
|
|
676
|
+
const projectSettings = path.join(cwd, ".claude", "settings.json");
|
|
677
|
+
const projectSettingsData = await this.readJsonFile(projectSettings);
|
|
678
|
+
if (projectSettingsData) {
|
|
679
|
+
mcpCount += this.countMcpServers(projectSettingsData);
|
|
680
|
+
hooksCount += this.countHooks(projectSettingsData);
|
|
681
|
+
}
|
|
682
|
+
const localSettings = path.join(cwd, ".claude", "settings.local.json");
|
|
683
|
+
const localSettingsData = await this.readJsonFile(localSettings);
|
|
684
|
+
if (localSettingsData) {
|
|
685
|
+
mcpCount += this.countMcpServers(localSettingsData);
|
|
686
|
+
hooksCount += this.countHooks(localSettingsData);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return { claudeMdCount, rulesCount, mcpCount, hooksCount };
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Check if file exists
|
|
693
|
+
*/
|
|
694
|
+
async fileExists(filePath) {
|
|
695
|
+
try {
|
|
696
|
+
await fs.access(filePath);
|
|
697
|
+
return true;
|
|
698
|
+
} catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Read and parse JSON file
|
|
704
|
+
*/
|
|
705
|
+
async readJsonFile(filePath) {
|
|
706
|
+
try {
|
|
707
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
708
|
+
return JSON.parse(content);
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Count MCP servers in config object
|
|
715
|
+
*/
|
|
716
|
+
countMcpServers(config) {
|
|
717
|
+
if (!config || !config.mcpServers || typeof config.mcpServers !== "object") {
|
|
718
|
+
return 0;
|
|
719
|
+
}
|
|
720
|
+
return Object.keys(config.mcpServers).length;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Count hooks in config object
|
|
724
|
+
*/
|
|
725
|
+
countHooks(config) {
|
|
726
|
+
if (!config || !config.hooks || typeof config.hooks !== "object") {
|
|
727
|
+
return 0;
|
|
728
|
+
}
|
|
729
|
+
return Object.keys(config.hooks).length;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Recursively count .md files in directory
|
|
733
|
+
*/
|
|
734
|
+
async countRulesInDir(rulesDir) {
|
|
735
|
+
const exists = await this.fileExists(rulesDir);
|
|
736
|
+
if (!exists) return 0;
|
|
737
|
+
try {
|
|
738
|
+
let count = 0;
|
|
739
|
+
const entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
740
|
+
for (const entry of entries) {
|
|
741
|
+
const fullPath = path.join(rulesDir, entry.name);
|
|
742
|
+
if (entry.isDirectory()) {
|
|
743
|
+
count += await this.countRulesInDir(fullPath);
|
|
744
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
745
|
+
count++;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return count;
|
|
749
|
+
} catch {
|
|
750
|
+
return 0;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/widgets/config-count-widget.ts
|
|
756
|
+
var ConfigCountWidget = class {
|
|
757
|
+
id = "config-count";
|
|
758
|
+
metadata = createWidgetMetadata(
|
|
759
|
+
"Config Count",
|
|
760
|
+
"Displays Claude Code configuration counts",
|
|
761
|
+
"1.0.0",
|
|
762
|
+
"claude-scope",
|
|
763
|
+
1
|
|
764
|
+
// Second line
|
|
765
|
+
);
|
|
766
|
+
configProvider = new ConfigProvider();
|
|
767
|
+
configs;
|
|
768
|
+
cwd;
|
|
769
|
+
async initialize() {
|
|
770
|
+
}
|
|
771
|
+
async update(data) {
|
|
772
|
+
this.cwd = data.cwd;
|
|
773
|
+
this.configs = await this.configProvider.getConfigs({ cwd: data.cwd });
|
|
774
|
+
}
|
|
775
|
+
isEnabled() {
|
|
776
|
+
if (!this.configs) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
const { claudeMdCount, rulesCount, mcpCount, hooksCount } = this.configs;
|
|
780
|
+
return claudeMdCount > 0 || rulesCount > 0 || mcpCount > 0 || hooksCount > 0;
|
|
781
|
+
}
|
|
782
|
+
async render(context) {
|
|
783
|
+
if (!this.configs) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
const { claudeMdCount, rulesCount, mcpCount, hooksCount } = this.configs;
|
|
787
|
+
const parts = [];
|
|
788
|
+
if (claudeMdCount > 0) {
|
|
789
|
+
parts.push(`\u{1F4C4} ${claudeMdCount} CLAUDE.md`);
|
|
790
|
+
}
|
|
791
|
+
if (rulesCount > 0) {
|
|
792
|
+
parts.push(`\u{1F4DC} ${rulesCount} rules`);
|
|
793
|
+
}
|
|
794
|
+
if (mcpCount > 0) {
|
|
795
|
+
parts.push(`\u{1F50C} ${mcpCount} MCPs`);
|
|
796
|
+
}
|
|
797
|
+
if (hooksCount > 0) {
|
|
798
|
+
parts.push(`\u{1FA9D} ${hooksCount} hooks`);
|
|
799
|
+
}
|
|
800
|
+
return parts.join(" \u2502 ") || null;
|
|
801
|
+
}
|
|
802
|
+
async cleanup() {
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
536
806
|
// src/validation/result.ts
|
|
537
807
|
function success(data) {
|
|
538
808
|
return { success: true, data };
|
|
539
809
|
}
|
|
540
|
-
function failure(
|
|
541
|
-
return { success: false, error: { path, message, value } };
|
|
810
|
+
function failure(path2, message, value) {
|
|
811
|
+
return { success: false, error: { path: path2, message, value } };
|
|
542
812
|
}
|
|
543
813
|
function formatError(error) {
|
|
544
|
-
const
|
|
545
|
-
return `${
|
|
814
|
+
const path2 = error.path.length > 0 ? error.path.join(".") : "root";
|
|
815
|
+
return `${path2}: ${error.message}`;
|
|
546
816
|
}
|
|
547
817
|
|
|
548
818
|
// src/validation/validators.ts
|
|
@@ -731,9 +1001,11 @@ async function main() {
|
|
|
731
1001
|
await registry.register(new ModelWidget());
|
|
732
1002
|
await registry.register(new ContextWidget());
|
|
733
1003
|
await registry.register(new CostWidget());
|
|
1004
|
+
await registry.register(new LinesWidget());
|
|
734
1005
|
await registry.register(new DurationWidget());
|
|
735
1006
|
await registry.register(new GitWidget());
|
|
736
1007
|
await registry.register(new GitChangesWidget());
|
|
1008
|
+
await registry.register(new ConfigCountWidget());
|
|
737
1009
|
const renderer = new Renderer({
|
|
738
1010
|
separator: " \u2502 ",
|
|
739
1011
|
onError: (error, widget) => {
|
|
@@ -743,11 +1015,11 @@ async function main() {
|
|
|
743
1015
|
for (const widget of registry.getAll()) {
|
|
744
1016
|
await widget.update(stdinData);
|
|
745
1017
|
}
|
|
746
|
-
const
|
|
1018
|
+
const lines = await renderer.render(
|
|
747
1019
|
registry.getEnabledWidgets(),
|
|
748
1020
|
{ width: 80, timestamp: Date.now() }
|
|
749
1021
|
);
|
|
750
|
-
return
|
|
1022
|
+
return lines.join("\n");
|
|
751
1023
|
} catch (error) {
|
|
752
1024
|
const fallback = await tryGitFallback();
|
|
753
1025
|
return fallback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-scope",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Claude Code plugin for session status and analytics",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"c8": "^10.1.3",
|
|
33
33
|
"chai": "^6.2.2",
|
|
34
34
|
"esbuild": "^0.24.2",
|
|
35
|
+
"rimraf": "^6.1.2",
|
|
35
36
|
"tsx": "^4.19.2",
|
|
36
37
|
"typescript": "^5.7.2"
|
|
37
38
|
},
|