learn_bash_from_session_data 1.0.1 → 1.0.2

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/bin/learn-bash.js CHANGED
@@ -107,12 +107,56 @@ ${colors.bright}SESSION LOCATION:${colors.reset}
107
107
  `);
108
108
  }
109
109
 
110
+ /**
111
+ * Check if running in WSL
112
+ */
113
+ function isWSL() {
114
+ try {
115
+ const version = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
116
+ return version.includes('microsoft') || version.includes('wsl');
117
+ } catch (e) {
118
+ return false;
119
+ }
120
+ }
121
+
110
122
  /**
111
123
  * Get the Claude projects directory path
124
+ * Handles WSL by checking both Linux and Windows paths
112
125
  */
113
126
  function getClaudeProjectsDir() {
114
127
  const homeDir = os.homedir();
115
- return path.join(homeDir, '.claude', 'projects');
128
+ const linuxPath = path.join(homeDir, '.claude', 'projects');
129
+
130
+ // Check if running in WSL
131
+ if (isWSL()) {
132
+ // Try Windows user directories
133
+ const windowsUsers = '/mnt/c/Users';
134
+ if (fs.existsSync(windowsUsers)) {
135
+ // Try current username first
136
+ const username = process.env.USER || '';
137
+ const windowsPath = path.join(windowsUsers, username, '.claude', 'projects');
138
+ if (fs.existsSync(windowsPath)) {
139
+ return windowsPath;
140
+ }
141
+
142
+ // Try to find any user with .claude folder
143
+ try {
144
+ const users = fs.readdirSync(windowsUsers, { withFileTypes: true });
145
+ for (const user of users) {
146
+ if (user.isDirectory() && !user.name.startsWith('Public') && !user.name.startsWith('Default')) {
147
+ const potentialPath = path.join(windowsUsers, user.name, '.claude', 'projects');
148
+ if (fs.existsSync(potentialPath)) {
149
+ return potentialPath;
150
+ }
151
+ }
152
+ }
153
+ } catch (e) {
154
+ // Ignore errors reading Windows users
155
+ }
156
+ }
157
+ }
158
+
159
+ return linuxPath;
116
160
  }
117
161
 
118
162
  /**
@@ -136,12 +180,18 @@ function listProjects() {
136
180
  const sessionsPath = path.join(projectPath, 'sessions');
137
181
  let sessionCount = 0;
138
182
 
183
+ // Check for sessions in sessions/ subdirectory (new structure)
139
184
  if (fs.existsSync(sessionsPath)) {
140
185
  const sessions = fs.readdirSync(sessionsPath)
141
186
  .filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
142
- sessionCount = sessions.length;
187
+ sessionCount += sessions.length;
143
188
  }
144
189
 
190
+ // Also check for .jsonl files directly in project directory (old structure)
191
+ const directJsonl = fs.readdirSync(projectPath)
192
+ .filter(f => f.endsWith('.jsonl') && !f.startsWith('.'));
193
+ sessionCount += directJsonl.length;
194
+
145
195
  return {
146
196
  name: entry.name,
147
197
  path: projectPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "learn_bash_from_session_data",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Learn bash from your Claude Code sessions - extracts commands and generates interactive HTML lessons",
5
5
  "main": "bin/learn-bash.js",
6
6
  "bin": {
package/scripts/main.py CHANGED
@@ -22,7 +22,48 @@ if sys.version_info < (3, 8):
22
22
  # Constants
23
23
  DEFAULT_OUTPUT_DIR = "./bash-learner-output/"
24
24
  MAX_UNIQUE_COMMANDS = 500
25
- SESSIONS_BASE_PATH = Path.home() / ".claude" / "projects"
25
+
26
+
27
+ def get_sessions_base_path() -> Path:
28
+ """
29
+ Get the base path for Claude session files.
30
+
31
+ Handles WSL by checking both Linux and Windows paths.
32
+ """
33
+ # Standard Linux/Mac path
34
+ linux_path = Path.home() / ".claude" / "projects"
35
+
36
+ # Check if we're in WSL
37
+ is_wsl = False
38
+ try:
39
+ with open("/proc/version", "r") as f:
40
+ is_wsl = "microsoft" in f.read().lower() or "wsl" in f.read().lower()
41
+ except (FileNotFoundError, PermissionError):
42
+ pass
43
+
44
+ if is_wsl:
45
+ # Try to find Windows user directory
46
+ # Check common Windows user paths via /mnt/c/Users/
47
+ windows_users = Path("/mnt/c/Users")
48
+ if windows_users.exists():
49
+ # Try current username first
50
+ username = os.environ.get("USER", "")
51
+ windows_path = windows_users / username / ".claude" / "projects"
52
+ if windows_path.exists():
53
+ return windows_path
54
+
55
+ # Try to find any user with .claude folder
56
+ for user_dir in windows_users.iterdir():
57
+ if user_dir.is_dir() and not user_dir.name.startswith(("Public", "Default")):
58
+ potential_path = user_dir / ".claude" / "projects"
59
+ if potential_path.exists():
60
+ return potential_path
61
+
62
+ # Fall back to Linux path
63
+ return linux_path
64
+
65
+
66
+ SESSIONS_BASE_PATH = get_sessions_base_path()
26
67
 
27
68
 
28
69
  def get_session_metadata(session_path: Path) -> Dict:
@@ -74,7 +115,8 @@ def format_file_size(size_bytes: int) -> str:
74
115
 
75
116
  def discover_sessions(
76
117
  project_filter: Optional[str] = None,
77
- limit: Optional[int] = None
118
+ limit: Optional[int] = None,
119
+ sessions_dir: Optional[Path] = None
78
120
  ) -> List[Dict]:
79
121
  """
80
122
  Discover available Claude session files.
@@ -82,25 +124,35 @@ def discover_sessions(
82
124
  Args:
83
125
  project_filter: Optional filter for project path substring
84
126
  limit: Maximum number of sessions to return
127
+ sessions_dir: Custom sessions directory (defaults to auto-detected)
85
128
 
86
129
  Returns:
87
130
  List of session metadata dictionaries, sorted by modification time (newest first)
88
131
  """
89
132
  sessions = []
133
+ base_path = sessions_dir or SESSIONS_BASE_PATH
90
134
 
91
- if not SESSIONS_BASE_PATH.exists():
135
+ if not base_path.exists():
92
136
  return sessions
93
137
 
94
- # Find all session files
95
- for project_dir in SESSIONS_BASE_PATH.iterdir():
138
+ # Find all session files - check both old and new directory structures
139
+ # New structure: projects/<hash>/sessions/*.jsonl
140
+ # Old structure: projects/<hash>/*.jsonl
141
+ for project_dir in base_path.iterdir():
96
142
  if not project_dir.is_dir():
97
143
  continue
98
144
 
99
- sessions_dir = project_dir / "sessions"
100
- if not sessions_dir.exists():
101
- continue
145
+ # Check for sessions subdirectory (new structure)
146
+ sessions_subdir = project_dir / "sessions"
147
+ if sessions_subdir.exists():
148
+ for session_file in sessions_subdir.glob("*.jsonl"):
149
+ metadata = get_session_metadata(session_file)
150
+ if project_filter and project_filter.lower() not in str(session_file).lower():
151
+ continue
152
+ sessions.append(metadata)
102
153
 
103
- for session_file in sessions_dir.glob("*.jsonl"):
154
+ # Also check for .jsonl files directly in project dir (old structure)
155
+ for session_file in project_dir.glob("*.jsonl"):
104
156
  metadata = get_session_metadata(session_file)
105
157
 
106
158
  # Apply project filter if specified
@@ -120,19 +172,23 @@ def discover_sessions(
120
172
  return sessions
121
173
 
122
174
 
123
- def list_sessions(project_filter: Optional[str] = None) -> None:
175
+ def list_sessions(project_filter: Optional[str] = None, sessions_dir: Optional[Path] = None) -> None:
124
176
  """
125
177
  Display available sessions in a formatted table.
126
178
 
127
179
  Args:
128
180
  project_filter: Optional filter for project path substring
181
+ sessions_dir: Custom sessions directory
129
182
  """
130
- sessions = discover_sessions(project_filter=project_filter)
183
+ base_path = sessions_dir or SESSIONS_BASE_PATH
184
+ sessions = discover_sessions(project_filter=project_filter, sessions_dir=sessions_dir)
131
185
 
132
186
  if not sessions:
133
187
  print("\nNo session files found.")
134
- print(f"\nExpected location: {SESSIONS_BASE_PATH}/<project-hash>/sessions/*.jsonl")
188
+ print(f"\nSearched in: {base_path}")
189
+ print(f"\nExpected structure: <sessions-dir>/<project-hash>/*.jsonl")
135
190
  print("\nMake sure you have Claude session data available.")
191
+ print("\nTip: On WSL, try using -s /mnt/c/Users/<username>/.claude/projects/")
136
192
  return
137
193
 
138
194
  print(f"\n{'='*80}")
@@ -371,6 +427,12 @@ Examples:
371
427
  help='Enable verbose output'
372
428
  )
373
429
 
430
+ parser.add_argument(
431
+ '-s', '--sessions-dir',
432
+ type=str,
433
+ help=f'Sessions directory (default: auto-detected, currently {SESSIONS_BASE_PATH})'
434
+ )
435
+
374
436
  return parser.parse_args()
375
437
 
376
438
 
@@ -383,9 +445,12 @@ def main() -> int:
383
445
  """
384
446
  args = parse_arguments()
385
447
 
448
+ # Handle custom sessions directory
449
+ custom_sessions_dir = Path(args.sessions_dir) if args.sessions_dir else None
450
+
386
451
  # Handle --list
387
452
  if args.list:
388
- list_sessions(project_filter=args.project)
453
+ list_sessions(project_filter=args.project, sessions_dir=custom_sessions_dir)
389
454
  return 0
390
455
 
391
456
  # Determine which sessions to process
@@ -404,16 +469,20 @@ def main() -> int:
404
469
 
405
470
  else:
406
471
  # Discover and select sessions
472
+ base_path = custom_sessions_dir or SESSIONS_BASE_PATH
407
473
  sessions = discover_sessions(
408
474
  project_filter=args.project,
409
- limit=args.sessions
475
+ limit=args.sessions,
476
+ sessions_dir=custom_sessions_dir
410
477
  )
411
478
 
412
479
  if not sessions:
413
480
  print("\nNo session files found.")
414
- print(f"\nExpected location: {SESSIONS_BASE_PATH}/<project-hash>/sessions/*.jsonl")
481
+ print(f"\nSearched in: {base_path}")
482
+ print(f"\nExpected structure: <sessions-dir>/<project-hash>/*.jsonl")
415
483
  print("\nTo create session data, use Claude Code and your sessions will be stored automatically.")
416
484
  print("\nUse --list to see available sessions once you have some.")
485
+ print("\nTip: On WSL, try using -s /mnt/c/Users/<username>/.claude/projects/")
417
486
  return 1
418
487
 
419
488
  sessions_to_process = sessions