skill-analyzer 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mathbullet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # skill-analyzer
2
+
3
+ Analyze your Claude Code skill library: usage frequency, dead skills, and description budget consumption.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx skill-analyzer report
9
+ ```
10
+
11
+ ## Install as Claude Code Plugin
12
+
13
+ ```
14
+ /plugin add mathbullet/skill-analyzer
15
+ ```
16
+
17
+ Then use `/skill-analyzer` in Claude Code.
18
+
19
+ ## Commands
20
+
21
+ | Command | Description |
22
+ |---------|-------------|
23
+ | `report` | Full analysis report (default) |
24
+ | `usage` | Skill usage frequency |
25
+ | `dead` | Unused skills |
26
+ | `budget` | Description budget consumption |
27
+
28
+ ## Options
29
+
30
+ - `--days N` — Limit to last N days
31
+ - `--json` — JSON output
32
+ - `--skills-dir <path>` — Skills directory (default: `~/.claude/skills`)
33
+ - `--logs-dir <path>` — Logs directory (default: `~/.claude/projects`)
34
+
35
+ ## What it does
36
+
37
+ - Scans `~/.claude/projects/**/*.jsonl` session logs for `Skill` tool invocations
38
+ - Parses `~/.claude/skills/*/SKILL.md` frontmatter for description budget calculation
39
+ - Normalizes skill names (e.g., `superpowers:brainstorming` → `brainstorming`)
40
+ - Reports unused skills and their budget cost for cleanup decisions
@@ -0,0 +1 @@
1
+ export declare function budgetCommand(args: string[]): void;
package/dist/budget.js ADDED
@@ -0,0 +1,45 @@
1
+ import chalk from "chalk";
2
+ import { getSkillsDir, loadSkillRegistry } from "./registry.js";
3
+ import { sectionHeader, keyValue, budgetBar, makeTable } from "./formatter.js";
4
+ const OVERHEAD_PER_SKILL = 109;
5
+ const DEFAULT_BUDGET = 16_000;
6
+ export function budgetCommand(args) {
7
+ const skillsDir = getSkillsDir(args);
8
+ const skills = loadSkillRegistry(skillsDir);
9
+ const budget = parseInt(process.env["SLASH_COMMAND_TOOL_CHAR_BUDGET"] ?? String(DEFAULT_BUDGET), 10);
10
+ const isJson = args.includes("--json");
11
+ const entries = skills
12
+ .map((s) => ({
13
+ name: s.name,
14
+ descriptionLength: s.descriptionLength,
15
+ cost: s.descriptionLength + OVERHEAD_PER_SKILL,
16
+ }))
17
+ .sort((a, b) => b.cost - a.cost);
18
+ const totalCost = entries.reduce((sum, e) => sum + e.cost, 0);
19
+ if (isJson) {
20
+ console.log(JSON.stringify({ budget, totalCost, entries }, null, 2));
21
+ return;
22
+ }
23
+ const overBudget = totalCost > budget;
24
+ sectionHeader("Description Budget", overBudget ? "OVER BUDGET" : undefined);
25
+ keyValue([
26
+ ["Budget:", budgetBar(totalCost, budget)],
27
+ [
28
+ "Used:",
29
+ `${totalCost.toLocaleString()} / ${budget.toLocaleString()} chars`,
30
+ ],
31
+ [
32
+ "Remaining:",
33
+ overBudget
34
+ ? chalk.red(`-${(totalCost - budget).toLocaleString()} chars`)
35
+ : chalk.green(`${(budget - totalCost).toLocaleString()} chars`),
36
+ ],
37
+ ["Skills:", `${entries.length} (${OVERHEAD_PER_SKILL} chars overhead each)`],
38
+ ]);
39
+ makeTable({
40
+ head: ["#", "Skill", "Description Chars", "Total Chars"],
41
+ colAligns: ["right", "left", "right", "right"],
42
+ rows: entries.map((e, i) => [i + 1, e.name, e.descriptionLength, e.cost]),
43
+ });
44
+ console.log();
45
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function run(args: string[]): void;
package/dist/cli.js ADDED
@@ -0,0 +1,39 @@
1
+ import { budgetCommand } from "./budget.js";
2
+ import { usageCommand } from "./usage.js";
3
+ import { deadCommand } from "./dead.js";
4
+ import { reportCommand } from "./report.js";
5
+ export function run(args) {
6
+ const command = args[0] ?? "report";
7
+ switch (command) {
8
+ case "budget":
9
+ budgetCommand(args.slice(1));
10
+ break;
11
+ case "usage":
12
+ usageCommand(args.slice(1));
13
+ break;
14
+ case "dead":
15
+ deadCommand(args.slice(1));
16
+ break;
17
+ case "report":
18
+ reportCommand(args.slice(1));
19
+ break;
20
+ default:
21
+ printHelp();
22
+ }
23
+ }
24
+ function printHelp() {
25
+ console.log(`skill-analyzer - Analyze your Claude Code skill library
26
+
27
+ Usage: skill-analyzer <command> [options]
28
+
29
+ Commands:
30
+ report Full analysis report (default)
31
+ usage Skill usage frequency
32
+ dead Unused skills
33
+ budget Description budget consumption
34
+
35
+ Options:
36
+ --skills-dir <path> Skills directory (default: ~/.claude/skills)
37
+ --json Output as JSON
38
+ --help Show this help`);
39
+ }
package/dist/dead.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function deadCommand(args: string[]): void;
package/dist/dead.js ADDED
@@ -0,0 +1,47 @@
1
+ import chalk from "chalk";
2
+ import { getSkillsDir, loadSkillRegistry } from "./registry.js";
3
+ import { getLogsDir, scanSessions, aggregateUsage } from "./scanner.js";
4
+ import { sectionHeader, keyValue, makeTable } from "./formatter.js";
5
+ export function deadCommand(args) {
6
+ const skillsDir = getSkillsDir(args);
7
+ const logsDir = getLogsDir(args);
8
+ const daysIdx = args.indexOf("--days");
9
+ const days = daysIdx !== -1 ? parseInt(args[daysIdx + 1], 10) : undefined;
10
+ const isJson = args.includes("--json");
11
+ const registry = loadSkillRegistry(skillsDir);
12
+ const invocations = scanSessions(logsDir, days);
13
+ const usage = aggregateUsage(invocations);
14
+ const usedSkills = new Set(usage.map((u) => u.skill));
15
+ const deadSkills = registry.filter((s) => !usedSkills.has(s.name));
16
+ if (isJson) {
17
+ console.log(JSON.stringify({ deadSkills: deadSkills.map((s) => s.name), count: deadSkills.length }, null, 2));
18
+ return;
19
+ }
20
+ const period = days ? `last ${days} days` : "all time";
21
+ sectionHeader(`Dead Skills (${period})`);
22
+ const totalWaste = deadSkills.reduce((sum, s) => sum + s.descriptionLength + 109, 0);
23
+ keyValue([
24
+ ["Dead:", `${deadSkills.length} of ${registry.length} local skills`],
25
+ [
26
+ "Savings:",
27
+ deadSkills.length > 0
28
+ ? chalk.yellow(`${totalWaste.toLocaleString()} chars recoverable`)
29
+ : chalk.green("None needed"),
30
+ ],
31
+ ]);
32
+ if (deadSkills.length === 0) {
33
+ console.log(chalk.green(" All skills have been used at least once.\n"));
34
+ return;
35
+ }
36
+ makeTable({
37
+ head: ["#", "Skill", "Description Chars", "Total Chars"],
38
+ colAligns: ["right", "left", "right", "right"],
39
+ rows: deadSkills.map((s, i) => [
40
+ i + 1,
41
+ chalk.red(s.name),
42
+ s.descriptionLength,
43
+ s.descriptionLength + 109,
44
+ ]),
45
+ });
46
+ console.log();
47
+ }
@@ -0,0 +1,8 @@
1
+ export declare function sectionHeader(title: string, warning?: string): void;
2
+ export declare function keyValue(pairs: [string, string][]): void;
3
+ export declare function budgetBar(used: number, total: number): string;
4
+ export declare function makeTable(options: {
5
+ head: string[];
6
+ rows: (string | number)[][];
7
+ colAligns?: ("left" | "right" | "middle")[];
8
+ }): void;
@@ -0,0 +1,57 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ export function sectionHeader(title, warning) {
4
+ const header = warning
5
+ ? chalk.red.bold(` ${title} ${warning}`)
6
+ : chalk.cyan.bold(` ${title}`);
7
+ console.log(`\n${header}\n`);
8
+ }
9
+ export function keyValue(pairs) {
10
+ for (const [key, value] of pairs) {
11
+ console.log(` ${chalk.dim(key)} ${value}`);
12
+ }
13
+ console.log();
14
+ }
15
+ export function budgetBar(used, total) {
16
+ const pct = used / total;
17
+ const width = 30;
18
+ const filled = Math.round(pct * width);
19
+ const empty = width - filled;
20
+ const color = pct > 1 ? chalk.red : pct > 0.7 ? chalk.yellow : chalk.green;
21
+ const bar = color("█".repeat(filled)) + chalk.dim("░".repeat(empty));
22
+ const label = color(`${(pct * 100).toFixed(0)}%`);
23
+ return `${bar} ${label}`;
24
+ }
25
+ export function makeTable(options) {
26
+ const table = new Table({
27
+ head: options.head.map((h) => chalk.bold(h)),
28
+ colAligns: options.colAligns,
29
+ style: {
30
+ head: [],
31
+ border: [],
32
+ "padding-left": 1,
33
+ "padding-right": 1,
34
+ },
35
+ chars: {
36
+ top: "─",
37
+ "top-mid": "┬",
38
+ "top-left": "┌",
39
+ "top-right": "┐",
40
+ bottom: "─",
41
+ "bottom-mid": "┴",
42
+ "bottom-left": "└",
43
+ "bottom-right": "┘",
44
+ left: "│",
45
+ "left-mid": "├",
46
+ mid: "─",
47
+ "mid-mid": "┼",
48
+ right: "│",
49
+ "right-mid": "┤",
50
+ middle: "│",
51
+ },
52
+ });
53
+ for (const row of options.rows) {
54
+ table.push(row.map(String));
55
+ }
56
+ console.log(table.toString());
57
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ run(process.argv.slice(2));
@@ -0,0 +1 @@
1
+ export declare function normalizeSkillName(input: Record<string, unknown>): string | null;
@@ -0,0 +1,11 @@
1
+ export function normalizeSkillName(input) {
2
+ const raw = (input["skill"] ?? input["skillName"] ?? input["skill_name"]);
3
+ if (!raw || typeof raw !== "string")
4
+ return null;
5
+ // Strip plugin prefix: "superpowers:brainstorming" → "brainstorming"
6
+ const colonIdx = raw.indexOf(":");
7
+ if (colonIdx !== -1) {
8
+ return raw.slice(colonIdx + 1);
9
+ }
10
+ return raw;
11
+ }
@@ -0,0 +1,8 @@
1
+ export interface SkillEntry {
2
+ name: string;
3
+ description: string;
4
+ descriptionLength: number;
5
+ path: string;
6
+ }
7
+ export declare function getSkillsDir(args: string[]): string;
8
+ export declare function loadSkillRegistry(skillsDir: string): SkillEntry[];
@@ -0,0 +1,51 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ export function getSkillsDir(args) {
5
+ const idx = args.indexOf("--skills-dir");
6
+ if (idx !== -1 && args[idx + 1]) {
7
+ return args[idx + 1];
8
+ }
9
+ return join(homedir(), ".claude", "skills");
10
+ }
11
+ export function loadSkillRegistry(skillsDir) {
12
+ const entries = [];
13
+ let dirs;
14
+ try {
15
+ dirs = readdirSync(skillsDir);
16
+ }
17
+ catch {
18
+ console.error(`Cannot read skills directory: ${skillsDir}`);
19
+ process.exit(1);
20
+ }
21
+ for (const dir of dirs) {
22
+ const skillMdPath = join(skillsDir, dir, "SKILL.md");
23
+ try {
24
+ statSync(skillMdPath);
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ const content = readFileSync(skillMdPath, "utf-8");
30
+ const { name, description } = parseFrontmatter(content, dir);
31
+ entries.push({
32
+ name,
33
+ description,
34
+ descriptionLength: description.length,
35
+ path: skillMdPath,
36
+ });
37
+ }
38
+ return entries;
39
+ }
40
+ function parseFrontmatter(content, fallbackName) {
41
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
42
+ if (!match) {
43
+ return { name: fallbackName, description: "" };
44
+ }
45
+ const frontmatter = match[1];
46
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
47
+ const name = nameMatch ? nameMatch[1].trim() : fallbackName;
48
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
49
+ const description = descMatch ? descMatch[1].trim() : "";
50
+ return { name, description };
51
+ }
@@ -0,0 +1 @@
1
+ export declare function reportCommand(args: string[]): void;
package/dist/report.js ADDED
@@ -0,0 +1,41 @@
1
+ import { getSkillsDir, loadSkillRegistry } from "./registry.js";
2
+ import { getLogsDir, scanSessions, aggregateUsage } from "./scanner.js";
3
+ import { budgetCommand } from "./budget.js";
4
+ import { usageCommand } from "./usage.js";
5
+ import { deadCommand } from "./dead.js";
6
+ export function reportCommand(args) {
7
+ const isJson = args.includes("--json");
8
+ if (isJson) {
9
+ reportJson(args);
10
+ return;
11
+ }
12
+ // Just run all three in sequence
13
+ usageCommand(args);
14
+ deadCommand(args);
15
+ budgetCommand(args);
16
+ }
17
+ function reportJson(args) {
18
+ const skillsDir = getSkillsDir(args);
19
+ const logsDir = getLogsDir(args);
20
+ const daysIdx = args.indexOf("--days");
21
+ const days = daysIdx !== -1 ? parseInt(args[daysIdx + 1], 10) : undefined;
22
+ const registry = loadSkillRegistry(skillsDir);
23
+ const invocations = scanSessions(logsDir, days);
24
+ const usage = aggregateUsage(invocations);
25
+ const usedSkills = new Set(usage.map((u) => u.skill));
26
+ const deadSkills = registry.filter((s) => !usedSkills.has(s.name)).map((s) => s.name);
27
+ const budget = parseInt(process.env["SLASH_COMMAND_TOOL_CHAR_BUDGET"] ?? "16000", 10);
28
+ const budgetEntries = registry
29
+ .map((s) => ({
30
+ name: s.name,
31
+ descriptionLength: s.descriptionLength,
32
+ cost: s.descriptionLength + 109,
33
+ }))
34
+ .sort((a, b) => b.cost - a.cost);
35
+ const totalCost = budgetEntries.reduce((sum, e) => sum + e.cost, 0);
36
+ console.log(JSON.stringify({
37
+ usage: { totalInvocations: invocations.length, skills: usage },
38
+ dead: { count: deadSkills.length, skills: deadSkills },
39
+ budget: { limit: budget, totalCost, entries: budgetEntries },
40
+ }, null, 2));
41
+ }
@@ -0,0 +1,13 @@
1
+ export interface SkillInvocation {
2
+ skill: string;
3
+ timestamp: string;
4
+ sessionId: string;
5
+ }
6
+ export interface UsageResult {
7
+ skill: string;
8
+ count: number;
9
+ lastUsed: string;
10
+ }
11
+ export declare function getLogsDir(args: string[]): string;
12
+ export declare function scanSessions(logsDir: string, daysLimit?: number): SkillInvocation[];
13
+ export declare function aggregateUsage(invocations: SkillInvocation[]): UsageResult[];
@@ -0,0 +1,114 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { normalizeSkillName } from "./normalizer.js";
5
+ export function getLogsDir(args) {
6
+ const idx = args.indexOf("--logs-dir");
7
+ if (idx !== -1 && args[idx + 1]) {
8
+ return args[idx + 1];
9
+ }
10
+ return join(homedir(), ".claude", "projects");
11
+ }
12
+ export function scanSessions(logsDir, daysLimit) {
13
+ const invocations = [];
14
+ const cutoff = daysLimit
15
+ ? new Date(Date.now() - daysLimit * 86_400_000)
16
+ : null;
17
+ let projectDirs;
18
+ try {
19
+ projectDirs = readdirSync(logsDir);
20
+ }
21
+ catch {
22
+ console.error(`Cannot read logs directory: ${logsDir}`);
23
+ process.exit(1);
24
+ }
25
+ for (const projectDir of projectDirs) {
26
+ const projectPath = join(logsDir, projectDir);
27
+ try {
28
+ if (!statSync(projectPath).isDirectory())
29
+ continue;
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ let files;
35
+ try {
36
+ files = readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ for (const file of files) {
42
+ const filePath = join(projectPath, file);
43
+ // Quick mtime filter when --days is specified
44
+ if (cutoff) {
45
+ try {
46
+ const mtime = statSync(filePath).mtime;
47
+ if (mtime < cutoff)
48
+ continue;
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ }
54
+ const content = readFileSync(filePath, "utf-8");
55
+ for (const line of content.split("\n")) {
56
+ if (!line.includes('"Skill"'))
57
+ continue;
58
+ try {
59
+ const obj = JSON.parse(line);
60
+ const msg = obj["message"];
61
+ if (!msg)
62
+ continue;
63
+ const contentArr = msg["content"];
64
+ if (!Array.isArray(contentArr))
65
+ continue;
66
+ for (const block of contentArr) {
67
+ if (typeof block === "object" &&
68
+ block !== null &&
69
+ block["type"] === "tool_use" &&
70
+ block["name"] === "Skill") {
71
+ const input = block["input"];
72
+ if (!input)
73
+ continue;
74
+ const name = normalizeSkillName(input);
75
+ if (!name)
76
+ continue;
77
+ invocations.push({
78
+ skill: name,
79
+ timestamp: obj["timestamp"] ?? "",
80
+ sessionId: obj["sessionId"] ?? "",
81
+ });
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // Skip malformed lines
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return invocations;
92
+ }
93
+ export function aggregateUsage(invocations) {
94
+ const map = new Map();
95
+ for (const inv of invocations) {
96
+ const existing = map.get(inv.skill);
97
+ if (!existing) {
98
+ map.set(inv.skill, { count: 1, lastUsed: inv.timestamp });
99
+ }
100
+ else {
101
+ existing.count++;
102
+ if (inv.timestamp > existing.lastUsed) {
103
+ existing.lastUsed = inv.timestamp;
104
+ }
105
+ }
106
+ }
107
+ return Array.from(map.entries())
108
+ .map(([skill, data]) => ({
109
+ skill,
110
+ count: data.count,
111
+ lastUsed: data.lastUsed,
112
+ }))
113
+ .sort((a, b) => b.count - a.count);
114
+ }
@@ -0,0 +1 @@
1
+ export declare function usageCommand(args: string[]): void;
package/dist/usage.js ADDED
@@ -0,0 +1,36 @@
1
+ import chalk from "chalk";
2
+ import { getLogsDir, scanSessions, aggregateUsage } from "./scanner.js";
3
+ import { sectionHeader, keyValue, makeTable } from "./formatter.js";
4
+ export function usageCommand(args) {
5
+ const logsDir = getLogsDir(args);
6
+ const daysIdx = args.indexOf("--days");
7
+ const days = daysIdx !== -1 ? parseInt(args[daysIdx + 1], 10) : undefined;
8
+ const isJson = args.includes("--json");
9
+ const invocations = scanSessions(logsDir, days);
10
+ const usage = aggregateUsage(invocations);
11
+ if (isJson) {
12
+ console.log(JSON.stringify({ totalInvocations: invocations.length, skills: usage }, null, 2));
13
+ return;
14
+ }
15
+ const period = days ? `last ${days} days` : "all time";
16
+ sectionHeader(`Skill Usage (${period})`);
17
+ keyValue([
18
+ ["Invocations:", String(invocations.length)],
19
+ ["Unique skills:", String(usage.length)],
20
+ ]);
21
+ if (usage.length === 0) {
22
+ console.log(chalk.dim(" No skill invocations found.\n"));
23
+ return;
24
+ }
25
+ makeTable({
26
+ head: ["#", "Skill", "Count", "Last Used"],
27
+ colAligns: ["right", "left", "right", "left"],
28
+ rows: usage.map((u, i) => [
29
+ i + 1,
30
+ u.skill,
31
+ u.count,
32
+ u.lastUsed ? u.lastUsed.slice(0, 10) : "unknown",
33
+ ]),
34
+ });
35
+ console.log();
36
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "skill-analyzer",
3
+ "version": "0.1.0",
4
+ "description": "Analyze Claude Code skill library: usage frequency, dead skills, description budget consumption",
5
+ "type": "module",
6
+ "bin": {
7
+ "skill-analyzer": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js"
12
+ },
13
+ "keywords": [
14
+ "claude-code",
15
+ "skills",
16
+ "analyzer",
17
+ "cli"
18
+ ],
19
+ "author": "mathbullet",
20
+ "license": "MIT",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "typescript": "^5.7.0"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "dependencies": {
35
+ "chalk": "^5.6.2",
36
+ "cli-table3": "^0.6.5"
37
+ }
38
+ }