claude-scope 0.2.5 → 0.3.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.
Files changed (2) hide show
  1. package/dist/claude-scope.cjs +324 -49
  2. package/package.json +2 -1
@@ -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
@@ -108,12 +118,6 @@ var COST_THRESHOLDS = {
108
118
  /** Above this value, show no decimal places ($123) */
109
119
  LARGE: 100
110
120
  };
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
121
  var DEFAULTS = {
118
122
  /** Default separator between widgets */
119
123
  SEPARATOR: " ",
@@ -143,34 +147,51 @@ var Renderer = class {
143
147
  this.showErrors = options.showErrors ?? false;
144
148
  }
145
149
  /**
146
- * Render widgets into a single line with error boundaries
150
+ * Render widgets into multiple lines with error boundaries
147
151
  *
148
- * Widgets that throw errors are logged (via onError callback) and skipped,
149
- * allowing other widgets to continue rendering.
152
+ * Widgets are grouped by their metadata.line property and rendered
153
+ * on separate lines. Widgets that throw errors are logged (via onError
154
+ * callback) and skipped, allowing other widgets to continue rendering.
150
155
  *
151
156
  * @param widgets - Array of widgets to render
152
157
  * @param context - Render context with width and timestamp
153
- * @returns Combined widget outputs separated by separator
158
+ * @returns Array of rendered lines (one per line number)
154
159
  */
155
160
  async render(widgets, context) {
156
- const outputs = [];
161
+ const lineMap = /* @__PURE__ */ new Map();
157
162
  for (const widget of widgets) {
158
163
  if (!widget.isEnabled()) {
159
164
  continue;
160
165
  }
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>`);
166
+ const line = widget.metadata.line ?? 0;
167
+ if (!lineMap.has(line)) {
168
+ lineMap.set(line, []);
169
+ }
170
+ lineMap.get(line).push(widget);
171
+ }
172
+ const lines = [];
173
+ const sortedLines = Array.from(lineMap.entries()).sort((a, b) => a[0] - b[0]);
174
+ for (const [, widgetsForLine] of sortedLines) {
175
+ const outputs = [];
176
+ for (const widget of widgetsForLine) {
177
+ try {
178
+ const output = await widget.render(context);
179
+ if (output !== null) {
180
+ outputs.push(output);
181
+ }
182
+ } catch (error) {
183
+ this.handleError(error, widget);
184
+ if (this.showErrors) {
185
+ outputs.push(`${widget.id}:<err>`);
186
+ }
170
187
  }
171
188
  }
189
+ const line = outputs.join(this.separator);
190
+ if (line) {
191
+ lines.push(line);
192
+ }
172
193
  }
173
- return outputs.join(this.separator);
194
+ return lines;
174
195
  }
175
196
  /**
176
197
  * Set custom separator
@@ -193,12 +214,13 @@ var Renderer = class {
193
214
  };
194
215
 
195
216
  // src/core/widget-types.ts
196
- function createWidgetMetadata(name, description, version = "1.0.0", author = "claude-scope") {
217
+ function createWidgetMetadata(name, description, version = "1.0.0", author = "claude-scope", line = 0) {
197
218
  return {
198
219
  name,
199
220
  description,
200
221
  version,
201
- author
222
+ author,
223
+ line
202
224
  };
203
225
  }
204
226
 
@@ -252,7 +274,11 @@ var GitWidget = class {
252
274
  id = "git";
253
275
  metadata = createWidgetMetadata(
254
276
  "Git Widget",
255
- "Displays current git branch"
277
+ "Displays current git branch",
278
+ "1.0.0",
279
+ "claude-scope",
280
+ 0
281
+ // First line
256
282
  );
257
283
  gitFactory;
258
284
  git = null;
@@ -360,7 +386,11 @@ var ModelWidget = class extends StdinDataWidget {
360
386
  id = "model";
361
387
  metadata = createWidgetMetadata(
362
388
  "Model",
363
- "Displays the current Claude model name"
389
+ "Displays the current Claude model name",
390
+ "1.0.0",
391
+ "claude-scope",
392
+ 0
393
+ // First line
364
394
  );
365
395
  renderWithData(data, context) {
366
396
  return data.model.display_name;
@@ -405,36 +435,61 @@ function progressBar(percent, width = DEFAULTS.PROGRESS_BAR_WIDTH) {
405
435
  const empty = width - filled;
406
436
  return "\u2588".repeat(filled) + "\u2591".repeat(empty);
407
437
  }
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
438
  function colorize(text, color) {
419
439
  return `${color}${text}${ANSI_COLORS.RESET}`;
420
440
  }
421
441
 
442
+ // src/ui/utils/colors.ts
443
+ var gray = "\x1B[90m";
444
+
445
+ // src/ui/theme/default-theme.ts
446
+ var DEFAULT_THEME = {
447
+ context: {
448
+ low: gray,
449
+ medium: gray,
450
+ high: gray
451
+ },
452
+ lines: {
453
+ added: gray,
454
+ removed: gray
455
+ }
456
+ };
457
+
422
458
  // src/widgets/context-widget.ts
423
459
  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
  );
469
+ colors;
470
+ constructor(colors) {
471
+ super();
472
+ this.colors = colors ?? DEFAULT_THEME.context;
473
+ }
429
474
  renderWithData(data, context) {
430
475
  const { current_usage, context_window_size } = data.context_window;
431
476
  if (!current_usage) return null;
432
477
  const used = current_usage.input_tokens + current_usage.cache_creation_input_tokens + current_usage.cache_read_input_tokens + current_usage.output_tokens;
433
478
  const percent = Math.round(used / context_window_size * 100);
434
479
  const bar = progressBar(percent, DEFAULTS.PROGRESS_BAR_WIDTH);
435
- const color = getContextColor(percent);
480
+ const color = this.getContextColor(percent);
436
481
  return colorize(`[${bar}] ${percent}%`, color);
437
482
  }
483
+ getContextColor(percent) {
484
+ const clampedPercent = Math.max(0, Math.min(100, percent));
485
+ if (clampedPercent < 50) {
486
+ return this.colors.low;
487
+ } else if (clampedPercent < 80) {
488
+ return this.colors.medium;
489
+ } else {
490
+ return this.colors.high;
491
+ }
492
+ }
438
493
  };
439
494
 
440
495
  // src/widgets/cost-widget.ts
@@ -442,7 +497,11 @@ var CostWidget = class extends StdinDataWidget {
442
497
  id = "cost";
443
498
  metadata = createWidgetMetadata(
444
499
  "Cost",
445
- "Displays session cost in USD"
500
+ "Displays session cost in USD",
501
+ "1.0.0",
502
+ "claude-scope",
503
+ 0
504
+ // First line
446
505
  );
447
506
  renderWithData(data, context) {
448
507
  if (!data.cost || data.cost.total_cost_usd === void 0) return null;
@@ -455,13 +514,22 @@ var LinesWidget = class extends StdinDataWidget {
455
514
  id = "lines";
456
515
  metadata = createWidgetMetadata(
457
516
  "Lines",
458
- "Displays lines added/removed in session"
517
+ "Displays lines added/removed in session",
518
+ "1.0.0",
519
+ "claude-scope",
520
+ 0
521
+ // First line
459
522
  );
523
+ colors;
524
+ constructor(colors) {
525
+ super();
526
+ this.colors = colors ?? DEFAULT_THEME.lines;
527
+ }
460
528
  renderWithData(data, context) {
461
529
  const added = data.cost?.total_lines_added ?? 0;
462
530
  const removed = data.cost?.total_lines_removed ?? 0;
463
- const addedStr = colorize(`+${added}`, ANSI_COLORS.GREEN);
464
- const removedStr = colorize(`-${removed}`, ANSI_COLORS.RED);
531
+ const addedStr = colorize(`+${added}`, this.colors.added);
532
+ const removedStr = colorize(`-${removed}`, this.colors.removed);
465
533
  return `${addedStr}/${removedStr}`;
466
534
  }
467
535
  };
@@ -471,7 +539,11 @@ var DurationWidget = class extends StdinDataWidget {
471
539
  id = "duration";
472
540
  metadata = createWidgetMetadata(
473
541
  "Duration",
474
- "Displays elapsed session time"
542
+ "Displays elapsed session time",
543
+ "1.0.0",
544
+ "claude-scope",
545
+ 0
546
+ // First line
475
547
  );
476
548
  renderWithData(data, context) {
477
549
  if (!data.cost || data.cost.total_duration_ms === void 0) return null;
@@ -484,7 +556,11 @@ var GitChangesWidget = class {
484
556
  id = "git-changes";
485
557
  metadata = createWidgetMetadata(
486
558
  "Git Changes",
487
- "Displays git diff statistics"
559
+ "Displays git diff statistics",
560
+ "1.0.0",
561
+ "claude-scope",
562
+ 0
563
+ // First line
488
564
  );
489
565
  gitFactory;
490
566
  git = null;
@@ -549,16 +625,214 @@ var GitChangesWidget = class {
549
625
  }
550
626
  };
551
627
 
628
+ // src/providers/config-provider.ts
629
+ var fs = __toESM(require("fs/promises"), 1);
630
+ var path = __toESM(require("path"), 1);
631
+ var os = __toESM(require("os"), 1);
632
+ var ConfigProvider = class {
633
+ cachedCounts;
634
+ lastScan = 0;
635
+ cacheInterval = 5e3;
636
+ // 5 seconds
637
+ /**
638
+ * Get config counts with hybrid caching
639
+ * Scans filesystem if cache is stale (>5 seconds)
640
+ */
641
+ async getConfigs(options = {}) {
642
+ const now = Date.now();
643
+ if (this.cachedCounts && now - this.lastScan < this.cacheInterval) {
644
+ return this.cachedCounts;
645
+ }
646
+ this.cachedCounts = await this.scanConfigs(options);
647
+ this.lastScan = now;
648
+ return this.cachedCounts;
649
+ }
650
+ /**
651
+ * Scan filesystem for Claude Code configurations
652
+ */
653
+ async scanConfigs(options) {
654
+ let claudeMdCount = 0;
655
+ let rulesCount = 0;
656
+ let mcpCount = 0;
657
+ let hooksCount = 0;
658
+ const homeDir = os.homedir();
659
+ const claudeDir = path.join(homeDir, ".claude");
660
+ const cwd = options.cwd;
661
+ if (await this.fileExists(path.join(claudeDir, "CLAUDE.md"))) {
662
+ claudeMdCount++;
663
+ }
664
+ rulesCount += await this.countRulesInDir(path.join(claudeDir, "rules"));
665
+ const userSettings = path.join(claudeDir, "settings.json");
666
+ const userSettingsData = await this.readJsonFile(userSettings);
667
+ if (userSettingsData) {
668
+ mcpCount += this.countMcpServers(userSettingsData);
669
+ hooksCount += this.countHooks(userSettingsData);
670
+ }
671
+ const userClaudeJson = path.join(homeDir, ".claude.json");
672
+ const userClaudeData = await this.readJsonFile(userClaudeJson);
673
+ if (userClaudeData) {
674
+ const userMcpCount = this.countMcpServers(userClaudeData);
675
+ mcpCount += Math.max(0, userMcpCount - this.countMcpServers(userSettingsData || {}));
676
+ }
677
+ if (cwd) {
678
+ if (await this.fileExists(path.join(cwd, "CLAUDE.md"))) {
679
+ claudeMdCount++;
680
+ }
681
+ if (await this.fileExists(path.join(cwd, "CLAUDE.local.md"))) {
682
+ claudeMdCount++;
683
+ }
684
+ if (await this.fileExists(path.join(cwd, ".claude", "CLAUDE.md"))) {
685
+ claudeMdCount++;
686
+ }
687
+ if (await this.fileExists(path.join(cwd, ".claude", "CLAUDE.local.md"))) {
688
+ claudeMdCount++;
689
+ }
690
+ rulesCount += await this.countRulesInDir(path.join(cwd, ".claude", "rules"));
691
+ const mcpJson = path.join(cwd, ".mcp.json");
692
+ const mcpData = await this.readJsonFile(mcpJson);
693
+ if (mcpData) {
694
+ mcpCount += this.countMcpServers(mcpData);
695
+ }
696
+ const projectSettings = path.join(cwd, ".claude", "settings.json");
697
+ const projectSettingsData = await this.readJsonFile(projectSettings);
698
+ if (projectSettingsData) {
699
+ mcpCount += this.countMcpServers(projectSettingsData);
700
+ hooksCount += this.countHooks(projectSettingsData);
701
+ }
702
+ const localSettings = path.join(cwd, ".claude", "settings.local.json");
703
+ const localSettingsData = await this.readJsonFile(localSettings);
704
+ if (localSettingsData) {
705
+ mcpCount += this.countMcpServers(localSettingsData);
706
+ hooksCount += this.countHooks(localSettingsData);
707
+ }
708
+ }
709
+ return { claudeMdCount, rulesCount, mcpCount, hooksCount };
710
+ }
711
+ /**
712
+ * Check if file exists
713
+ */
714
+ async fileExists(filePath) {
715
+ try {
716
+ await fs.access(filePath);
717
+ return true;
718
+ } catch {
719
+ return false;
720
+ }
721
+ }
722
+ /**
723
+ * Read and parse JSON file
724
+ */
725
+ async readJsonFile(filePath) {
726
+ try {
727
+ const content = await fs.readFile(filePath, "utf8");
728
+ return JSON.parse(content);
729
+ } catch {
730
+ return null;
731
+ }
732
+ }
733
+ /**
734
+ * Count MCP servers in config object
735
+ */
736
+ countMcpServers(config) {
737
+ if (!config || !config.mcpServers || typeof config.mcpServers !== "object") {
738
+ return 0;
739
+ }
740
+ return Object.keys(config.mcpServers).length;
741
+ }
742
+ /**
743
+ * Count hooks in config object
744
+ */
745
+ countHooks(config) {
746
+ if (!config || !config.hooks || typeof config.hooks !== "object") {
747
+ return 0;
748
+ }
749
+ return Object.keys(config.hooks).length;
750
+ }
751
+ /**
752
+ * Recursively count .md files in directory
753
+ */
754
+ async countRulesInDir(rulesDir) {
755
+ const exists = await this.fileExists(rulesDir);
756
+ if (!exists) return 0;
757
+ try {
758
+ let count = 0;
759
+ const entries = await fs.readdir(rulesDir, { withFileTypes: true });
760
+ for (const entry of entries) {
761
+ const fullPath = path.join(rulesDir, entry.name);
762
+ if (entry.isDirectory()) {
763
+ count += await this.countRulesInDir(fullPath);
764
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
765
+ count++;
766
+ }
767
+ }
768
+ return count;
769
+ } catch {
770
+ return 0;
771
+ }
772
+ }
773
+ };
774
+
775
+ // src/widgets/config-count-widget.ts
776
+ var ConfigCountWidget = class {
777
+ id = "config-count";
778
+ metadata = createWidgetMetadata(
779
+ "Config Count",
780
+ "Displays Claude Code configuration counts",
781
+ "1.0.0",
782
+ "claude-scope",
783
+ 1
784
+ // Second line
785
+ );
786
+ configProvider = new ConfigProvider();
787
+ configs;
788
+ cwd;
789
+ async initialize() {
790
+ }
791
+ async update(data) {
792
+ this.cwd = data.cwd;
793
+ this.configs = await this.configProvider.getConfigs({ cwd: data.cwd });
794
+ }
795
+ isEnabled() {
796
+ if (!this.configs) {
797
+ return false;
798
+ }
799
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = this.configs;
800
+ return claudeMdCount > 0 || rulesCount > 0 || mcpCount > 0 || hooksCount > 0;
801
+ }
802
+ async render(context) {
803
+ if (!this.configs) {
804
+ return null;
805
+ }
806
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = this.configs;
807
+ const parts = [];
808
+ if (claudeMdCount > 0) {
809
+ parts.push(`\u{1F4C4} ${claudeMdCount} CLAUDE.md`);
810
+ }
811
+ if (rulesCount > 0) {
812
+ parts.push(`\u{1F4DC} ${rulesCount} rules`);
813
+ }
814
+ if (mcpCount > 0) {
815
+ parts.push(`\u{1F50C} ${mcpCount} MCPs`);
816
+ }
817
+ if (hooksCount > 0) {
818
+ parts.push(`\u{1FA9D} ${hooksCount} hooks`);
819
+ }
820
+ return parts.join(" \u2502 ") || null;
821
+ }
822
+ async cleanup() {
823
+ }
824
+ };
825
+
552
826
  // src/validation/result.ts
553
827
  function success(data) {
554
828
  return { success: true, data };
555
829
  }
556
- function failure(path, message, value) {
557
- return { success: false, error: { path, message, value } };
830
+ function failure(path2, message, value) {
831
+ return { success: false, error: { path: path2, message, value } };
558
832
  }
559
833
  function formatError(error) {
560
- const path = error.path.length > 0 ? error.path.join(".") : "root";
561
- return `${path}: ${error.message}`;
834
+ const path2 = error.path.length > 0 ? error.path.join(".") : "root";
835
+ return `${path2}: ${error.message}`;
562
836
  }
563
837
 
564
838
  // src/validation/validators.ts
@@ -751,6 +1025,7 @@ async function main() {
751
1025
  await registry.register(new DurationWidget());
752
1026
  await registry.register(new GitWidget());
753
1027
  await registry.register(new GitChangesWidget());
1028
+ await registry.register(new ConfigCountWidget());
754
1029
  const renderer = new Renderer({
755
1030
  separator: " \u2502 ",
756
1031
  onError: (error, widget) => {
@@ -760,11 +1035,11 @@ async function main() {
760
1035
  for (const widget of registry.getAll()) {
761
1036
  await widget.update(stdinData);
762
1037
  }
763
- const output = await renderer.render(
1038
+ const lines = await renderer.render(
764
1039
  registry.getEnabledWidgets(),
765
1040
  { width: 80, timestamp: Date.now() }
766
1041
  );
767
- return output || "";
1042
+ return lines.join("\n");
768
1043
  } catch (error) {
769
1044
  const fallback = await tryGitFallback();
770
1045
  return fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scope",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
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
  },