claude-code-templates 1.24.17 → 1.26.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.
@@ -78,6 +78,7 @@ program
78
78
  .option('--sandbox <provider>', 'execute Claude Code in isolated sandbox environment (e.g., e2b)')
79
79
  .option('--e2b-api-key <key>', 'E2B API key for sandbox execution (alternative to environment variable)')
80
80
  .option('--anthropic-api-key <key>', 'Anthropic API key for Claude Code (alternative to environment variable)')
81
+ .option('--clone-session <url>', 'download and import a shared Claude Code session from URL')
81
82
  .action(async (options) => {
82
83
  try {
83
84
  // Only show banner for non-agent-list commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.24.17",
3
+ "version": "1.26.0",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -74,6 +74,7 @@
74
74
  "js-yaml": "^4.1.0",
75
75
  "open": "^8.4.2",
76
76
  "ora": "^5.4.1",
77
+ "qrcode": "^1.5.3",
77
78
  "uuid": "^11.1.0",
78
79
  "ws": "^8.18.3"
79
80
  },
@@ -704,31 +704,82 @@
704
704
  box-shadow: 0 0 8px rgba(255, 170, 0, 0.6);
705
705
  }
706
706
 
707
- /* Search toggle button in header */
708
- .search-toggle-btn {
709
- background: var(--bg-tertiary);
710
- border: 1px solid var(--border-primary);
711
- color: var(--terminal-orange);
712
- padding: 6px 12px;
713
- border-radius: 6px;
707
+ /* Action buttons group - unified design with orange accent */
708
+ .action-buttons-group {
709
+ display: inline-flex;
710
+ border-radius: 8px;
711
+ overflow: hidden;
712
+ border: 2px solid rgba(255, 170, 0, 0.4);
713
+ background: rgba(255, 170, 0, 0.08);
714
+ box-shadow: 0 2px 8px rgba(255, 170, 0, 0.15);
715
+ }
716
+
717
+ .action-btn {
718
+ background: transparent;
719
+ border: none;
720
+ border-right: 1px solid rgba(255, 170, 0, 0.2);
721
+ color: var(--text-primary);
722
+ padding: 10px 18px;
714
723
  cursor: pointer;
715
724
  font-size: 0.875rem;
725
+ font-weight: 600;
716
726
  transition: all 0.2s ease;
717
727
  display: flex;
718
728
  align-items: center;
719
- gap: 6px;
729
+ gap: 8px;
730
+ white-space: nowrap;
720
731
  }
721
732
 
722
- .search-toggle-btn:hover {
723
- background: var(--terminal-orange);
724
- color: white;
725
- border-color: var(--terminal-orange);
733
+ .action-btn:last-child {
734
+ border-right: none;
726
735
  }
727
736
 
728
- .search-toggle-btn.active {
729
- background: var(--terminal-orange);
730
- color: white;
731
- border-color: var(--terminal-orange);
737
+ .action-btn svg {
738
+ width: 15px;
739
+ height: 15px;
740
+ transition: all 0.2s ease;
741
+ stroke: var(--text-primary);
742
+ }
743
+
744
+ .action-btn:hover {
745
+ background: rgba(255, 170, 0, 0.12);
746
+ transform: translateY(-1px);
747
+ }
748
+
749
+ .action-btn.resume-btn:hover {
750
+ background: rgba(63, 185, 80, 0.15);
751
+ color: rgba(63, 185, 80, 1);
752
+ }
753
+
754
+ .action-btn.resume-btn:hover svg {
755
+ stroke: rgba(63, 185, 80, 1);
756
+ }
757
+
758
+ .action-btn.download-btn:hover {
759
+ background: rgba(59, 130, 246, 0.15);
760
+ color: rgba(59, 130, 246, 1);
761
+ }
762
+
763
+ .action-btn.download-btn:hover svg {
764
+ stroke: rgba(59, 130, 246, 1);
765
+ }
766
+
767
+ .action-btn.search-btn:hover {
768
+ background: rgba(251, 146, 60, 0.15);
769
+ color: rgba(251, 146, 60, 1);
770
+ }
771
+
772
+ .action-btn.search-btn:hover svg {
773
+ stroke: rgba(251, 146, 60, 1);
774
+ }
775
+
776
+ .action-btn.search-btn.active {
777
+ background: rgba(251, 146, 60, 0.2);
778
+ color: rgba(251, 146, 60, 1);
779
+ }
780
+
781
+ .action-btn.search-btn.active svg {
782
+ stroke: rgba(251, 146, 60, 1);
732
783
  }
733
784
 
734
785
  /* Conversations list */
@@ -816,6 +867,88 @@
816
867
  margin-top: 4px;
817
868
  }
818
869
 
870
+ /* Project grouping styles */
871
+ .project-group {
872
+ border-bottom: 1px solid var(--border-secondary);
873
+ }
874
+
875
+ .project-header {
876
+ display: flex;
877
+ align-items: center;
878
+ padding: 16px 20px;
879
+ background: var(--bg-secondary);
880
+ border-bottom: 1px solid var(--border-primary);
881
+ cursor: pointer;
882
+ transition: background-color 0.2s ease;
883
+ position: sticky;
884
+ top: 0;
885
+ z-index: 10;
886
+ }
887
+
888
+ .project-header:hover {
889
+ background: var(--bg-tertiary);
890
+ }
891
+
892
+ .project-avatar {
893
+ width: 44px;
894
+ height: 44px;
895
+ border-radius: 8px;
896
+ background: linear-gradient(135deg, var(--terminal-orange) 0%, #e67e22 100%);
897
+ display: flex;
898
+ align-items: center;
899
+ justify-content: center;
900
+ font-size: 1.2rem;
901
+ margin-right: 12px;
902
+ flex-shrink: 0;
903
+ color: var(--bg-primary);
904
+ font-weight: bold;
905
+ }
906
+
907
+ .project-info {
908
+ flex: 1;
909
+ min-width: 0;
910
+ }
911
+
912
+ .project-name {
913
+ font-weight: 600;
914
+ color: var(--text-primary);
915
+ font-size: 1rem;
916
+ margin-bottom: 2px;
917
+ }
918
+
919
+ .project-count {
920
+ color: var(--text-secondary);
921
+ font-size: 0.85rem;
922
+ }
923
+
924
+ .project-toggle {
925
+ margin-left: 8px;
926
+ color: var(--text-secondary);
927
+ transition: transform 0.2s ease;
928
+ }
929
+
930
+ .toggle-icon {
931
+ transition: transform 0.2s ease;
932
+ }
933
+
934
+ .toggle-icon.expanded {
935
+ transform: rotate(180deg);
936
+ }
937
+
938
+ /* Smaller avatar for conversations within groups */
939
+ .conversation-avatar-small {
940
+ width: 36px;
941
+ height: 36px;
942
+ font-size: 0.85rem;
943
+ background: var(--bg-tertiary);
944
+ color: var(--text-primary);
945
+ }
946
+
947
+ /* Indent conversations within project groups */
948
+ .project-group .conversation-item {
949
+ padding-left: 36px;
950
+ }
951
+
819
952
  .conversation-state {
820
953
  font-size: 0.7rem;
821
954
  padding: 3px 8px;
@@ -1353,13 +1486,16 @@
1353
1486
  align-items: center;
1354
1487
  justify-content: center;
1355
1488
  flex: 0 0 auto;
1489
+ gap: 8px;
1356
1490
  }
1357
1491
 
1492
+
1358
1493
  .header-right {
1359
1494
  display: flex;
1360
1495
  align-items: center;
1361
1496
  justify-content: flex-end;
1362
1497
  flex: 1;
1498
+ gap: 12px;
1363
1499
  }
1364
1500
 
1365
1501
  .chat-view-back {
@@ -1904,20 +2040,36 @@
1904
2040
 
1905
2041
  </button>
1906
2042
  <div class="chat-view-info">
1907
- <h2 class="chat-view-title" id="chatViewTitle">Select a conversation</h2>
2043
+ <h2 class="chat-view-title" id="chatViewTitle">Select a session</h2>
1908
2044
  <p class="chat-view-subtitle" id="chatViewSubtitle"></p>
1909
2045
  </div>
1910
2046
  </div>
1911
2047
  <div class="header-center">
1912
- <button class="header-btn resume-btn" id="resumeConversation" style="display: none;" onclick="resumeConversationWithClaude()">
1913
- ▶️ Resume
1914
- </button>
2048
+ <div class="action-buttons-group" style="display: none;" id="actionButtonsGroup">
2049
+ <button class="action-btn resume-btn" id="resumeConversation" onclick="resumeConversationWithClaude()">
2050
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2051
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
2052
+ </svg>
2053
+ <span>Resume</span>
2054
+ </button>
2055
+ <button class="action-btn download-btn" id="downloadConversation" onclick="downloadConversation()">
2056
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2057
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
2058
+ <polyline points="7 10 12 15 17 10"></polyline>
2059
+ <line x1="12" y1="15" x2="12" y2="3"></line>
2060
+ </svg>
2061
+ <span>Download</span>
2062
+ </button>
2063
+ <button class="action-btn search-btn" id="chatSearchToggle">
2064
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2065
+ <circle cx="11" cy="11" r="8"></circle>
2066
+ <path d="m21 21-4.35-4.35"></path>
2067
+ </svg>
2068
+ <span>Search</span>
2069
+ </button>
2070
+ </div>
1915
2071
  </div>
1916
2072
  <div class="header-right">
1917
- <button class="search-toggle-btn" id="chatSearchToggle">
1918
- <span>🔍</span>
1919
- <span>Search</span>
1920
- </button>
1921
2073
  <div class="tools-toggle" id="toolsToggle">
1922
2074
  <span class="tools-toggle-label" onclick="document.getElementById('showToolsSwitch').click()">Show Tools</span>
1923
2075
  <label class="toggle-switch">
@@ -1991,6 +2143,58 @@
1991
2143
  </div>
1992
2144
  </div>
1993
2145
 
2146
+ <!-- Download Context Modal -->
2147
+ <div class="modal-overlay" id="downloadModal">
2148
+ <div class="modal">
2149
+ <div class="modal-header" style="background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);">
2150
+ <span class="modal-icon">📥</span>
2151
+ <h3 class="modal-title">Download Conversation Context</h3>
2152
+ </div>
2153
+ <div style="padding: 20px;">
2154
+ <p class="modal-description" style="margin-bottom: 16px; line-height: 1.5;">
2155
+ Download this conversation as a <strong>Markdown context file</strong> that Claude Code can read to continue your work.
2156
+ </p>
2157
+
2158
+ <div style="background: rgba(59, 130, 246, 0.08); border-left: 3px solid #3b82f6; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
2159
+ <h4 style="color: #3b82f6; margin: 0 0 8px 0; font-size: 14px;">📋 What's included</h4>
2160
+ <p style="margin: 0; font-size: 13px; color: var(--text-secondary); line-height: 1.4;">
2161
+ Full conversation history (last <strong>100 messages</strong>) formatted for Claude Code to understand
2162
+ </p>
2163
+ </div>
2164
+
2165
+ <div style="background: rgba(16, 185, 129, 0.08); border-left: 3px solid #10b981; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
2166
+ <h4 style="color: #10b981; margin: 0 0 8px 0; font-size: 14px;">💡 How to continue the conversation</h4>
2167
+ <p style="margin: 0 0 12px 0; font-size: 13px; color: var(--text-secondary); line-height: 1.4;">
2168
+ After downloading, use one of these commands to load the context:
2169
+ </p>
2170
+
2171
+ <div style="margin-bottom: 10px;">
2172
+ <p style="margin: 0 0 4px 0; font-size: 12px; font-weight: 600; color: #10b981;">Option 1: Direct reference</p>
2173
+ <code style="display: block; background: rgba(0,0,0,0.5); padding: 8px; border-radius: 4px; font-size: 12px; color: #e5e7eb; overflow-x: auto; white-space: nowrap;">claude "read @context-file.md and continue"</code>
2174
+ </div>
2175
+
2176
+ <div>
2177
+ <p style="margin: 0 0 4px 0; font-size: 12px; font-weight: 600; color: #10b981;">Option 2: Using cat (pipe)</p>
2178
+ <code style="display: block; background: rgba(0,0,0,0.5); padding: 8px; border-radius: 4px; font-size: 12px; color: #e5e7eb; overflow-x: auto; white-space: nowrap;">cat context-file.md | claude</code>
2179
+ </div>
2180
+ </div>
2181
+
2182
+ <div style="background: rgba(100, 116, 139, 0.08); border-left: 3px solid #64748b; padding: 12px; border-radius: 6px;">
2183
+ <p style="margin: 0; font-size: 12px; color: var(--text-secondary); line-height: 1.4;">
2184
+ 💾 <strong>Tip:</strong> Save the downloaded file in your project directory for easy access
2185
+ </p>
2186
+ </div>
2187
+ </div>
2188
+
2189
+ <div class="modal-actions">
2190
+ <button class="modal-btn secondary" onclick="closeDownloadModal()">Cancel</button>
2191
+ <button class="modal-btn primary" onclick="proceedWithDownload()" style="background: #3b82f6;">
2192
+ Download Context File
2193
+ </button>
2194
+ </div>
2195
+ </div>
2196
+ </div>
2197
+
1994
2198
  <!-- Import WebSocket and Data Services -->
1995
2199
  <script src="services/WebSocketService.js"></script>
1996
2200
  <script src="services/DataService.js"></script>
@@ -2239,56 +2443,101 @@
2239
2443
 
2240
2444
  renderConversations(conversations, states = {}) {
2241
2445
  const conversationsList = document.getElementById('conversationsList');
2242
-
2446
+
2243
2447
  if (conversations.length === 0) {
2244
2448
  conversationsList.innerHTML = `
2245
2449
  <div class="no-conversations">
2246
2450
  <div class="no-conversations-icon">💬</div>
2247
- <h3>No conversations found</h3>
2248
- <p>Start a conversation with Claude Code to see it here</p>
2451
+ <h3>No sessions found</h3>
2452
+ <p>Start a session with Claude Code to see it here</p>
2249
2453
  </div>
2250
2454
  `;
2251
2455
  return;
2252
2456
  }
2253
2457
 
2254
- conversationsList.innerHTML = conversations.map(conv => {
2255
- const state = states[conv.id] || 'inactive';
2256
- const stateClass = this.getStateClass(state);
2257
- const stateLabel = this.getStateLabel(state);
2258
-
2259
- // Debug logging for first few conversations
2260
- console.log(`🔍 Conversation ${conv.id.slice(-8)}: State="${state}" -> Label="${stateLabel}" Class="${stateClass}"`);
2261
-
2262
- const lastActivity = this.formatRelativeTime(new Date(conv.lastModified));
2263
- const messageCount = conv.messageCount || 0;
2458
+ // Group conversations by project
2459
+ const groupedByProject = conversations.reduce((groups, conv) => {
2264
2460
  const projectName = conv.project || 'Unknown Project';
2265
- const conversationId = conv.id.slice(-8);
2266
-
2267
- // Get first letter of project name for avatar
2461
+ if (!groups[projectName]) {
2462
+ groups[projectName] = [];
2463
+ }
2464
+ groups[projectName].push(conv);
2465
+ return groups;
2466
+ }, {});
2467
+
2468
+ // Sort projects alphabetically
2469
+ const sortedProjects = Object.keys(groupedByProject).sort();
2470
+
2471
+ // Initialize expanded state if not exists
2472
+ if (!this.expandedProjects) {
2473
+ this.expandedProjects = new Set(); // All collapsed by default
2474
+ }
2475
+
2476
+ // Render grouped conversations
2477
+ conversationsList.innerHTML = sortedProjects.map(projectName => {
2478
+ const projectConversations = groupedByProject[projectName];
2479
+ const isExpanded = this.expandedProjects.has(projectName);
2268
2480
  const firstLetter = projectName.charAt(0).toUpperCase();
2269
-
2270
- return `
2271
- <div class="conversation-item" data-conversation-id="${conv.id}">
2272
- <div class="conversation-avatar">
2273
- ${firstLetter}
2481
+ const conversationCount = projectConversations.length;
2482
+
2483
+ // Render conversations for this project
2484
+ const conversationsHTML = projectConversations.map(conv => {
2485
+ const state = states[conv.id] || 'inactive';
2486
+ const stateClass = this.getStateClass(state);
2487
+ const stateLabel = this.getStateLabel(state);
2488
+
2489
+ const lastActivity = this.formatRelativeTime(new Date(conv.lastModified));
2490
+ const messageCount = conv.messageCount || 0;
2491
+ const conversationId = conv.id.slice(-8);
2492
+
2493
+ return `
2494
+ <div class="conversation-item" data-conversation-id="${conv.id}" style="display: ${isExpanded ? 'flex' : 'none'}">
2495
+ <div class="conversation-avatar conversation-avatar-small">
2496
+ ${conversationId.substring(0, 2).toUpperCase()}
2497
+ </div>
2498
+ <div class="conversation-content">
2499
+ <div class="conversation-header">
2500
+ <div class="conversation-name">Session ${conversationId}</div>
2501
+ <div class="conversation-time">${lastActivity}</div>
2502
+ </div>
2503
+ <div class="conversation-meta">
2504
+ <span class="conversation-state ${stateClass}">${stateLabel}</span>
2505
+ ${messageCount > 0 ? `<span class="message-count">${messageCount}</span>` : ''}
2506
+ </div>
2507
+ </div>
2274
2508
  </div>
2275
- <div class="conversation-content">
2276
- <div class="conversation-header">
2277
- <div class="conversation-name">${projectName}</div>
2278
- <div class="conversation-time">${lastActivity}</div>
2509
+ `;
2510
+ }).join('');
2511
+
2512
+ return `
2513
+ <div class="project-group" data-project="${projectName}">
2514
+ <div class="project-header" data-project="${projectName}">
2515
+ <div class="project-avatar">
2516
+ ${firstLetter}
2279
2517
  </div>
2280
- <div class="conversation-preview">
2281
- Conversation ${conversationId}
2518
+ <div class="project-info">
2519
+ <div class="project-name">${projectName}</div>
2520
+ <div class="project-count">${conversationCount} session${conversationCount !== 1 ? 's' : ''}</div>
2282
2521
  </div>
2283
- <div class="conversation-meta">
2284
- <span class="conversation-state ${stateClass}">${stateLabel}</span>
2285
- ${messageCount > 0 ? `<span class="message-count">${messageCount}</span>` : ''}
2522
+ <div class="project-toggle">
2523
+ <svg class="toggle-icon ${isExpanded ? 'expanded' : ''}" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
2524
+ <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
2525
+ </svg>
2286
2526
  </div>
2287
2527
  </div>
2528
+ ${conversationsHTML}
2288
2529
  </div>
2289
2530
  `;
2290
2531
  }).join('');
2291
2532
 
2533
+ // Bind project toggle events
2534
+ conversationsList.querySelectorAll('.project-header').forEach(header => {
2535
+ header.addEventListener('click', (e) => {
2536
+ const projectName = header.dataset.project;
2537
+ this.toggleProject(projectName);
2538
+ });
2539
+ });
2540
+
2292
2541
  // Bind conversation click events
2293
2542
  conversationsList.querySelectorAll('.conversation-item').forEach(item => {
2294
2543
  item.addEventListener('click', () => {
@@ -2298,6 +2547,17 @@
2298
2547
  });
2299
2548
  }
2300
2549
 
2550
+ toggleProject(projectName) {
2551
+ if (this.expandedProjects.has(projectName)) {
2552
+ this.expandedProjects.delete(projectName);
2553
+ } else {
2554
+ this.expandedProjects.add(projectName);
2555
+ }
2556
+
2557
+ // Re-render conversations with current states
2558
+ this.renderConversations(this.conversations, this.conversationStates);
2559
+ }
2560
+
2301
2561
  selectConversation(conversationId) {
2302
2562
  this.selectedConversationId = conversationId;
2303
2563
 
@@ -2322,7 +2582,7 @@
2322
2582
  const projectName = conversation.project || 'Unknown Project';
2323
2583
  const convId = conversation.id.slice(-8);
2324
2584
  chatViewTitle.textContent = projectName;
2325
- chatViewSubtitle.textContent = `Conversation ${convId}`;
2585
+ chatViewSubtitle.textContent = `Session ${convId}`;
2326
2586
 
2327
2587
  // Show chat view with animation
2328
2588
  chatView.classList.add('active');
@@ -2334,10 +2594,14 @@
2334
2594
  chatView.classList.remove('show-tools');
2335
2595
  }
2336
2596
 
2337
- // Show resume button
2597
+ // Show action buttons group
2598
+ const actionButtonsGroup = document.getElementById('actionButtonsGroup');
2599
+ actionButtonsGroup.style.display = 'inline-flex';
2600
+
2338
2601
  const resumeBtn = document.getElementById('resumeConversation');
2339
- resumeBtn.style.display = 'block';
2602
+ const downloadBtn = document.getElementById('downloadConversation');
2340
2603
  resumeBtn.setAttribute('data-conversation-id', conversationId);
2604
+ downloadBtn.setAttribute('data-conversation-id', conversationId);
2341
2605
 
2342
2606
  // Load messages (placeholder for now)
2343
2607
  this.loadChatMessages(conversationId);
@@ -2352,9 +2616,9 @@
2352
2616
  // Clean up scroll tracking when leaving conversation
2353
2617
  this.removeScrollTracking();
2354
2618
 
2355
- // Hide resume button
2356
- const resumeBtn = document.getElementById('resumeConversation');
2357
- resumeBtn.style.display = 'none';
2619
+ // Hide action buttons group
2620
+ const actionButtonsGroup = document.getElementById('actionButtonsGroup');
2621
+ actionButtonsGroup.style.display = 'none';
2358
2622
 
2359
2623
  // Remove active state from conversations
2360
2624
  document.querySelectorAll('.conversation-item').forEach(item => {
@@ -3024,14 +3288,68 @@
3024
3288
 
3025
3289
 
3026
3290
  filterConversations(searchTerm) {
3027
- const items = document.querySelectorAll('.conversation-item');
3028
- const term = searchTerm.toLowerCase();
3291
+ const term = searchTerm.toLowerCase().trim();
3292
+ const projectGroups = document.querySelectorAll('.project-group');
3293
+
3294
+ if (!term) {
3295
+ // If search is empty, collapse all projects and reset
3296
+ this.expandedProjects.clear(); // Close all projects
3029
3297
 
3030
- items.forEach(item => {
3031
- const name = item.querySelector('.conversation-name').textContent.toLowerCase();
3032
- const preview = item.querySelector('.conversation-preview').textContent.toLowerCase();
3033
- const matches = name.includes(term) || preview.includes(term);
3034
- item.style.display = matches ? 'flex' : 'none';
3298
+ projectGroups.forEach(group => {
3299
+ group.style.display = 'block';
3300
+
3301
+ // Hide all conversations
3302
+ const conversations = group.querySelectorAll('.conversation-item');
3303
+ conversations.forEach(conv => {
3304
+ conv.style.display = 'none';
3305
+ });
3306
+
3307
+ // Update toggle icon to collapsed state
3308
+ const toggleIcon = group.querySelector('.toggle-icon');
3309
+ if (toggleIcon) {
3310
+ toggleIcon.classList.remove('expanded');
3311
+ }
3312
+ });
3313
+ return;
3314
+ }
3315
+
3316
+ // Filter projects and conversations
3317
+ projectGroups.forEach(group => {
3318
+ const projectName = group.dataset.project;
3319
+ const projectNameLower = projectName.toLowerCase();
3320
+ const conversations = group.querySelectorAll('.conversation-item');
3321
+
3322
+ let hasMatchingConversation = false;
3323
+
3324
+ // Check each conversation in this project
3325
+ conversations.forEach(item => {
3326
+ const nameElement = item.querySelector('.conversation-name');
3327
+ const name = nameElement ? nameElement.textContent.toLowerCase() : '';
3328
+ const matches = name.includes(term) || projectNameLower.includes(term);
3329
+
3330
+ if (matches) {
3331
+ item.style.display = 'flex';
3332
+ hasMatchingConversation = true;
3333
+ } else {
3334
+ item.style.display = 'none';
3335
+ }
3336
+ });
3337
+
3338
+ // Show project group if it has matching conversations or if project name matches
3339
+ if (hasMatchingConversation || projectNameLower.includes(term)) {
3340
+ group.style.display = 'block';
3341
+ // Auto-expand project when searching
3342
+ if (!this.expandedProjects.has(projectName)) {
3343
+ this.expandedProjects.add(projectName);
3344
+ // Update toggle icon
3345
+ const toggleIcon = group.querySelector('.toggle-icon');
3346
+ if (toggleIcon) {
3347
+ toggleIcon.classList.add('expanded');
3348
+ }
3349
+ }
3350
+ } else {
3351
+ group.style.display = 'none';
3352
+ }
3035
3353
  });
3036
3354
  }
3037
3355
 
@@ -3647,6 +3965,11 @@
3647
3965
  // Clear advanced search filters
3648
3966
  this.resetAdvancedSearch();
3649
3967
 
3968
+ // Reset expanded projects state (close all projects)
3969
+ if (this.expandedProjects) {
3970
+ this.expandedProjects.clear();
3971
+ }
3972
+
3650
3973
  // Hide search results info
3651
3974
  searchResultsInfo.classList.remove('active');
3652
3975
 
@@ -4351,6 +4674,93 @@
4351
4674
  }
4352
4675
  }
4353
4676
 
4677
+ // Download conversation context functions
4678
+ function downloadConversation() {
4679
+ const downloadBtn = document.getElementById('downloadConversation');
4680
+ const conversationId = downloadBtn.getAttribute('data-conversation-id');
4681
+
4682
+ if (!conversationId) {
4683
+ console.error('No conversation ID found');
4684
+ return;
4685
+ }
4686
+
4687
+ console.log('📥 Opening download modal for conversation:', conversationId);
4688
+
4689
+ // Show download modal
4690
+ const downloadModal = document.getElementById('downloadModal');
4691
+ downloadModal.classList.add('show');
4692
+
4693
+ // Close modal when clicking outside
4694
+ downloadModal.addEventListener('click', (e) => {
4695
+ if (e.target === downloadModal) {
4696
+ closeDownloadModal();
4697
+ }
4698
+ });
4699
+ }
4700
+
4701
+ function closeDownloadModal() {
4702
+ const downloadModal = document.getElementById('downloadModal');
4703
+ downloadModal.classList.remove('show');
4704
+ }
4705
+
4706
+ async function proceedWithDownload() {
4707
+ // Close modal
4708
+ closeDownloadModal();
4709
+
4710
+ const downloadBtn = document.getElementById('downloadConversation');
4711
+ const conversationId = downloadBtn.getAttribute('data-conversation-id');
4712
+
4713
+ if (!conversationId) {
4714
+ console.error('No conversation ID found');
4715
+ return;
4716
+ }
4717
+
4718
+ console.log('📥 Downloading context for conversation:', conversationId);
4719
+
4720
+ try {
4721
+ // Call API to export the conversation as markdown
4722
+ const response = await fetch(`/api/conversations/${conversationId}/download`, {
4723
+ method: 'POST',
4724
+ headers: {
4725
+ 'Content-Type': 'application/json'
4726
+ }
4727
+ });
4728
+
4729
+ if (!response.ok) {
4730
+ throw new Error(`Failed to export session: ${response.statusText}`);
4731
+ }
4732
+
4733
+ const data = await response.json();
4734
+
4735
+ console.log('✅ Session exported successfully:', data);
4736
+
4737
+ // Create a blob from the markdown content
4738
+ const blob = new Blob([data.markdown], { type: 'text/markdown;charset=utf-8' });
4739
+
4740
+ // Create a temporary download link
4741
+ const url = window.URL.createObjectURL(blob);
4742
+ const a = document.createElement('a');
4743
+ a.href = url;
4744
+ a.download = data.filename;
4745
+ document.body.appendChild(a);
4746
+ a.click();
4747
+
4748
+ // Cleanup
4749
+ window.URL.revokeObjectURL(url);
4750
+ document.body.removeChild(a);
4751
+
4752
+ // Show success notification
4753
+ console.log(`📥 Downloaded: ${data.filename}`);
4754
+ if (data.wasLimited) {
4755
+ console.log(`⚠️ Exported ${data.messageCount} of ${data.totalMessageCount} messages`);
4756
+ }
4757
+
4758
+ } catch (error) {
4759
+ console.error('❌ Failed to download context:', error);
4760
+ alert(`Failed to download context: ${error.message}`);
4761
+ }
4762
+ }
4763
+
4354
4764
  // Initialize the app
4355
4765
  document.addEventListener('DOMContentLoaded', () => {
4356
4766
  new ChatsMobileApp();
@@ -10,6 +10,7 @@ const StateCalculator = require('./analytics/core/StateCalculator');
10
10
  const FileWatcher = require('./analytics/core/FileWatcher');
11
11
  const DataCache = require('./analytics/data/DataCache');
12
12
  const WebSocketServer = require('./analytics/notifications/WebSocketServer');
13
+ const SessionSharing = require('./session-sharing');
13
14
 
14
15
  class ChatsMobile {
15
16
  constructor(options = {}) {
@@ -28,7 +29,10 @@ class ChatsMobile {
28
29
  const homeDir = os.homedir();
29
30
  const claudeDir = path.join(homeDir, '.claude');
30
31
  this.conversationAnalyzer = new ConversationAnalyzer(claudeDir, this.dataCache);
31
-
32
+
33
+ // Initialize SessionSharing for export/import functionality
34
+ this.sessionSharing = new SessionSharing(this.conversationAnalyzer);
35
+
32
36
  this.data = {
33
37
  conversations: [],
34
38
  conversationStates: {},
@@ -474,6 +478,40 @@ class ChatsMobile {
474
478
  }
475
479
  });
476
480
 
481
+ // API to download a conversation session as markdown
482
+ this.app.post('/api/conversations/:id/download', async (req, res) => {
483
+ try {
484
+ const conversationId = req.params.id;
485
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
486
+
487
+ if (!conversation) {
488
+ return res.status(404).json({ error: 'Conversation not found' });
489
+ }
490
+
491
+ console.log(chalk.cyan(`📥 Exporting conversation ${conversationId} as markdown...`));
492
+
493
+ // Export the session as markdown using SessionSharing module
494
+ const exportResult = await this.sessionSharing.exportSessionAsMarkdown(conversationId, conversation);
495
+
496
+ res.json({
497
+ success: true,
498
+ conversationId: conversationId,
499
+ markdown: exportResult.markdown,
500
+ filename: exportResult.filename,
501
+ messageCount: exportResult.messageCount,
502
+ totalMessageCount: exportResult.totalMessageCount,
503
+ wasLimited: exportResult.wasLimited,
504
+ timestamp: new Date().toISOString()
505
+ });
506
+ } catch (error) {
507
+ console.error('Error exporting conversation:', error);
508
+ res.status(500).json({
509
+ error: 'Failed to export session',
510
+ message: error.message
511
+ });
512
+ }
513
+ });
514
+
477
515
  // Serve the mobile chats page as default
478
516
  this.app.get('/', (req, res) => {
479
517
  res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
package/src/index.js CHANGED
@@ -18,6 +18,8 @@ const { runHealthCheck } = require('./health-check');
18
18
  const { runPluginDashboard } = require('./plugin-dashboard');
19
19
  const { trackingService } = require('./tracking-service');
20
20
  const { createGlobalAgent, listGlobalAgents, removeGlobalAgent, updateGlobalAgent } = require('./sdk/global-agent-manager');
21
+ const SessionSharing = require('./session-sharing');
22
+ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
21
23
 
22
24
  async function showMainMenu() {
23
25
  console.log('');
@@ -222,7 +224,48 @@ async function createClaudeConfig(options = {}) {
222
224
  await startChatsMobile(options);
223
225
  return;
224
226
  }
225
-
227
+
228
+ // Handle session clone (download and import shared session)
229
+ if (options.cloneSession) {
230
+ console.log(chalk.blue('📥 Cloning shared Claude Code session...'));
231
+
232
+ try {
233
+ const os = require('os');
234
+ const homeDir = os.homedir();
235
+ const claudeDir = path.join(homeDir, '.claude');
236
+
237
+ // Initialize ConversationAnalyzer and SessionSharing
238
+ const conversationAnalyzer = new ConversationAnalyzer(claudeDir);
239
+ const sessionSharing = new SessionSharing(conversationAnalyzer);
240
+
241
+ // Clone the session (cloneSession method handles all console output)
242
+ const result = await sessionSharing.cloneSession(options.cloneSession, {
243
+ projectPath: options.directory || process.cwd()
244
+ });
245
+
246
+ // Track session clone
247
+ trackingService.trackAnalyticsDashboard({
248
+ page: 'session-clone',
249
+ source: 'command_line',
250
+ success: true
251
+ });
252
+ } catch (error) {
253
+ console.error(chalk.red('❌ Failed to clone session:'), error.message);
254
+
255
+ // Track failed clone
256
+ trackingService.trackAnalyticsDashboard({
257
+ page: 'session-clone',
258
+ source: 'command_line',
259
+ success: false,
260
+ error: error.message
261
+ });
262
+
263
+ process.exit(1);
264
+ }
265
+
266
+ return;
267
+ }
268
+
226
269
  // Handle health check
227
270
  let shouldRunSetup = false;
228
271
  if (options.healthCheck || options.health || options.check || options.verify) {
@@ -0,0 +1,457 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { exec } = require('child_process');
6
+ const { promisify } = require('util');
7
+ const execAsync = promisify(exec);
8
+ const QRCode = require('qrcode');
9
+
10
+ /**
11
+ * SessionSharing - Handles exporting Claude Code sessions as downloadable context
12
+ */
13
+ class SessionSharing {
14
+ constructor(conversationAnalyzer) {
15
+ this.conversationAnalyzer = conversationAnalyzer;
16
+ }
17
+
18
+ /**
19
+ * Export conversation session as downloadable markdown file
20
+ * @param {string} conversationId - Conversation ID to export
21
+ * @param {Object} conversationData - Full conversation data object
22
+ * @param {Object} options - Export options (messageLimit, etc.)
23
+ * @returns {Promise<Object>} Export result with markdown content and filename
24
+ */
25
+ async exportSessionAsMarkdown(conversationId, conversationData, options = {}) {
26
+ console.log(chalk.blue(`📥 Preparing session ${conversationId} for download...`));
27
+
28
+ try {
29
+ // 1. Get conversation messages
30
+ const allMessages = await this.conversationAnalyzer.getParsedConversation(conversationData.filePath);
31
+
32
+ // Limit messages to avoid large file sizes (default: last 100 messages)
33
+ const messageLimit = options.messageLimit || 100;
34
+ const messages = allMessages.slice(-messageLimit);
35
+
36
+ // 2. Convert to markdown format
37
+ const markdown = this.convertToMarkdown(messages, conversationData, {
38
+ messageCount: messages.length,
39
+ totalMessageCount: allMessages.length,
40
+ wasLimited: allMessages.length > messageLimit
41
+ });
42
+
43
+ // 3. Generate filename
44
+ const projectName = (conversationData.project || 'session').replace(/[^a-zA-Z0-9-_]/g, '-');
45
+ const date = new Date().toISOString().split('T')[0];
46
+ const filename = `claude-context-${projectName}-${date}.md`;
47
+
48
+ console.log(chalk.green(`✅ Session exported successfully!`));
49
+ console.log(chalk.gray(`📊 Exported ${messages.length} messages`));
50
+
51
+ return {
52
+ success: true,
53
+ markdown,
54
+ filename,
55
+ messageCount: messages.length,
56
+ totalMessageCount: allMessages.length,
57
+ wasLimited: allMessages.length > messageLimit
58
+ };
59
+ } catch (error) {
60
+ console.error(chalk.red('❌ Failed to export session:'), error.message);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Convert conversation messages to markdown format optimized for Claude Code
67
+ * @param {Array} messages - Parsed conversation messages
68
+ * @param {Object} conversationData - Conversation metadata
69
+ * @param {Object} stats - Export statistics
70
+ * @returns {string} Markdown formatted content
71
+ */
72
+ convertToMarkdown(messages, conversationData, stats) {
73
+ const lines = [];
74
+
75
+ // Header for Claude Code
76
+ lines.push('# Previous Conversation Context\n');
77
+ lines.push('> **Note to Claude Code**: This file contains the complete conversation history from a previous session. Read and understand this context to continue helping the user with their task.\n');
78
+ lines.push(`**Project:** ${conversationData.project || 'Unknown'}`);
79
+ lines.push(`**Date:** ${new Date().toISOString().split('T')[0]}`);
80
+ lines.push(`**Messages in this export:** ${stats.messageCount}${stats.wasLimited ? ` (most recent from a total of ${stats.totalMessageCount})` : ''}`);
81
+ lines.push('');
82
+ lines.push('---');
83
+ lines.push('');
84
+
85
+ // Conversation
86
+ lines.push('## 💬 Conversation History\n');
87
+
88
+ messages.forEach((msg, index) => {
89
+ const role = msg.role === 'user' ? '👤 User' : '🤖 Assistant';
90
+ const timestamp = new Date(msg.timestamp).toLocaleString();
91
+
92
+ lines.push(`### Message ${index + 1}: ${role}`);
93
+ lines.push(`*${timestamp}*\n`);
94
+
95
+ // Extract text content from message
96
+ if (Array.isArray(msg.content)) {
97
+ msg.content.forEach(block => {
98
+ if (block.type === 'text') {
99
+ lines.push(block.text);
100
+ } else if (block.type === 'tool_use') {
101
+ lines.push(`\`\`\`${block.name || 'tool'}`);
102
+ lines.push(JSON.stringify(block.input || {}, null, 2));
103
+ lines.push('```');
104
+ } else if (block.type === 'tool_result') {
105
+ lines.push('**Tool Result:**');
106
+ lines.push('```');
107
+ lines.push(typeof block.content === 'string' ? block.content : JSON.stringify(block.content, null, 2));
108
+ lines.push('```');
109
+ }
110
+ });
111
+ } else if (typeof msg.content === 'string') {
112
+ lines.push(msg.content);
113
+ }
114
+
115
+ lines.push('');
116
+ lines.push('---');
117
+ lines.push('');
118
+ });
119
+
120
+ // Footer
121
+ lines.push('\n---');
122
+ lines.push('');
123
+ lines.push('*Generated by Claude Code Templates - [aitmpl.com](https://aitmpl.com)*');
124
+
125
+ return lines.join('\n');
126
+ }
127
+
128
+ /**
129
+ * Export session data to standardized format
130
+ * @param {string} conversationId - Conversation ID
131
+ * @param {Object} conversationData - Conversation metadata
132
+ * @param {Object} options - Export options
133
+ * @returns {Promise<Object>} Exported session object
134
+ */
135
+ async exportSessionData(conversationId, conversationData, options = {}) {
136
+ // Get all messages from the conversation
137
+ const allMessages = await this.conversationAnalyzer.getParsedConversation(conversationData.filePath);
138
+
139
+ // Limit messages to avoid large file sizes (default: last 100 messages)
140
+ const messageLimit = options.messageLimit || 100;
141
+ const messages = allMessages.slice(-messageLimit);
142
+
143
+ // Convert parsed messages back to JSONL format (original Claude Code format)
144
+ const jsonlMessages = messages.map(msg => {
145
+ // Reconstruct original JSONL entry format
146
+ const entry = {
147
+ uuid: msg.uuid || msg.id,
148
+ type: msg.role === 'assistant' ? 'assistant' : 'user',
149
+ timestamp: msg.timestamp.toISOString(),
150
+ message: {
151
+ id: msg.id,
152
+ role: msg.role,
153
+ content: msg.content
154
+ }
155
+ };
156
+
157
+ // Add model info for assistant messages
158
+ if (msg.model) {
159
+ entry.message.model = msg.model;
160
+ }
161
+
162
+ // Add usage info
163
+ if (msg.usage) {
164
+ entry.message.usage = msg.usage;
165
+ }
166
+
167
+ // Add compact summary flag if present
168
+ if (msg.isCompactSummary) {
169
+ entry.isCompactSummary = true;
170
+ }
171
+
172
+ return entry;
173
+ });
174
+
175
+ // Create export package
176
+ const exportData = {
177
+ version: '1.0.0',
178
+ exported_at: new Date().toISOString(),
179
+ conversation: {
180
+ id: conversationId,
181
+ project: conversationData.project || 'shared-session',
182
+ created: conversationData.created,
183
+ lastModified: conversationData.lastModified,
184
+ messageCount: messages.length,
185
+ totalMessageCount: allMessages.length,
186
+ wasLimited: allMessages.length > messageLimit,
187
+ tokens: conversationData.tokens,
188
+ model: conversationData.modelInfo?.primaryModel || 'claude-sonnet-4-5-20250929'
189
+ },
190
+ messages: jsonlMessages,
191
+ metadata: {
192
+ exportTool: 'claude-code-templates',
193
+ exportVersion: require('../package.json').version || '1.0.0',
194
+ messageLimit: messageLimit,
195
+ description: 'Claude Code session export - can be cloned with: npx claude-code-templates@latest --clone-session <url>'
196
+ }
197
+ };
198
+
199
+ // Log information about exported messages
200
+ if (allMessages.length > messageLimit) {
201
+ console.log(chalk.yellow(`⚠️ Session has ${allMessages.length} messages, exporting last ${messageLimit} messages`));
202
+ } else {
203
+ console.log(chalk.gray(`📊 Exporting ${messages.length} messages`));
204
+ }
205
+
206
+ return exportData;
207
+ }
208
+
209
+ /**
210
+ * Upload session to x0.at
211
+ * @param {Object} sessionData - Session export data
212
+ * @param {string} conversationId - Conversation ID for filename
213
+ * @returns {Promise<string>} Upload URL
214
+ */
215
+ async uploadToX0(sessionData, conversationId) {
216
+ const tmpDir = path.join(os.tmpdir(), 'claude-code-sessions');
217
+ await fs.ensureDir(tmpDir);
218
+
219
+ const tmpFile = path.join(tmpDir, `session-${conversationId}.json`);
220
+
221
+ try {
222
+ // Write session data to temp file
223
+ await fs.writeFile(tmpFile, JSON.stringify(sessionData, null, 2), 'utf8');
224
+
225
+ console.log(chalk.gray(`📁 Created temp file: ${tmpFile}`));
226
+ console.log(chalk.gray(`📤 Uploading to x0.at...`));
227
+
228
+ // Upload to x0.at using curl with form data
229
+ // x0.at API: curl -F'file=@yourfile.png' https://x0.at
230
+ // Response: Direct URL in plain text
231
+ const { stdout, stderr } = await execAsync(
232
+ `curl -s -F "file=@${tmpFile}" ${this.uploadUrl}`,
233
+ { maxBuffer: 10 * 1024 * 1024 } // 10MB buffer
234
+ );
235
+
236
+ // x0.at returns URL directly in plain text
237
+ const uploadUrl = stdout.trim();
238
+
239
+ // Validate response
240
+ if (!uploadUrl || !uploadUrl.startsWith('http')) {
241
+ throw new Error(`Invalid response from x0.at: ${uploadUrl || stderr}`);
242
+ }
243
+
244
+ console.log(chalk.green(`✅ Uploaded to x0.at successfully`));
245
+ console.log(chalk.yellow(`⚠️ Files kept for 3-100 days (based on size)`));
246
+ console.log(chalk.gray(`🔓 Note: Files are not encrypted by default`));
247
+
248
+ // Clean up temp file
249
+ await fs.remove(tmpFile);
250
+
251
+ return uploadUrl;
252
+ } catch (error) {
253
+ // Clean up temp file on error
254
+ await fs.remove(tmpFile).catch(() => {});
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Clone a session from a shared URL
261
+ * Downloads the session and places it in the correct Claude Code location
262
+ * @param {string} url - URL to download session from
263
+ * @param {Object} options - Clone options
264
+ * @returns {Promise<Object>} Result with session path
265
+ */
266
+ async cloneSession(url, options = {}) {
267
+ console.log(chalk.blue(`📥 Downloading session from ${url}...`));
268
+
269
+ try {
270
+ // 1. Download session data
271
+ const sessionData = await this.downloadSession(url);
272
+
273
+ // 2. Validate session data
274
+ this.validateSessionData(sessionData);
275
+
276
+ console.log(chalk.green(`✅ Session downloaded successfully`));
277
+ console.log(chalk.gray(`📊 Project: ${sessionData.conversation.project}`));
278
+ console.log(chalk.gray(`💬 Messages: ${sessionData.conversation.messageCount}`));
279
+ console.log(chalk.gray(`🤖 Model: ${sessionData.conversation.model}`));
280
+
281
+ // 3. Install session in Claude Code directory
282
+ const installResult = await this.installSession(sessionData, options);
283
+
284
+ console.log(chalk.green(`\n✅ Session installed successfully!`));
285
+ console.log(chalk.cyan(`📂 Location: ${installResult.sessionPath}`));
286
+
287
+ // Show resume command (only conversation ID needed)
288
+ const resumeCommand = `claude --resume ${installResult.conversationId}`;
289
+ console.log(chalk.yellow(`\n💡 To continue this conversation, run:`));
290
+ console.log(chalk.white(`\n ${resumeCommand}\n`));
291
+ console.log(chalk.gray(` Or open Claude Code to see it in your sessions list`));
292
+
293
+ return installResult;
294
+ } catch (error) {
295
+ console.error(chalk.red('❌ Failed to clone session:'), error.message);
296
+ throw error;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Download session data from URL
302
+ * @param {string} url - URL to download from
303
+ * @returns {Promise<Object>} Session data
304
+ */
305
+ async downloadSession(url) {
306
+ try {
307
+ // Use curl to download (works with x0.at and other services)
308
+ const { stdout, stderr } = await execAsync(`curl -L "${url}"`, {
309
+ maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large sessions
310
+ });
311
+
312
+ if (stderr && !stdout) {
313
+ throw new Error(`Download failed: ${stderr}`);
314
+ }
315
+
316
+ // Parse JSON response
317
+ const sessionData = JSON.parse(stdout);
318
+ return sessionData;
319
+ } catch (error) {
320
+ if (error.message.includes('Unexpected token')) {
321
+ throw new Error('Invalid session file - corrupted or not a Claude Code session');
322
+ }
323
+ throw error;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Validate session data structure
329
+ * @param {Object} sessionData - Session data to validate
330
+ * @throws {Error} If validation fails
331
+ */
332
+ validateSessionData(sessionData) {
333
+ if (!sessionData.version) {
334
+ throw new Error('Invalid session file - missing version');
335
+ }
336
+
337
+ if (!sessionData.conversation || !sessionData.conversation.id) {
338
+ throw new Error('Invalid session file - missing conversation data');
339
+ }
340
+
341
+ if (!sessionData.messages || !Array.isArray(sessionData.messages)) {
342
+ throw new Error('Invalid session file - missing or invalid messages');
343
+ }
344
+
345
+ if (sessionData.messages.length === 0) {
346
+ throw new Error('Invalid session file - no messages found');
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Install session in Claude Code directory structure
352
+ * @param {Object} sessionData - Session data to install
353
+ * @param {Object} options - Installation options
354
+ * @returns {Promise<Object>} Installation result
355
+ */
356
+ async installSession(sessionData, options = {}) {
357
+ const homeDir = os.homedir();
358
+ const claudeDir = path.join(homeDir, '.claude');
359
+
360
+ // Determine project directory
361
+ const projectName = sessionData.conversation.project || 'shared-session';
362
+ const projectDirName = this.sanitizeProjectName(projectName);
363
+
364
+ // Create project directory structure
365
+ // Format: ~/.claude/projects/-path-to-project/
366
+ const projectDir = path.join(claudeDir, 'projects', projectDirName);
367
+ await fs.ensureDir(projectDir);
368
+
369
+ // Generate conversation filename with original ID
370
+ const conversationId = sessionData.conversation.id;
371
+ const conversationFile = path.join(projectDir, `${conversationId}.jsonl`);
372
+
373
+ // Convert messages back to JSONL format (one JSON object per line)
374
+ const jsonlContent = sessionData.messages
375
+ .map(msg => JSON.stringify(msg))
376
+ .join('\n');
377
+
378
+ // Write conversation file
379
+ await fs.writeFile(conversationFile, jsonlContent, 'utf8');
380
+
381
+ console.log(chalk.gray(`📝 Created conversation file: ${conversationFile}`));
382
+
383
+ // Create or update settings.json
384
+ const settingsFile = path.join(projectDir, 'settings.json');
385
+ const settings = {
386
+ projectName: sessionData.conversation.project,
387
+ projectPath: options.projectPath || process.cwd(),
388
+ sharedSession: true,
389
+ originalExport: {
390
+ exportedAt: sessionData.exported_at,
391
+ exportTool: sessionData.metadata?.exportTool,
392
+ exportVersion: sessionData.metadata?.exportVersion
393
+ },
394
+ importedAt: new Date().toISOString()
395
+ };
396
+
397
+ await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
398
+
399
+ console.log(chalk.gray(`⚙️ Created settings file: ${settingsFile}`));
400
+
401
+ return {
402
+ success: true,
403
+ sessionPath: conversationFile,
404
+ projectDir,
405
+ projectPath: settings.projectPath,
406
+ conversationId,
407
+ messageCount: sessionData.messages.length
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Generate QR code for share command
413
+ * @param {string} command - Command to encode in QR
414
+ * @returns {Promise<Object>} QR code data (Data URL for web display)
415
+ */
416
+ async generateQRCode(command) {
417
+ try {
418
+ // Generate QR code as Data URL (for web display)
419
+ const qrDataUrl = await QRCode.toDataURL(command, {
420
+ errorCorrectionLevel: 'M',
421
+ type: 'image/png',
422
+ width: 300,
423
+ margin: 2,
424
+ color: {
425
+ dark: '#000000',
426
+ light: '#FFFFFF'
427
+ }
428
+ });
429
+
430
+ return {
431
+ dataUrl: qrDataUrl,
432
+ command: command
433
+ };
434
+ } catch (error) {
435
+ console.warn(chalk.yellow('⚠️ Could not generate QR code:'), error.message);
436
+ return {
437
+ dataUrl: null,
438
+ command: command
439
+ };
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Sanitize project name for directory usage
445
+ * @param {string} projectName - Original project name
446
+ * @returns {string} Sanitized name
447
+ */
448
+ sanitizeProjectName(projectName) {
449
+ // Replace spaces and special chars with hyphens
450
+ return projectName
451
+ .replace(/[^a-zA-Z0-9-_]/g, '-')
452
+ .replace(/-+/g, '-')
453
+ .toLowerCase();
454
+ }
455
+ }
456
+
457
+ module.exports = SessionSharing;