claude-code-workflow 6.2.4 → 6.2.6

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.
Files changed (62) hide show
  1. package/ccw/dist/core/lite-scanner-complete.d.ts.map +1 -1
  2. package/ccw/dist/core/lite-scanner-complete.js +4 -1
  3. package/ccw/dist/core/lite-scanner-complete.js.map +1 -1
  4. package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
  5. package/ccw/dist/core/lite-scanner.js +4 -1
  6. package/ccw/dist/core/lite-scanner.js.map +1 -1
  7. package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
  8. package/ccw/dist/core/routes/claude-routes.js +3 -5
  9. package/ccw/dist/core/routes/claude-routes.js.map +1 -1
  10. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  11. package/ccw/dist/core/routes/cli-routes.js +2 -1
  12. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  13. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  14. package/ccw/dist/core/routes/codexlens-routes.js +31 -6
  15. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  16. package/ccw/dist/core/routes/rules-routes.d.ts.map +1 -1
  17. package/ccw/dist/core/routes/rules-routes.js +4 -3
  18. package/ccw/dist/core/routes/rules-routes.js.map +1 -1
  19. package/ccw/dist/core/routes/skills-routes.d.ts.map +1 -1
  20. package/ccw/dist/core/routes/skills-routes.js +124 -6
  21. package/ccw/dist/core/routes/skills-routes.js.map +1 -1
  22. package/ccw/dist/tools/cli-executor.d.ts +4 -1
  23. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  24. package/ccw/dist/tools/cli-executor.js +54 -2
  25. package/ccw/dist/tools/cli-executor.js.map +1 -1
  26. package/ccw/dist/tools/codex-lens.d.ts +20 -3
  27. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  28. package/ccw/dist/tools/codex-lens.js +166 -37
  29. package/ccw/dist/tools/codex-lens.js.map +1 -1
  30. package/ccw/package.json +1 -1
  31. package/ccw/src/core/lite-scanner-complete.ts +5 -1
  32. package/ccw/src/core/lite-scanner.ts +5 -1
  33. package/ccw/src/core/routes/claude-routes.ts +3 -5
  34. package/ccw/src/core/routes/cli-routes.ts +2 -1
  35. package/ccw/src/core/routes/codexlens-routes.ts +34 -6
  36. package/ccw/src/core/routes/rules-routes.ts +4 -3
  37. package/ccw/src/core/routes/skills-routes.ts +144 -6
  38. package/ccw/src/templates/dashboard-js/components/mcp-manager.js +7 -12
  39. package/ccw/src/templates/dashboard-js/i18n.js +167 -5
  40. package/ccw/src/templates/dashboard-js/views/claude-manager.js +18 -4
  41. package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -3
  42. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +790 -25
  43. package/ccw/src/templates/dashboard-js/views/rules-manager.js +35 -6
  44. package/ccw/src/templates/dashboard-js/views/skills-manager.js +385 -21
  45. package/ccw/src/tools/cli-executor.ts +70 -2
  46. package/ccw/src/tools/codex-lens.ts +183 -35
  47. package/codex-lens/pyproject.toml +66 -48
  48. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  49. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  50. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  51. package/codex-lens/src/codexlens/cli/embedding_manager.py +3 -3
  52. package/codex-lens/src/codexlens/cli/model_manager.py +24 -2
  53. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  54. package/codex-lens/src/codexlens/search/hybrid_search.py +313 -313
  55. package/codex-lens/src/codexlens/semantic/__init__.py +76 -39
  56. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  57. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  58. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  59. package/codex-lens/src/codexlens/semantic/__pycache__/ollama_backend.cpython-313.pyc +0 -0
  60. package/codex-lens/src/codexlens/semantic/embedder.py +244 -185
  61. package/codex-lens/src/codexlens/semantic/gpu_support.py +192 -0
  62. package/package.json +1 -1
@@ -638,9 +638,26 @@ function addRulePath() {
638
638
 
639
639
  function removeRulePath(index) {
640
640
  ruleCreateState.paths.splice(index, 1);
641
- // Re-render paths list
642
- closeRuleCreateModal();
643
- openRuleCreateModal();
641
+
642
+ // Re-render paths list without closing modal
643
+ const pathsList = document.getElementById('rulePathsList');
644
+ if (pathsList) {
645
+ pathsList.innerHTML = ruleCreateState.paths.map((path, idx) => `
646
+ <div class="flex gap-2">
647
+ <input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
648
+ placeholder="src/**/*.ts"
649
+ value="${path}"
650
+ data-index="${idx}">
651
+ ${idx > 0 ? `
652
+ <button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
653
+ onclick="removeRulePath(${idx})">
654
+ <i data-lucide="x" class="w-4 h-4"></i>
655
+ </button>
656
+ ` : ''}
657
+ </div>
658
+ `).join('');
659
+ if (typeof lucide !== 'undefined') lucide.createIcons();
660
+ }
644
661
  }
645
662
 
646
663
  function switchRuleCreateMode(mode) {
@@ -674,9 +691,21 @@ function switchRuleCreateMode(mode) {
674
691
  if (contentSection) contentSection.style.display = 'block';
675
692
  }
676
693
 
677
- // Re-render modal to update button states
678
- closeRuleCreateModal();
679
- openRuleCreateModal();
694
+ // Update mode button styles without re-rendering
695
+ const modeButtons = document.querySelectorAll('#ruleCreateModal .mode-btn');
696
+ modeButtons.forEach(btn => {
697
+ const btnText = btn.querySelector('.font-medium')?.textContent || '';
698
+ const isInput = btnText.includes(t('rules.manualInput'));
699
+ const isCliGenerate = btnText.includes(t('rules.cliGenerate'));
700
+
701
+ if ((isInput && mode === 'input') || (isCliGenerate && mode === 'cli-generate')) {
702
+ btn.classList.remove('border-border', 'hover:border-primary/50');
703
+ btn.classList.add('border-primary', 'bg-primary/10');
704
+ } else {
705
+ btn.classList.remove('border-primary', 'bg-primary/10');
706
+ btn.classList.add('border-border', 'hover:border-primary/50');
707
+ }
708
+ });
680
709
  }
681
710
 
682
711
  function switchRuleGenerationType(type) {
@@ -153,10 +153,11 @@ function renderSkillCard(skill, location) {
153
153
  const locationIcon = location === 'project' ? 'folder' : 'user';
154
154
  const locationClass = location === 'project' ? 'text-primary' : 'text-indigo';
155
155
  const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10';
156
+ const folderName = skill.folderName || skill.name;
156
157
 
157
158
  return `
158
159
  <div class="skill-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
159
- onclick="showSkillDetail('${escapeHtml(skill.name)}', '${location}')">
160
+ onclick="showSkillDetail('${escapeHtml(folderName)}', '${location}')">
160
161
  <div class="flex items-start justify-between mb-3">
161
162
  <div class="flex items-center gap-3">
162
163
  <div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center">
@@ -198,6 +199,7 @@ function renderSkillCard(skill, location) {
198
199
  function renderSkillDetailPanel(skill) {
199
200
  const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
200
201
  const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
202
+ const folderName = skill.folderName || skill.name;
201
203
 
202
204
  return `
203
205
  <div class="skill-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col">
@@ -243,20 +245,54 @@ function renderSkillDetailPanel(skill) {
243
245
  </div>
244
246
  ` : ''}
245
247
 
246
- <!-- Supporting Files -->
247
- ${hasSupportingFiles ? `
248
- <div>
249
- <h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.supportingFiles')}</h4>
250
- <div class="space-y-2">
251
- ${skill.supportingFiles.map(file => `
252
- <div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg">
253
- <i data-lucide="file-text" class="w-4 h-4 text-muted-foreground"></i>
254
- <span class="text-sm font-mono text-foreground">${escapeHtml(file)}</span>
248
+ <!-- Skill Files (SKILL.md + Supporting Files) -->
249
+ <div>
250
+ <h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.files') || 'Files'}</h4>
251
+ <div class="space-y-2">
252
+ <!-- SKILL.md (main file) -->
253
+ <div class="flex items-center justify-between p-2 bg-primary/5 border border-primary/20 rounded-lg cursor-pointer hover:bg-primary/10 transition-colors"
254
+ onclick="viewSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')">
255
+ <div class="flex items-center gap-2">
256
+ <i data-lucide="file-text" class="w-4 h-4 text-primary"></i>
257
+ <span class="text-sm font-mono text-foreground font-medium">SKILL.md</span>
258
+ </div>
259
+ <div class="flex items-center gap-1">
260
+ <button class="p-1 text-primary hover:bg-primary/20 rounded transition-colors"
261
+ onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')"
262
+ title="${t('common.edit')}">
263
+ <i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
264
+ </button>
265
+ </div>
266
+ </div>
267
+ ${hasSupportingFiles ? skill.supportingFiles.map(file => {
268
+ const isDir = file.endsWith('/');
269
+ const dirName = isDir ? file.slice(0, -1) : file;
270
+ return `
271
+ <!-- Supporting file: ${escapeHtml(file)} -->
272
+ <div class="skill-file-item" data-path="${escapeHtml(dirName)}">
273
+ <div class="flex items-center justify-between p-2 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
274
+ onclick="${isDir ? `toggleSkillFolder('${escapeHtml(folderName)}', '${escapeHtml(dirName)}', '${skill.location}', this)` : `viewSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')`}">
275
+ <div class="flex items-center gap-2">
276
+ <i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
277
+ <span class="text-sm font-mono text-foreground">${escapeHtml(isDir ? dirName : file)}</span>
278
+ ${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
255
279
  </div>
256
- `).join('')}
280
+ ${!isDir ? `
281
+ <div class="flex items-center gap-1">
282
+ <button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
283
+ onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')"
284
+ title="${t('common.edit')}">
285
+ <i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
286
+ </button>
287
+ </div>
288
+ ` : ''}
289
+ </div>
290
+ <div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
257
291
  </div>
292
+ `;
293
+ }).join('') : ''}
258
294
  </div>
259
- ` : ''}
295
+ </div>
260
296
 
261
297
  <!-- Path -->
262
298
  <div>
@@ -269,12 +305,12 @@ function renderSkillDetailPanel(skill) {
269
305
  <!-- Actions -->
270
306
  <div class="px-5 py-4 border-t border-border flex justify-between">
271
307
  <button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2"
272
- onclick="deleteSkill('${escapeHtml(skill.name)}', '${skill.location}')">
308
+ onclick="deleteSkill('${escapeHtml(folderName)}', '${skill.location}')">
273
309
  <i data-lucide="trash-2" class="w-4 h-4"></i>
274
310
  ${t('common.delete')}
275
311
  </button>
276
312
  <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
277
- onclick="editSkill('${escapeHtml(skill.name)}', '${skill.location}')">
313
+ onclick="editSkill('${escapeHtml(folderName)}', '${skill.location}')">
278
314
  <i data-lucide="edit" class="w-4 h-4"></i>
279
315
  ${t('common.edit')}
280
316
  </button>
@@ -525,7 +561,7 @@ function openSkillCreateModal() {
525
561
  </div>
526
562
 
527
563
  <!-- Footer -->
528
- <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
564
+ <div id="skillModalFooter" class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
529
565
  <button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
530
566
  onclick="closeSkillCreateModal()">
531
567
  ${t('common.cancel')}
@@ -588,16 +624,76 @@ function selectSkillLocation(location) {
588
624
 
589
625
  function switchSkillCreateMode(mode) {
590
626
  skillCreateState.mode = mode;
591
- // Re-render modal
592
- closeSkillCreateModal();
593
- openSkillCreateModal();
627
+
628
+ // Toggle visibility of mode sections
629
+ const importSection = document.getElementById('skillImportMode');
630
+ const cliGenerateSection = document.getElementById('skillCliGenerateMode');
631
+ const footerContainer = document.getElementById('skillModalFooter');
632
+
633
+ if (importSection) importSection.style.display = mode === 'import' ? 'block' : 'none';
634
+ if (cliGenerateSection) cliGenerateSection.style.display = mode === 'cli-generate' ? 'block' : 'none';
635
+
636
+ // Update mode button styles
637
+ const modeButtons = document.querySelectorAll('#skillCreateModal .mode-btn');
638
+ modeButtons.forEach(btn => {
639
+ const btnText = btn.querySelector('.font-medium')?.textContent || '';
640
+ const isImport = btnText.includes(t('skills.importFolder'));
641
+ const isCliGenerate = btnText.includes(t('skills.cliGenerate'));
642
+
643
+ if ((isImport && mode === 'import') || (isCliGenerate && mode === 'cli-generate')) {
644
+ btn.classList.remove('border-border', 'hover:border-primary/50');
645
+ btn.classList.add('border-primary', 'bg-primary/10');
646
+ } else {
647
+ btn.classList.remove('border-primary', 'bg-primary/10');
648
+ btn.classList.add('border-border', 'hover:border-primary/50');
649
+ }
650
+ });
651
+
652
+ // Update footer buttons
653
+ if (footerContainer) {
654
+ if (mode === 'import') {
655
+ footerContainer.innerHTML = `
656
+ <button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
657
+ onclick="closeSkillCreateModal()">
658
+ ${t('common.cancel')}
659
+ </button>
660
+ <button class="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
661
+ onclick="validateSkillImport()">
662
+ ${t('skills.validate')}
663
+ </button>
664
+ <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
665
+ onclick="createSkill()">
666
+ ${t('skills.import')}
667
+ </button>
668
+ `;
669
+ } else {
670
+ footerContainer.innerHTML = `
671
+ <button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
672
+ onclick="closeSkillCreateModal()">
673
+ ${t('common.cancel')}
674
+ </button>
675
+ <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
676
+ onclick="createSkill()">
677
+ <i data-lucide="sparkles" class="w-4 h-4"></i>
678
+ ${t('skills.generate')}
679
+ </button>
680
+ `;
681
+ }
682
+ if (typeof lucide !== 'undefined') lucide.createIcons();
683
+ }
594
684
  }
595
685
 
596
686
  function switchSkillGenerationType(type) {
597
687
  skillCreateState.generationType = type;
598
- // Re-render modal
599
- closeSkillCreateModal();
600
- openSkillCreateModal();
688
+
689
+ // Toggle visibility of description area
690
+ const descriptionArea = document.getElementById('skillDescriptionArea');
691
+ if (descriptionArea) {
692
+ descriptionArea.style.display = type === 'description' ? 'block' : 'none';
693
+ }
694
+
695
+ // Update generation type button styles (only the description button is active, template is disabled)
696
+ // No need to update button styles since template button is disabled
601
697
  }
602
698
 
603
699
  function browseSkillFolder() {
@@ -817,3 +913,271 @@ async function createSkill() {
817
913
  }
818
914
  }
819
915
  }
916
+
917
+
918
+ // ========== Skill File View/Edit Functions ==========
919
+
920
+ var skillFileEditorState = {
921
+ skillName: '',
922
+ fileName: '',
923
+ location: '',
924
+ content: '',
925
+ isEditing: false
926
+ };
927
+
928
+ async function viewSkillFile(skillName, fileName, location) {
929
+ try {
930
+ const response = await fetch(
931
+ '/api/skills/' + encodeURIComponent(skillName) + '/file?filename=' + encodeURIComponent(fileName) +
932
+ '&location=' + location + '&path=' + encodeURIComponent(projectPath)
933
+ );
934
+
935
+ if (!response.ok) {
936
+ const error = await response.json();
937
+ throw new Error(error.error || 'Failed to load file');
938
+ }
939
+
940
+ const data = await response.json();
941
+
942
+ skillFileEditorState = {
943
+ skillName,
944
+ fileName,
945
+ location,
946
+ content: data.content,
947
+ isEditing: false
948
+ };
949
+
950
+ renderSkillFileModal();
951
+ } catch (err) {
952
+ console.error('Failed to load skill file:', err);
953
+ if (window.showToast) {
954
+ showToast(err.message || t('skills.fileLoadError') || 'Failed to load file', 'error');
955
+ }
956
+ }
957
+ }
958
+
959
+ function editSkillFile(skillName, fileName, location) {
960
+ viewSkillFile(skillName, fileName, location).then(() => {
961
+ skillFileEditorState.isEditing = true;
962
+ renderSkillFileModal();
963
+ });
964
+ }
965
+
966
+ function renderSkillFileModal() {
967
+ // Remove existing modal if any
968
+ const existingModal = document.getElementById('skillFileModal');
969
+ if (existingModal) existingModal.remove();
970
+
971
+ const { skillName, fileName, content, isEditing, location } = skillFileEditorState;
972
+
973
+ const modalHtml = `
974
+ <div class="modal-overlay fixed inset-0 bg-black/50 z-[60] flex items-center justify-center" onclick="closeSkillFileModal(event)">
975
+ <div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-4xl max-h-[90vh] mx-4 flex flex-col" onclick="event.stopPropagation()">
976
+ <!-- Header -->
977
+ <div class="flex items-center justify-between px-6 py-4 border-b border-border">
978
+ <div class="flex items-center gap-3">
979
+ <i data-lucide="file-text" class="w-5 h-5 text-primary"></i>
980
+ <div>
981
+ <h3 class="text-lg font-semibold text-foreground font-mono">${escapeHtml(fileName)}</h3>
982
+ <p class="text-xs text-muted-foreground">${escapeHtml(skillName)} / ${location}</p>
983
+ </div>
984
+ </div>
985
+ <div class="flex items-center gap-2">
986
+ ${!isEditing ? `
987
+ <button class="px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
988
+ onclick="toggleSkillFileEdit()">
989
+ <i data-lucide="edit-2" class="w-4 h-4"></i>
990
+ ${t('common.edit')}
991
+ </button>
992
+ ` : ''}
993
+ <button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
994
+ onclick="closeSkillFileModal()">&times;</button>
995
+ </div>
996
+ </div>
997
+
998
+ <!-- Content -->
999
+ <div class="flex-1 overflow-hidden p-4">
1000
+ ${isEditing ? `
1001
+ <textarea id="skillFileContent"
1002
+ class="w-full h-full min-h-[400px] px-4 py-3 bg-background border border-border rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary resize-none"
1003
+ spellcheck="false">${escapeHtml(content)}</textarea>
1004
+ ` : `
1005
+ <div class="w-full h-full min-h-[400px] overflow-auto">
1006
+ <pre class="px-4 py-3 bg-muted/30 rounded-lg text-sm font-mono whitespace-pre-wrap break-words">${escapeHtml(content)}</pre>
1007
+ </div>
1008
+ `}
1009
+ </div>
1010
+
1011
+ <!-- Footer -->
1012
+ ${isEditing ? `
1013
+ <div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
1014
+ <button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
1015
+ onclick="cancelSkillFileEdit()">
1016
+ ${t('common.cancel')}
1017
+ </button>
1018
+ <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
1019
+ onclick="saveSkillFile()">
1020
+ <i data-lucide="save" class="w-4 h-4"></i>
1021
+ ${t('common.save')}
1022
+ </button>
1023
+ </div>
1024
+ ` : ''}
1025
+ </div>
1026
+ </div>
1027
+ `;
1028
+
1029
+ const modalContainer = document.createElement('div');
1030
+ modalContainer.id = 'skillFileModal';
1031
+ modalContainer.innerHTML = modalHtml;
1032
+ document.body.appendChild(modalContainer);
1033
+
1034
+ if (typeof lucide !== 'undefined') lucide.createIcons();
1035
+ }
1036
+
1037
+ function closeSkillFileModal(event) {
1038
+ if (event && event.target !== event.currentTarget) return;
1039
+ const modal = document.getElementById('skillFileModal');
1040
+ if (modal) modal.remove();
1041
+ skillFileEditorState = { skillName: '', fileName: '', location: '', content: '', isEditing: false };
1042
+ }
1043
+
1044
+ function toggleSkillFileEdit() {
1045
+ skillFileEditorState.isEditing = true;
1046
+ renderSkillFileModal();
1047
+ }
1048
+
1049
+ function cancelSkillFileEdit() {
1050
+ skillFileEditorState.isEditing = false;
1051
+ renderSkillFileModal();
1052
+ }
1053
+
1054
+ async function saveSkillFile() {
1055
+ const contentTextarea = document.getElementById('skillFileContent');
1056
+ if (!contentTextarea) return;
1057
+
1058
+ const newContent = contentTextarea.value;
1059
+ const { skillName, fileName, location } = skillFileEditorState;
1060
+
1061
+ try {
1062
+ const response = await fetch('/api/skills/' + encodeURIComponent(skillName) + '/file', {
1063
+ method: 'POST',
1064
+ headers: { 'Content-Type': 'application/json' },
1065
+ body: JSON.stringify({
1066
+ fileName,
1067
+ content: newContent,
1068
+ location,
1069
+ projectPath
1070
+ })
1071
+ });
1072
+
1073
+ if (!response.ok) {
1074
+ const error = await response.json();
1075
+ throw new Error(error.error || 'Failed to save file');
1076
+ }
1077
+
1078
+ // Update state and close edit mode
1079
+ skillFileEditorState.content = newContent;
1080
+ skillFileEditorState.isEditing = false;
1081
+ renderSkillFileModal();
1082
+
1083
+ // Refresh skill detail if SKILL.md was edited
1084
+ if (fileName === 'SKILL.md') {
1085
+ await loadSkillsData();
1086
+ // Reload current skill detail
1087
+ if (selectedSkill) {
1088
+ await showSkillDetail(skillName, location);
1089
+ }
1090
+ }
1091
+
1092
+ if (window.showToast) {
1093
+ showToast(t('skills.fileSaved') || 'File saved successfully', 'success');
1094
+ }
1095
+ } catch (err) {
1096
+ console.error('Failed to save skill file:', err);
1097
+ if (window.showToast) {
1098
+ showToast(err.message || t('skills.fileSaveError') || 'Failed to save file', 'error');
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+
1104
+
1105
+ // ========== Skill Folder Expansion Functions ==========
1106
+
1107
+ var expandedFolders = new Set();
1108
+
1109
+ async function toggleSkillFolder(skillName, subPath, location, element) {
1110
+ const fileItem = element.closest('.skill-file-item');
1111
+ if (!fileItem) return;
1112
+
1113
+ const contentsDiv = fileItem.querySelector('.folder-contents');
1114
+ const chevron = element.querySelector('.folder-chevron');
1115
+ const folderIcon = element.querySelector('.folder-icon');
1116
+ const folderKey = `${skillName}:${subPath}:${location}`;
1117
+
1118
+ if (expandedFolders.has(folderKey)) {
1119
+ // Collapse folder
1120
+ expandedFolders.delete(folderKey);
1121
+ contentsDiv.classList.add('hidden');
1122
+ contentsDiv.innerHTML = '';
1123
+ if (chevron) chevron.style.transform = '';
1124
+ if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder');
1125
+ if (typeof lucide !== 'undefined') lucide.createIcons();
1126
+ } else {
1127
+ // Expand folder
1128
+ try {
1129
+ const response = await fetch(
1130
+ '/api/skills/' + encodeURIComponent(skillName) + '/dir?subpath=' + encodeURIComponent(subPath) +
1131
+ '&location=' + location + '&path=' + encodeURIComponent(projectPath)
1132
+ );
1133
+
1134
+ if (!response.ok) {
1135
+ const error = await response.json();
1136
+ throw new Error(error.error || 'Failed to load folder');
1137
+ }
1138
+
1139
+ const data = await response.json();
1140
+
1141
+ expandedFolders.add(folderKey);
1142
+ if (chevron) chevron.style.transform = 'rotate(90deg)';
1143
+ if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder-open');
1144
+
1145
+ // Render folder contents
1146
+ contentsDiv.innerHTML = data.files.map(file => {
1147
+ const filePath = file.path;
1148
+ const isDir = file.isDirectory;
1149
+ return `
1150
+ <div class="skill-file-item" data-path="${escapeHtml(filePath)}">
1151
+ <div class="flex items-center justify-between p-2 bg-muted/30 rounded-lg cursor-pointer hover:bg-muted/50 transition-colors"
1152
+ onclick="${isDir ? `toggleSkillFolder('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}', this)` : `viewSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')`}">
1153
+ <div class="flex items-center gap-2">
1154
+ <i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
1155
+ <span class="text-sm font-mono text-foreground">${escapeHtml(file.name)}</span>
1156
+ ${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
1157
+ </div>
1158
+ ${!isDir ? `
1159
+ <div class="flex items-center gap-1">
1160
+ <button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
1161
+ onclick="event.stopPropagation(); editSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')"
1162
+ title="${t('common.edit')}">
1163
+ <i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
1164
+ </button>
1165
+ </div>
1166
+ ` : ''}
1167
+ </div>
1168
+ <div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
1169
+ </div>
1170
+ `;
1171
+ }).join('');
1172
+
1173
+ contentsDiv.classList.remove('hidden');
1174
+ if (typeof lucide !== 'undefined') lucide.createIcons();
1175
+ } catch (err) {
1176
+ console.error('Failed to load folder contents:', err);
1177
+ if (window.showToast) {
1178
+ showToast(err.message || 'Failed to load folder', 'error');
1179
+ }
1180
+ }
1181
+ }
1182
+ }
1183
+
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { z } from 'zod';
7
7
  import type { ToolSchema, ToolResult } from '../types/tool.js';
8
+ import type { HistoryIndexEntry } from './cli-history-store.js';
8
9
  import { spawn, ChildProcess } from 'child_process';
9
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs';
10
11
  import { join, relative } from 'path';
@@ -1982,6 +1983,7 @@ export async function getEnrichedConversation(baseDir: string, ccwId: string) {
1982
1983
 
1983
1984
  /**
1984
1985
  * Get history with native session info
1986
+ * Supports recursive querying of child projects
1985
1987
  */
1986
1988
  export async function getHistoryWithNativeInfo(baseDir: string, options?: {
1987
1989
  limit?: number;
@@ -1990,9 +1992,75 @@ export async function getHistoryWithNativeInfo(baseDir: string, options?: {
1990
1992
  status?: string | null;
1991
1993
  category?: ExecutionCategory | null;
1992
1994
  search?: string | null;
1995
+ recursive?: boolean;
1993
1996
  }) {
1994
- const store = await getSqliteStore(baseDir);
1995
- return store.getHistoryWithNativeInfo(options || {});
1997
+ const { limit = 50, recursive = false, ...queryOptions } = options || {};
1998
+
1999
+ // Non-recursive mode: query single project
2000
+ if (!recursive) {
2001
+ const store = await getSqliteStore(baseDir);
2002
+ return store.getHistoryWithNativeInfo({ limit, ...queryOptions });
2003
+ }
2004
+
2005
+ // Recursive mode: aggregate data from parent and all child projects
2006
+ const { scanChildProjectsAsync } = await import('../config/storage-paths.js');
2007
+ const childProjects = await scanChildProjectsAsync(baseDir);
2008
+
2009
+ // Use the same type as store.getHistoryWithNativeInfo returns
2010
+ type ExecutionWithNativeAndSource = HistoryIndexEntry & {
2011
+ hasNativeSession: boolean;
2012
+ nativeSessionId?: string;
2013
+ nativeSessionPath?: string;
2014
+ };
2015
+
2016
+ const allExecutions: ExecutionWithNativeAndSource[] = [];
2017
+ let totalCount = 0;
2018
+
2019
+ // Query parent project
2020
+ try {
2021
+ const parentStore = await getSqliteStore(baseDir);
2022
+ const parentResult = parentStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
2023
+ totalCount += parentResult.total;
2024
+
2025
+ for (const exec of parentResult.executions) {
2026
+ allExecutions.push({ ...exec, sourceDir: baseDir });
2027
+ }
2028
+ } catch (error) {
2029
+ if (process.env.DEBUG) {
2030
+ console.error(`[CLI History] Failed to query parent project ${baseDir}:`, error);
2031
+ }
2032
+ }
2033
+
2034
+ // Query all child projects
2035
+ for (const child of childProjects) {
2036
+ try {
2037
+ const childStore = await getSqliteStore(child.projectPath);
2038
+ const childResult = childStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
2039
+ totalCount += childResult.total;
2040
+
2041
+ for (const exec of childResult.executions) {
2042
+ allExecutions.push({ ...exec, sourceDir: child.projectPath });
2043
+ }
2044
+ } catch (error) {
2045
+ if (process.env.DEBUG) {
2046
+ console.error(`[CLI History] Failed to query child project ${child.projectPath}:`, error);
2047
+ }
2048
+ }
2049
+ }
2050
+
2051
+ // Sort by updated_at descending and apply limit
2052
+ allExecutions.sort((a, b) => {
2053
+ const timeA = a.updated_at ? new Date(a.updated_at).getTime() : new Date(a.timestamp).getTime();
2054
+ const timeB = b.updated_at ? new Date(b.updated_at).getTime() : new Date(b.timestamp).getTime();
2055
+ return timeB - timeA;
2056
+ });
2057
+ const limitedExecutions = allExecutions.slice(0, limit);
2058
+
2059
+ return {
2060
+ total: totalCount,
2061
+ count: limitedExecutions.length,
2062
+ executions: limitedExecutions
2063
+ };
1996
2064
  }
1997
2065
 
1998
2066
  // Export types