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.
- package/bin/create-claude-config.js +1 -0
- package/package.json +2 -1
- package/src/analytics-web/chats_mobile.html +477 -67
- package/src/chats-mobile.js +39 -1
- package/src/index.js +44 -1
- package/src/session-sharing.js +457 -0
|
@@ -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.
|
|
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
|
-
/*
|
|
708
|
-
.
|
|
709
|
-
|
|
710
|
-
border:
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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:
|
|
729
|
+
gap: 8px;
|
|
730
|
+
white-space: nowrap;
|
|
720
731
|
}
|
|
721
732
|
|
|
722
|
-
.
|
|
723
|
-
|
|
724
|
-
color: white;
|
|
725
|
-
border-color: var(--terminal-orange);
|
|
733
|
+
.action-btn:last-child {
|
|
734
|
+
border-right: none;
|
|
726
735
|
}
|
|
727
736
|
|
|
728
|
-
.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
|
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
|
-
<
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
|
2248
|
-
<p>Start a
|
|
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
|
-
|
|
2255
|
-
|
|
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
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
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
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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="
|
|
2281
|
-
|
|
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="
|
|
2284
|
-
<
|
|
2285
|
-
|
|
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 = `
|
|
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
|
|
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
|
-
|
|
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
|
|
2356
|
-
const
|
|
2357
|
-
|
|
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
|
|
3028
|
-
const
|
|
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
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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();
|
package/src/chats-mobile.js
CHANGED
|
@@ -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;
|