learn_bash_from_session_data 1.0.7 → 1.0.9
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/README.md +141 -18
- package/package.json +1 -1
- package/scripts/html_generator.py +144 -11
- package/scripts/knowledge_base.py +2 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# learn_bash_from_session_data
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Turn your Claude Code sessions into personalized bash lessons. This tool extracts every command you've run, enriches them with descriptions and flag breakdowns from a 402-command knowledge base, generates interactive quizzes, and produces a self-contained HTML learning resource.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,37 +8,160 @@ Learn bash from your Claude Code sessions. Extracts commands you've used, catego
|
|
|
8
8
|
npm install -g learn_bash_from_session_data
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**Requirements:** Node.js >= 14.0.0 and Python >= 3.8
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
|
-
#
|
|
16
|
+
# Generate a lesson from your current project's sessions
|
|
15
17
|
learn-bash
|
|
16
18
|
|
|
17
|
-
#
|
|
19
|
+
# Process the last 5 sessions only
|
|
18
20
|
learn-bash -n 5
|
|
19
21
|
|
|
20
|
-
# List available projects
|
|
22
|
+
# List all available Claude Code projects
|
|
21
23
|
learn-bash --list
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The generated HTML opens automatically in your browser.
|
|
27
|
+
|
|
28
|
+
## CLI Reference
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
learn-bash [options]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Flag | Short | Description |
|
|
35
|
+
|------|-------|-------------|
|
|
36
|
+
| `--sessions <count>` | `-n` | Number of recent sessions to process (default: all) |
|
|
37
|
+
| `--file <path>` | `-f` | Process a specific session JSONL file |
|
|
38
|
+
| `--output <path>` | `-o` | Output directory path (default: `./bash-learner-output/`) |
|
|
39
|
+
| `--project <name>` | `-p` | Process sessions from a specific project by name |
|
|
40
|
+
| `--list` | `-l` | List available Claude Code projects with session counts |
|
|
41
|
+
| `--no-open` | | Don't auto-open the generated HTML in browser |
|
|
42
|
+
| `--help` | `-h` | Show help message |
|
|
43
|
+
|
|
44
|
+
### Examples
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Process a specific session file
|
|
48
|
+
learn-bash --file ~/.claude/projects/my-project/abc123.jsonl
|
|
49
|
+
|
|
50
|
+
# Output to a custom location without opening browser
|
|
51
|
+
learn-bash -o ./my-lessons --no-open
|
|
22
52
|
|
|
23
|
-
# Process
|
|
24
|
-
learn-bash --
|
|
53
|
+
# Process sessions from a named project
|
|
54
|
+
learn-bash --project "C--Users-me-my-app"
|
|
25
55
|
|
|
26
|
-
#
|
|
27
|
-
learn-bash
|
|
56
|
+
# Process last 3 sessions, custom output
|
|
57
|
+
learn-bash -n 3 -o ./review
|
|
28
58
|
```
|
|
29
59
|
|
|
30
|
-
##
|
|
60
|
+
## What You Get
|
|
61
|
+
|
|
62
|
+
The tool generates a single interactive HTML file with four sections:
|
|
63
|
+
|
|
64
|
+
### Commands Tab
|
|
65
|
+
Every bash command you used, organized by category with:
|
|
66
|
+
- Syntax-highlighted full command display
|
|
67
|
+
- Flag breakdowns with descriptions (e.g., `-l` = "Long format listing with permissions, size, dates")
|
|
68
|
+
- Subcommand explanations (e.g., `git add` = "Stage file contents for commit")
|
|
69
|
+
- Common usage patterns from the knowledge base
|
|
70
|
+
- Search, sort (by frequency, complexity, category, name), and category filtering
|
|
71
|
+
|
|
72
|
+
### Lessons Tab
|
|
73
|
+
Step-by-step walkthrough of commands grouped by category, with flag details and complexity indicators. Designed for sequential learning.
|
|
74
|
+
|
|
75
|
+
### Quiz Tab
|
|
76
|
+
20 auto-generated questions in four types:
|
|
77
|
+
|
|
78
|
+
| Type | Weight | What It Tests |
|
|
79
|
+
|------|--------|---------------|
|
|
80
|
+
| What does this do? | 40% | Identify a command's purpose from its syntax |
|
|
81
|
+
| Which flag? | 25% | Match a flag to its behavior |
|
|
82
|
+
| Build the command | 20% | Construct the correct command for a task |
|
|
83
|
+
| Spot the difference | 15% | Compare two similar commands |
|
|
84
|
+
|
|
85
|
+
Quizzes are **session-adaptive** (based on commands you actually used), **randomized** (different questions and answer order each run), and use plausible distractors drawn from 402 real commands.
|
|
86
|
+
|
|
87
|
+
### Summary Tab
|
|
88
|
+
Statistics on your session: total commands, category distribution, complexity breakdown, most-used commands.
|
|
89
|
+
|
|
90
|
+
## Knowledge Base
|
|
31
91
|
|
|
32
|
-
-
|
|
33
|
-
- **Categorization**: Groups commands by category (Git, File System, Text Processing, etc.)
|
|
34
|
-
- **Complexity Scoring**: Rates commands from 1-5 based on complexity
|
|
35
|
-
- **Interactive Quizzes**: Test your knowledge with auto-generated quizzes
|
|
36
|
-
- **Self-Contained HTML**: No external dependencies, works offline
|
|
92
|
+
The built-in knowledge base powers descriptions, flag lookups, and quiz generation:
|
|
37
93
|
|
|
38
|
-
|
|
94
|
+
| Metric | Count |
|
|
95
|
+
|--------|-------|
|
|
96
|
+
| Commands documented | 402 |
|
|
97
|
+
| Flag definitions | 1,961 |
|
|
98
|
+
| Common usage patterns | 1,357 |
|
|
99
|
+
| Categories | 11 |
|
|
100
|
+
| Bash operators | 16 |
|
|
101
|
+
| Bash concepts | 6 |
|
|
39
102
|
|
|
40
|
-
|
|
41
|
-
|
|
103
|
+
### Categories
|
|
104
|
+
|
|
105
|
+
File System, Text Processing, Git, Package Management, Process & System, Networking, Permissions, Compression, Search & Navigation, Development, Shell Builtins
|
|
106
|
+
|
|
107
|
+
## How It Works
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Claude Code session (.jsonl)
|
|
111
|
+
|
|
|
112
|
+
v
|
|
113
|
+
[Parser] --> Extract bash tool_use blocks
|
|
114
|
+
|
|
|
115
|
+
v
|
|
116
|
+
[Extractor] --> Split compound commands (pipes, &&, ;)
|
|
117
|
+
|
|
|
118
|
+
v
|
|
119
|
+
[Analyzer] --> Categorize, score complexity (1-5), count frequency
|
|
120
|
+
|
|
|
121
|
+
v
|
|
122
|
+
[Knowledge Base] --> Enrich with 402 commands, 1961 flags, 1357 patterns
|
|
123
|
+
|
|
|
124
|
+
v
|
|
125
|
+
[Quiz Generator] --> 20 randomized, session-adaptive questions
|
|
126
|
+
|
|
|
127
|
+
v
|
|
128
|
+
[HTML Generator] --> Self-contained interactive HTML (no dependencies)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Session File Location
|
|
132
|
+
|
|
133
|
+
Claude Code stores sessions at:
|
|
134
|
+
|
|
135
|
+
| Platform | Path |
|
|
136
|
+
|----------|------|
|
|
137
|
+
| macOS/Linux | `~/.claude/projects/` |
|
|
138
|
+
| Windows | `%USERPROFILE%\.claude\projects\` |
|
|
139
|
+
| WSL | Auto-detected from `/mnt/c/Users/<name>/.claude/projects/` |
|
|
140
|
+
|
|
141
|
+
Each project directory contains `.jsonl` session files that this tool reads.
|
|
142
|
+
|
|
143
|
+
## Programmatic Usage
|
|
144
|
+
|
|
145
|
+
You can also run the Python pipeline directly:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python scripts/main.py --sessions 5 --output ./output
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or import modules in Python:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from scripts.knowledge_base import COMMAND_DB, get_flags_for_command, get_command_info
|
|
155
|
+
from scripts.quiz_generator import generate_quiz_set
|
|
156
|
+
from scripts.analyzer import analyze_commands
|
|
157
|
+
|
|
158
|
+
# Look up a command
|
|
159
|
+
info = get_command_info("grep")
|
|
160
|
+
flags = get_flags_for_command("grep")
|
|
161
|
+
|
|
162
|
+
# Generate quizzes from analyzed commands
|
|
163
|
+
quizzes = generate_quiz_set(analyzed_commands, count=10)
|
|
164
|
+
```
|
|
42
165
|
|
|
43
166
|
## License
|
|
44
167
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "learn_bash_from_session_data",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Learn bash from your Claude Code sessions - extracts commands and generates interactive HTML lessons with 400+ commands, quizzes, and comprehensive coverage",
|
|
5
5
|
"main": "bin/learn-bash.js",
|
|
6
6
|
"bin": {
|
|
@@ -433,7 +433,7 @@ def render_commands_tab(commands: list[dict]) -> str:
|
|
|
433
433
|
<span class="category-badge">{category}</span>
|
|
434
434
|
</div>
|
|
435
435
|
<div class="command-meta">
|
|
436
|
-
<span class="
|
|
436
|
+
<span class="cmd-preview">{' '.join(description.split())[:60]}{'...' if len(' '.join(description.split())) > 60 else ''}</span>
|
|
437
437
|
<span class="expand-icon">▼</span>
|
|
438
438
|
</div>
|
|
439
439
|
</div>
|
|
@@ -1358,9 +1358,13 @@ def get_inline_css() -> str:
|
|
|
1358
1358
|
gap: 16px;
|
|
1359
1359
|
}
|
|
1360
1360
|
|
|
1361
|
-
.
|
|
1362
|
-
font-size: 0.
|
|
1361
|
+
.cmd-preview {
|
|
1362
|
+
font-size: 0.8rem;
|
|
1363
1363
|
color: var(--text-secondary);
|
|
1364
|
+
max-width: 400px;
|
|
1365
|
+
overflow: hidden;
|
|
1366
|
+
text-overflow: ellipsis;
|
|
1367
|
+
white-space: nowrap;
|
|
1364
1368
|
}
|
|
1365
1369
|
|
|
1366
1370
|
.expand-icon {
|
|
@@ -2181,25 +2185,54 @@ def generate_html_files(
|
|
|
2181
2185
|
base_cmd = cmd.get('base_command', cmd_str.split()[0] if cmd_str else '')
|
|
2182
2186
|
complexity_score = cmd.get('complexity', 1)
|
|
2183
2187
|
|
|
2188
|
+
# Filter out non-bash entries (Python/JS code fragments, single chars, status text)
|
|
2189
|
+
if not base_cmd or len(base_cmd) < 2:
|
|
2190
|
+
continue
|
|
2191
|
+
# Skip entries that look like code fragments (contain parens, equals, dots as methods)
|
|
2192
|
+
if any(c in base_cmd for c in ('(', ')', '=', '{', '}')) and not base_cmd.startswith('.'):
|
|
2193
|
+
continue
|
|
2194
|
+
# Skip entries that are clearly not commands (capitalized status words, text fragments)
|
|
2195
|
+
if base_cmd[0].isupper() and base_cmd.isalpha() and base_cmd not in ('PATH', 'HOME'):
|
|
2196
|
+
continue
|
|
2197
|
+
# Skip common text fragments that get misidentified as commands
|
|
2198
|
+
junk_tokens = {'version', 'total', 'package', 'success', 'error', 'reading',
|
|
2199
|
+
'editing', 'done', 'warning', 'info', 'note', 'output'}
|
|
2200
|
+
if base_cmd.lower() in junk_tokens:
|
|
2201
|
+
continue
|
|
2202
|
+
|
|
2203
|
+
# Tokenize the command for subcommand/description generation
|
|
2204
|
+
cmd_tokens = cmd_str.split() if cmd_str else []
|
|
2205
|
+
|
|
2184
2206
|
# Look up COMMAND_DB info for this command
|
|
2185
2207
|
cmd_info = COMMAND_DB.get(base_cmd, {})
|
|
2186
2208
|
kb_flags = get_flags_for_command(base_cmd)
|
|
2187
2209
|
|
|
2188
2210
|
# Convert flags to expected format WITH descriptions from knowledge base
|
|
2211
|
+
# Filter out non-flag tokens: bare dashes, numeric args (-5, -30), trailing colons
|
|
2212
|
+
import re
|
|
2189
2213
|
raw_flags = cmd.get('flags', [])
|
|
2190
2214
|
formatted_flags = []
|
|
2215
|
+
seen_flags = set()
|
|
2191
2216
|
for f in raw_flags:
|
|
2217
|
+
flag_name = f.get('flag', '') if isinstance(f, dict) else f
|
|
2218
|
+
# Skip bare dash, numeric-only flags (-5, -30), and artifact flags with colons
|
|
2219
|
+
if not flag_name or flag_name == '-' or flag_name.endswith(':'):
|
|
2220
|
+
continue
|
|
2221
|
+
if re.match(r'^-\d+$', flag_name):
|
|
2222
|
+
continue
|
|
2223
|
+
# Deduplicate flags within same command
|
|
2224
|
+
if flag_name in seen_flags:
|
|
2225
|
+
continue
|
|
2226
|
+
seen_flags.add(flag_name)
|
|
2227
|
+
|
|
2192
2228
|
if isinstance(f, dict) and 'flag' in f:
|
|
2193
|
-
# Already formatted - but enrich description if empty
|
|
2194
|
-
flag_name = f.get('flag', '')
|
|
2195
2229
|
flag_desc = f.get('description', '')
|
|
2196
2230
|
if not flag_desc and flag_name in kb_flags:
|
|
2197
2231
|
flag_desc = kb_flags[flag_name]
|
|
2198
2232
|
formatted_flags.append({'flag': flag_name, 'description': flag_desc})
|
|
2199
2233
|
elif isinstance(f, str):
|
|
2200
|
-
# Raw flag string - look up description from knowledge base
|
|
2201
2234
|
flag_desc = kb_flags.get(f, '')
|
|
2202
|
-
# For combined flags like -la,
|
|
2235
|
+
# For combined flags like -la, decompose into individual flags
|
|
2203
2236
|
if not flag_desc and len(f) > 2 and f.startswith('-') and not f.startswith('--'):
|
|
2204
2237
|
char_descs = []
|
|
2205
2238
|
for char in f[1:]:
|
|
@@ -2208,17 +2241,117 @@ def generate_html_files(
|
|
|
2208
2241
|
char_descs.append(f'{single}: {kb_flags[single]}')
|
|
2209
2242
|
if char_descs:
|
|
2210
2243
|
flag_desc = '; '.join(char_descs)
|
|
2244
|
+
# For find-style flags (-name, -type, -path, -maxdepth), add descriptions
|
|
2245
|
+
if not flag_desc:
|
|
2246
|
+
find_flags = {
|
|
2247
|
+
'-name': 'Match files by name pattern',
|
|
2248
|
+
'-type': 'Filter by file type (f=file, d=directory)',
|
|
2249
|
+
'-path': 'Match files by path pattern',
|
|
2250
|
+
'-maxdepth': 'Limit directory recursion depth',
|
|
2251
|
+
'-mindepth': 'Set minimum directory depth',
|
|
2252
|
+
'-exec': 'Execute command on each match',
|
|
2253
|
+
'-not': 'Negate the following expression',
|
|
2254
|
+
'-size': 'Match files by size',
|
|
2255
|
+
'-mtime': 'Match by modification time',
|
|
2256
|
+
'-perm': 'Match by file permissions',
|
|
2257
|
+
'-ls': 'List matched files in ls -l format',
|
|
2258
|
+
'-delete': 'Delete matched files',
|
|
2259
|
+
'-print': 'Print matched file paths',
|
|
2260
|
+
}
|
|
2261
|
+
flag_desc = find_flags.get(f, '')
|
|
2262
|
+
# For common CLI flags without KB entries
|
|
2263
|
+
if not flag_desc:
|
|
2264
|
+
common_flags = {
|
|
2265
|
+
'--help': 'Show help and usage information',
|
|
2266
|
+
'--version': 'Show version number',
|
|
2267
|
+
'--verbose': 'Enable verbose output',
|
|
2268
|
+
'--dry-run': 'Preview changes without executing',
|
|
2269
|
+
'--output': 'Specify output file or directory',
|
|
2270
|
+
'--open': 'Open result in default application',
|
|
2271
|
+
'--stat': 'Show diffstat summary of changes',
|
|
2272
|
+
'--sessions': 'Number of sessions to process',
|
|
2273
|
+
'--title': 'Set custom title',
|
|
2274
|
+
'--no-open': 'Skip auto-opening in browser',
|
|
2275
|
+
'--from': 'Specify input source path',
|
|
2276
|
+
'-s': 'Silent/short output mode',
|
|
2277
|
+
'-n': 'Numeric/count or line number',
|
|
2278
|
+
'-c': 'Execute command string or count',
|
|
2279
|
+
'-g': 'Global scope',
|
|
2280
|
+
'-p': 'Preserve attributes or port',
|
|
2281
|
+
'-o': 'Output file',
|
|
2282
|
+
'-P': 'No dereference (physical path)',
|
|
2283
|
+
}
|
|
2284
|
+
flag_desc = common_flags.get(f, '')
|
|
2211
2285
|
formatted_flags.append({'flag': f, 'description': flag_desc})
|
|
2212
2286
|
|
|
2213
|
-
#
|
|
2287
|
+
# Generate a contextual description that differentiates commands with the same base
|
|
2214
2288
|
session_desc = cmd.get('description', '')
|
|
2215
2289
|
kb_desc = cmd_info.get('description', '')
|
|
2216
|
-
|
|
2290
|
+
|
|
2291
|
+
# Build a specific description from the actual command content
|
|
2292
|
+
args_list = cmd.get('args', [])
|
|
2293
|
+
flag_list = [fl.get('flag', '') if isinstance(fl, dict) else str(fl) for fl in formatted_flags]
|
|
2294
|
+
contextual_desc = ''
|
|
2295
|
+
|
|
2296
|
+
# For inline code execution (python -c, bash -c), summarize the code snippet
|
|
2297
|
+
if base_cmd in ('python', 'python3', 'bash', 'sh', 'node') and '-c' in flag_list:
|
|
2298
|
+
# Extract the inline code from the full command after -c
|
|
2299
|
+
c_idx = cmd_str.find('-c')
|
|
2300
|
+
if c_idx >= 0:
|
|
2301
|
+
raw_code = cmd_str[c_idx + 2:].strip().strip('"').strip("'")
|
|
2302
|
+
# Split on actual newlines before collapsing
|
|
2303
|
+
code_lines = [l.strip() for l in raw_code.splitlines() if l.strip()]
|
|
2304
|
+
# Find first non-import line for a distinctive preview
|
|
2305
|
+
action_lines = [l for l in code_lines if not l.startswith(('import ', 'from ', '#'))]
|
|
2306
|
+
if action_lines:
|
|
2307
|
+
code_part = ' '.join(action_lines[0].split())[:60]
|
|
2308
|
+
elif code_lines:
|
|
2309
|
+
# All imports - show what's being imported
|
|
2310
|
+
code_part = ' '.join(code_lines[0].split())[:60]
|
|
2311
|
+
else:
|
|
2312
|
+
code_part = ''
|
|
2313
|
+
if code_part:
|
|
2314
|
+
contextual_desc = f"{base_cmd} -c: {code_part}{'...' if len(code_part) >= 60 else ''}"
|
|
2315
|
+
|
|
2316
|
+
# For commands with subcommands (git, npm, docker, etc.), use subcommand context
|
|
2317
|
+
if not contextual_desc and cmd_tokens and len(cmd_tokens) > 1:
|
|
2318
|
+
subcmd_token = next((t for t in cmd_tokens[1:] if not t.startswith('-') and not t.startswith('"') and not t.startswith("'")), '')
|
|
2319
|
+
if subcmd_token and subcmd_token != base_cmd:
|
|
2320
|
+
subcmd_info = cmd_info.get('subcommands', {}).get(subcmd_token, '')
|
|
2321
|
+
if subcmd_info:
|
|
2322
|
+
contextual_desc = f"{base_cmd} {subcmd_token}: {subcmd_info}"
|
|
2323
|
+
else:
|
|
2324
|
+
contextual_desc = f"{base_cmd} {subcmd_token}"
|
|
2325
|
+
# Add meaningful args (skip very long ones, quotes, code)
|
|
2326
|
+
short_args = [a for a in args_list if len(str(a)) < 40 and a != subcmd_token and not a.startswith('"')]
|
|
2327
|
+
if short_args:
|
|
2328
|
+
contextual_desc += f" ({', '.join(short_args[:3])})"
|
|
2329
|
+
|
|
2330
|
+
# For commands with flags but no subcommand, describe with flags
|
|
2331
|
+
if not contextual_desc and flag_list:
|
|
2332
|
+
flag_summary = ', '.join(flag_list[:3])
|
|
2333
|
+
short_args = [a for a in args_list if len(str(a)) < 40]
|
|
2334
|
+
if short_args:
|
|
2335
|
+
contextual_desc = f"{base_cmd} {flag_summary} on {', '.join(short_args[:2])}"
|
|
2336
|
+
else:
|
|
2337
|
+
contextual_desc = f"{base_cmd} with {flag_summary}"
|
|
2338
|
+
|
|
2339
|
+
# For simple commands with just args
|
|
2340
|
+
if not contextual_desc and args_list:
|
|
2341
|
+
short_args = [a for a in args_list if len(str(a)) < 40]
|
|
2342
|
+
if short_args:
|
|
2343
|
+
contextual_desc = f"{base_cmd} {' '.join(short_args[:3])}"
|
|
2344
|
+
|
|
2345
|
+
# Priority: contextual > session > knowledge base
|
|
2346
|
+
if contextual_desc:
|
|
2347
|
+
description = contextual_desc
|
|
2348
|
+
elif session_desc:
|
|
2349
|
+
description = session_desc
|
|
2350
|
+
else:
|
|
2351
|
+
description = kb_desc if kb_desc else f"Run {base_cmd} command"
|
|
2217
2352
|
|
|
2218
2353
|
# Get subcommand info (for commands like git, docker, npm)
|
|
2219
2354
|
subcommands = cmd_info.get('subcommands', {})
|
|
2220
|
-
# Try to identify the subcommand from the full command
|
|
2221
|
-
cmd_tokens = cmd_str.split() if cmd_str else []
|
|
2222
2355
|
subcommand_desc = ''
|
|
2223
2356
|
if subcommands and len(cmd_tokens) > 1:
|
|
2224
2357
|
for token in cmd_tokens[1:]:
|
|
@@ -91,6 +91,8 @@ CATEGORY_MAPPINGS: Dict[str, Set[str]] = {
|
|
|
91
91
|
"history", "fc", "true", "false", "test", "[", "[[", "exit",
|
|
92
92
|
"return", "break", "continue", "shift", "getopts", "trap",
|
|
93
93
|
"ulimit", "times", "let", ":", "compgen", "complete", "compopt",
|
|
94
|
+
"cmd.exe", "cmd", "start", "where", "type",
|
|
95
|
+
"session-slides", "learn-bash", "bash-learner", "claude",
|
|
94
96
|
},
|
|
95
97
|
}
|
|
96
98
|
|