x-ipe 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl
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.
- x_ipe/app.py +32 -1
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +289 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +10 -2
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +50 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +535 -2
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +37 -7
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +148 -1
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +23 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/uiux-feedback.css +7 -1
- x_ipe/static/css/workplace.css +636 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/confirm-dialog.js +169 -0
- x_ipe/static/js/features/folder-view.js +742 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-drag.js +227 -0
- x_ipe/static/js/features/tree-search.js +228 -0
- x_ipe/static/js/features/workplace.js +661 -33
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/terminal-v2.js +45 -14
- x_ipe/static/js/terminal.js +50 -49
- x_ipe/static/js/uiux-feedback.js +75 -16
- x_ipe/templates/base.html +24 -0
- x_ipe/templates/index.html +10 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
- x_ipe/app.py.bak +0 -1333
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-025-A: KB Core Infrastructure
|
|
3
|
+
|
|
4
|
+
KBService: Core Knowledge Base operations
|
|
5
|
+
- Folder structure initialization
|
|
6
|
+
- File index management
|
|
7
|
+
- Topic metadata management
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Any, Optional
|
|
14
|
+
|
|
15
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class KBService:
|
|
19
|
+
"""
|
|
20
|
+
Service for managing Knowledge Base infrastructure.
|
|
21
|
+
|
|
22
|
+
Provides core operations for the x-ipe-docs/knowledge-base/ directory:
|
|
23
|
+
- initialize_structure(): Create KB folder structure
|
|
24
|
+
- get_index(): Get current file index
|
|
25
|
+
- refresh_index(): Rebuild index from file system
|
|
26
|
+
- get_topics(): List all topics
|
|
27
|
+
- get_topic_metadata(): Get topic metadata
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
KB_PATH = 'x-ipe-docs/knowledge-base'
|
|
31
|
+
|
|
32
|
+
# File type mapping
|
|
33
|
+
FILE_TYPE_MAP = {
|
|
34
|
+
# Documents
|
|
35
|
+
'.pdf': 'pdf',
|
|
36
|
+
'.md': 'markdown',
|
|
37
|
+
'.markdown': 'markdown',
|
|
38
|
+
'.txt': 'text',
|
|
39
|
+
'.docx': 'docx',
|
|
40
|
+
'.xlsx': 'xlsx',
|
|
41
|
+
|
|
42
|
+
# Code
|
|
43
|
+
'.py': 'python',
|
|
44
|
+
'.js': 'javascript',
|
|
45
|
+
'.ts': 'typescript',
|
|
46
|
+
'.java': 'java',
|
|
47
|
+
'.go': 'go',
|
|
48
|
+
'.rs': 'rust',
|
|
49
|
+
'.c': 'c',
|
|
50
|
+
'.cpp': 'cpp',
|
|
51
|
+
'.h': 'header',
|
|
52
|
+
'.html': 'html',
|
|
53
|
+
'.css': 'css',
|
|
54
|
+
'.json': 'json',
|
|
55
|
+
'.yaml': 'yaml',
|
|
56
|
+
'.yml': 'yaml',
|
|
57
|
+
|
|
58
|
+
# Images
|
|
59
|
+
'.png': 'image',
|
|
60
|
+
'.jpg': 'image',
|
|
61
|
+
'.jpeg': 'image',
|
|
62
|
+
'.gif': 'image',
|
|
63
|
+
'.svg': 'image',
|
|
64
|
+
'.webp': 'image',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def __init__(self, project_root: str):
|
|
68
|
+
"""
|
|
69
|
+
Initialize KBService.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
project_root: Absolute path to the project root directory
|
|
73
|
+
"""
|
|
74
|
+
self.project_root = Path(project_root).resolve()
|
|
75
|
+
self.kb_root = self.project_root / self.KB_PATH
|
|
76
|
+
self.index_path = self.kb_root / 'index' / 'file-index.json'
|
|
77
|
+
|
|
78
|
+
@x_ipe_tracing(level="INFO")
|
|
79
|
+
def initialize_structure(self) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Create KB folder structure if it doesn't exist.
|
|
82
|
+
|
|
83
|
+
Creates:
|
|
84
|
+
- landing/: Raw uploads
|
|
85
|
+
- topics/: Organized by topic
|
|
86
|
+
- processed/: AI summaries
|
|
87
|
+
- index/: Search index files
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if successful
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Create main KB directory and subfolders
|
|
94
|
+
folders = [
|
|
95
|
+
self.kb_root / 'landing',
|
|
96
|
+
self.kb_root / 'topics',
|
|
97
|
+
self.kb_root / 'processed',
|
|
98
|
+
self.kb_root / 'index',
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for folder in folders:
|
|
102
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
# Create empty index if it doesn't exist
|
|
105
|
+
if not self.index_path.exists():
|
|
106
|
+
empty_index = {
|
|
107
|
+
"version": "1.0",
|
|
108
|
+
"last_updated": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
|
109
|
+
"files": []
|
|
110
|
+
}
|
|
111
|
+
self._write_json(self.index_path, empty_index)
|
|
112
|
+
|
|
113
|
+
return True
|
|
114
|
+
except Exception as e:
|
|
115
|
+
# Log error but don't raise
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
@x_ipe_tracing(level="INFO")
|
|
119
|
+
def get_index(self) -> Dict:
|
|
120
|
+
"""
|
|
121
|
+
Get current file index.
|
|
122
|
+
|
|
123
|
+
If KB folder doesn't exist, initializes structure first.
|
|
124
|
+
If index is corrupted, recreates empty index.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
File index dictionary with version, last_updated, and files list
|
|
128
|
+
"""
|
|
129
|
+
# Initialize structure if needed
|
|
130
|
+
if not self.kb_root.exists():
|
|
131
|
+
self.initialize_structure()
|
|
132
|
+
|
|
133
|
+
# Read index
|
|
134
|
+
try:
|
|
135
|
+
if self.index_path.exists():
|
|
136
|
+
with open(self.index_path, 'r', encoding='utf-8') as f:
|
|
137
|
+
return json.load(f)
|
|
138
|
+
except (json.JSONDecodeError, IOError):
|
|
139
|
+
# Corrupted index - recreate
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# Return/create empty index
|
|
143
|
+
empty_index = {
|
|
144
|
+
"version": "1.0",
|
|
145
|
+
"last_updated": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
|
146
|
+
"files": []
|
|
147
|
+
}
|
|
148
|
+
self._write_json(self.index_path, empty_index)
|
|
149
|
+
return empty_index
|
|
150
|
+
|
|
151
|
+
@x_ipe_tracing(level="INFO")
|
|
152
|
+
def refresh_index(self) -> Dict:
|
|
153
|
+
"""
|
|
154
|
+
Rebuild index from file system.
|
|
155
|
+
|
|
156
|
+
Scans landing/, topics/, and processed/ folders.
|
|
157
|
+
Updates topic metadata files.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Updated file index dictionary
|
|
161
|
+
"""
|
|
162
|
+
# Initialize structure if needed
|
|
163
|
+
if not self.kb_root.exists():
|
|
164
|
+
self.initialize_structure()
|
|
165
|
+
|
|
166
|
+
files = []
|
|
167
|
+
|
|
168
|
+
# Scan landing folder
|
|
169
|
+
landing_path = self.kb_root / 'landing'
|
|
170
|
+
if landing_path.exists():
|
|
171
|
+
files.extend(self._scan_folder(landing_path, topic=None))
|
|
172
|
+
|
|
173
|
+
# Scan topics folder
|
|
174
|
+
topics_path = self.kb_root / 'topics'
|
|
175
|
+
if topics_path.exists():
|
|
176
|
+
for topic_dir in topics_path.iterdir():
|
|
177
|
+
if topic_dir.is_dir() and not topic_dir.name.startswith('.'):
|
|
178
|
+
topic_name = topic_dir.name
|
|
179
|
+
# Scan raw subfolder
|
|
180
|
+
raw_path = topic_dir / 'raw'
|
|
181
|
+
if raw_path.exists():
|
|
182
|
+
files.extend(self._scan_folder(raw_path, topic=topic_name))
|
|
183
|
+
# Update topic metadata
|
|
184
|
+
self._update_topic_metadata(topic_name)
|
|
185
|
+
|
|
186
|
+
# Scan processed folder
|
|
187
|
+
processed_path = self.kb_root / 'processed'
|
|
188
|
+
if processed_path.exists():
|
|
189
|
+
for topic_dir in processed_path.iterdir():
|
|
190
|
+
if topic_dir.is_dir() and not topic_dir.name.startswith('.'):
|
|
191
|
+
topic_name = topic_dir.name
|
|
192
|
+
files.extend(self._scan_folder(topic_dir, topic=topic_name, prefix='processed'))
|
|
193
|
+
|
|
194
|
+
# Build index
|
|
195
|
+
index = {
|
|
196
|
+
"version": "1.0",
|
|
197
|
+
"last_updated": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
|
198
|
+
"files": files
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Write index
|
|
202
|
+
self._write_json(self.index_path, index)
|
|
203
|
+
|
|
204
|
+
return index
|
|
205
|
+
|
|
206
|
+
@x_ipe_tracing(level="INFO")
|
|
207
|
+
def get_topics(self) -> List[str]:
|
|
208
|
+
"""
|
|
209
|
+
Get list of all topic names.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of topic folder names
|
|
213
|
+
"""
|
|
214
|
+
topics = []
|
|
215
|
+
topics_path = self.kb_root / 'topics'
|
|
216
|
+
|
|
217
|
+
if topics_path.exists():
|
|
218
|
+
for item in topics_path.iterdir():
|
|
219
|
+
if item.is_dir() and not item.name.startswith('.'):
|
|
220
|
+
topics.append(item.name)
|
|
221
|
+
|
|
222
|
+
return sorted(topics)
|
|
223
|
+
|
|
224
|
+
@x_ipe_tracing(level="INFO")
|
|
225
|
+
def get_topic_metadata(self, topic: str) -> Optional[Dict]:
|
|
226
|
+
"""
|
|
227
|
+
Get metadata for a specific topic.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
topic: Topic folder name
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Topic metadata dictionary or None if not found
|
|
234
|
+
"""
|
|
235
|
+
metadata_path = self.kb_root / 'topics' / topic / 'metadata.json'
|
|
236
|
+
|
|
237
|
+
if not metadata_path.exists():
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
with open(metadata_path, 'r', encoding='utf-8') as f:
|
|
242
|
+
return json.load(f)
|
|
243
|
+
except (json.JSONDecodeError, IOError):
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def _scan_folder(self, folder: Path, topic: Optional[str], prefix: str = None) -> List[Dict]:
|
|
247
|
+
"""
|
|
248
|
+
Scan a folder and return file entries.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
folder: Path to folder to scan
|
|
252
|
+
topic: Topic name (None for landing)
|
|
253
|
+
prefix: Path prefix (e.g., 'processed')
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of file entry dictionaries
|
|
257
|
+
"""
|
|
258
|
+
files = []
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
for entry in folder.iterdir():
|
|
262
|
+
if entry.name.startswith('.'):
|
|
263
|
+
continue # Skip hidden files
|
|
264
|
+
|
|
265
|
+
if entry.is_file():
|
|
266
|
+
# Build relative path
|
|
267
|
+
if prefix:
|
|
268
|
+
rel_path = f"{prefix}/{topic}/{entry.name}"
|
|
269
|
+
elif topic:
|
|
270
|
+
rel_path = f"topics/{topic}/raw/{entry.name}"
|
|
271
|
+
else:
|
|
272
|
+
rel_path = f"landing/{entry.name}"
|
|
273
|
+
|
|
274
|
+
# Get file stats
|
|
275
|
+
stat = entry.stat()
|
|
276
|
+
|
|
277
|
+
file_entry = {
|
|
278
|
+
"path": rel_path,
|
|
279
|
+
"name": entry.name,
|
|
280
|
+
"type": self._get_file_type(entry.suffix),
|
|
281
|
+
"size": stat.st_size,
|
|
282
|
+
"topic": topic,
|
|
283
|
+
"created_date": datetime.fromtimestamp(
|
|
284
|
+
stat.st_mtime, tz=timezone.utc
|
|
285
|
+
).isoformat().replace('+00:00', 'Z'),
|
|
286
|
+
"keywords": self._extract_keywords(entry.name)
|
|
287
|
+
}
|
|
288
|
+
files.append(file_entry)
|
|
289
|
+
except PermissionError:
|
|
290
|
+
pass # Skip folders we can't read
|
|
291
|
+
|
|
292
|
+
return files
|
|
293
|
+
|
|
294
|
+
def _get_file_type(self, extension: str) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Get file type from extension.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
extension: File extension (e.g., '.pdf')
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
File type string (e.g., 'pdf', 'unknown')
|
|
303
|
+
"""
|
|
304
|
+
return self.FILE_TYPE_MAP.get(extension.lower(), 'unknown')
|
|
305
|
+
|
|
306
|
+
def _extract_keywords(self, filename: str) -> List[str]:
|
|
307
|
+
"""
|
|
308
|
+
Extract keywords from filename.
|
|
309
|
+
|
|
310
|
+
Splits on hyphens, underscores, spaces, and removes extension.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
filename: Filename to extract keywords from
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
List of lowercase keyword strings
|
|
317
|
+
"""
|
|
318
|
+
# Remove extension
|
|
319
|
+
name_without_ext = Path(filename).stem
|
|
320
|
+
|
|
321
|
+
# Split on common separators
|
|
322
|
+
parts = re.split(r'[-_\s]+', name_without_ext)
|
|
323
|
+
|
|
324
|
+
# Filter and lowercase
|
|
325
|
+
keywords = [p.lower() for p in parts if p]
|
|
326
|
+
|
|
327
|
+
return keywords
|
|
328
|
+
|
|
329
|
+
def _update_topic_metadata(self, topic: str) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Update or create metadata.json for a topic.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
topic: Topic folder name
|
|
335
|
+
"""
|
|
336
|
+
topic_path = self.kb_root / 'topics' / topic
|
|
337
|
+
metadata_path = topic_path / 'metadata.json'
|
|
338
|
+
raw_path = topic_path / 'raw'
|
|
339
|
+
|
|
340
|
+
# Count files in raw folder
|
|
341
|
+
file_count = 0
|
|
342
|
+
if raw_path.exists():
|
|
343
|
+
file_count = len([f for f in raw_path.iterdir()
|
|
344
|
+
if f.is_file() and not f.name.startswith('.')])
|
|
345
|
+
|
|
346
|
+
# Load existing or create new
|
|
347
|
+
if metadata_path.exists():
|
|
348
|
+
try:
|
|
349
|
+
with open(metadata_path, 'r', encoding='utf-8') as f:
|
|
350
|
+
metadata = json.load(f)
|
|
351
|
+
except (json.JSONDecodeError, IOError):
|
|
352
|
+
metadata = {}
|
|
353
|
+
else:
|
|
354
|
+
metadata = {}
|
|
355
|
+
|
|
356
|
+
# Update fields
|
|
357
|
+
metadata.update({
|
|
358
|
+
"name": topic,
|
|
359
|
+
"description": metadata.get("description", ""),
|
|
360
|
+
"file_count": file_count,
|
|
361
|
+
"last_updated": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
|
|
362
|
+
"tags": metadata.get("tags", [])
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
# Write
|
|
366
|
+
self._write_json(metadata_path, metadata)
|
|
367
|
+
|
|
368
|
+
def _write_json(self, path: Path, data: Dict) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Write JSON data to file with pretty formatting.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
path: Path to write to
|
|
374
|
+
data: Dictionary to write
|
|
375
|
+
"""
|
|
376
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
378
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
x_ipe/services/proxy_service.py
CHANGED
|
@@ -12,6 +12,8 @@ from bs4 import BeautifulSoup
|
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from typing import Tuple
|
|
14
14
|
|
|
15
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
16
|
+
|
|
15
17
|
ALLOWED_HOSTS = {'localhost', '127.0.0.1'}
|
|
16
18
|
ALLOWED_SCHEMES = {'http', 'https', 'file'}
|
|
17
19
|
PROXY_TIMEOUT = 10 # seconds
|
|
@@ -118,11 +120,31 @@ class ProxyResult:
|
|
|
118
120
|
content_type: str = "text/html"
|
|
119
121
|
error: str = ""
|
|
120
122
|
status_code: int = 200
|
|
123
|
+
binary_content: bytes = None # TASK-235: For binary files like fonts
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# TASK-235: Binary content types that should not be decoded as text
|
|
127
|
+
BINARY_CONTENT_TYPES = {
|
|
128
|
+
'font/', 'image/', 'audio/', 'video/',
|
|
129
|
+
'application/octet-stream', 'application/font', 'application/x-font',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _is_binary_content_type(content_type: str) -> bool:
|
|
134
|
+
"""Check if content type represents binary data."""
|
|
135
|
+
if not content_type:
|
|
136
|
+
return False
|
|
137
|
+
content_type = content_type.lower()
|
|
138
|
+
for binary_type in BINARY_CONTENT_TYPES:
|
|
139
|
+
if binary_type in content_type:
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
121
142
|
|
|
122
143
|
|
|
123
144
|
class ProxyService:
|
|
124
145
|
"""Service for proxying localhost URLs."""
|
|
125
146
|
|
|
147
|
+
@x_ipe_tracing()
|
|
126
148
|
def validate_url(self, url: str) -> Tuple[bool, str]:
|
|
127
149
|
"""
|
|
128
150
|
Validate URL is localhost or local file.
|
|
@@ -163,6 +185,7 @@ class ProxyService:
|
|
|
163
185
|
except Exception as e:
|
|
164
186
|
return False, f"Invalid URL format: {str(e)}"
|
|
165
187
|
|
|
188
|
+
@x_ipe_tracing()
|
|
166
189
|
def fetch_and_rewrite(self, url: str) -> ProxyResult:
|
|
167
190
|
"""
|
|
168
191
|
Fetch URL and rewrite asset paths for proxy.
|
|
@@ -209,20 +232,27 @@ class ProxyService:
|
|
|
209
232
|
|
|
210
233
|
content_type = response.headers.get('Content-Type', 'text/html')
|
|
211
234
|
|
|
235
|
+
# TASK-235: Handle binary content (fonts, images, etc.) without decoding
|
|
236
|
+
if _is_binary_content_type(content_type):
|
|
237
|
+
return ProxyResult(
|
|
238
|
+
success=True,
|
|
239
|
+
binary_content=response.content,
|
|
240
|
+
content_type=content_type
|
|
241
|
+
)
|
|
242
|
+
|
|
212
243
|
# Only rewrite HTML
|
|
213
244
|
if 'text/html' in content_type:
|
|
214
245
|
html = self._rewrite_html(response.text, url)
|
|
215
246
|
return ProxyResult(success=True, html=html, content_type=content_type)
|
|
247
|
+
# TASK-235: Rewrite CSS url() references for font files
|
|
248
|
+
elif 'text/css' in content_type:
|
|
249
|
+
css = self._rewrite_css_urls(response.text, url)
|
|
250
|
+
return ProxyResult(success=True, html=css, content_type=content_type)
|
|
216
251
|
else:
|
|
217
|
-
# Return
|
|
218
|
-
# For text content, decode; for binary, return bytes as string (will be raw in Response)
|
|
219
|
-
try:
|
|
220
|
-
content = response.text
|
|
221
|
-
except Exception:
|
|
222
|
-
content = response.content.decode('utf-8', errors='replace')
|
|
252
|
+
# Return text content as-is
|
|
223
253
|
return ProxyResult(
|
|
224
254
|
success=True,
|
|
225
|
-
html=
|
|
255
|
+
html=response.text,
|
|
226
256
|
content_type=content_type
|
|
227
257
|
)
|
|
228
258
|
|
|
@@ -8,6 +8,8 @@ import os
|
|
|
8
8
|
import sqlite3
|
|
9
9
|
from typing import Dict, List, Optional, Any
|
|
10
10
|
|
|
11
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class SettingsService:
|
|
13
15
|
"""
|
|
@@ -74,6 +76,7 @@ class SettingsService:
|
|
|
74
76
|
finally:
|
|
75
77
|
conn.close()
|
|
76
78
|
|
|
79
|
+
@x_ipe_tracing()
|
|
77
80
|
def get(self, key: str, default: Any = None) -> Optional[str]:
|
|
78
81
|
"""
|
|
79
82
|
Get a single setting value.
|
|
@@ -94,6 +97,7 @@ class SettingsService:
|
|
|
94
97
|
finally:
|
|
95
98
|
conn.close()
|
|
96
99
|
|
|
100
|
+
@x_ipe_tracing()
|
|
97
101
|
def get_all(self) -> Dict[str, str]:
|
|
98
102
|
"""
|
|
99
103
|
Get all settings as dictionary.
|
|
@@ -109,6 +113,7 @@ class SettingsService:
|
|
|
109
113
|
finally:
|
|
110
114
|
conn.close()
|
|
111
115
|
|
|
116
|
+
@x_ipe_tracing()
|
|
112
117
|
def set(self, key: str, value: str) -> None:
|
|
113
118
|
"""
|
|
114
119
|
Set a single setting value.
|
|
@@ -133,6 +138,7 @@ class SettingsService:
|
|
|
133
138
|
finally:
|
|
134
139
|
conn.close()
|
|
135
140
|
|
|
141
|
+
@x_ipe_tracing()
|
|
136
142
|
def validate_project_root(self, path: str) -> Dict[str, str]:
|
|
137
143
|
"""
|
|
138
144
|
Validate project root path.
|
|
@@ -265,6 +271,7 @@ class ProjectFoldersService:
|
|
|
265
271
|
finally:
|
|
266
272
|
conn.close()
|
|
267
273
|
|
|
274
|
+
@x_ipe_tracing()
|
|
268
275
|
def get_all(self) -> List[Dict]:
|
|
269
276
|
"""
|
|
270
277
|
Get all project folders.
|
|
@@ -280,6 +287,7 @@ class ProjectFoldersService:
|
|
|
280
287
|
finally:
|
|
281
288
|
conn.close()
|
|
282
289
|
|
|
290
|
+
@x_ipe_tracing()
|
|
283
291
|
def get_by_id(self, project_id: int) -> Optional[Dict]:
|
|
284
292
|
"""
|
|
285
293
|
Get project folder by ID.
|
|
@@ -299,6 +307,7 @@ class ProjectFoldersService:
|
|
|
299
307
|
finally:
|
|
300
308
|
conn.close()
|
|
301
309
|
|
|
310
|
+
@x_ipe_tracing()
|
|
302
311
|
def add(self, name: str, path: str) -> Dict:
|
|
303
312
|
"""
|
|
304
313
|
Add a new project folder.
|
|
@@ -333,6 +342,7 @@ class ProjectFoldersService:
|
|
|
333
342
|
finally:
|
|
334
343
|
conn.close()
|
|
335
344
|
|
|
345
|
+
@x_ipe_tracing()
|
|
336
346
|
def update(self, project_id: int, name: str = None, path: str = None) -> Dict:
|
|
337
347
|
"""
|
|
338
348
|
Update an existing project folder.
|
|
@@ -374,6 +384,7 @@ class ProjectFoldersService:
|
|
|
374
384
|
finally:
|
|
375
385
|
conn.close()
|
|
376
386
|
|
|
387
|
+
@x_ipe_tracing()
|
|
377
388
|
def delete(self, project_id: int, active_project_id: int = None) -> Dict:
|
|
378
389
|
"""
|
|
379
390
|
Delete a project folder.
|
|
@@ -437,6 +448,7 @@ class ProjectFoldersService:
|
|
|
437
448
|
|
|
438
449
|
return errors
|
|
439
450
|
|
|
451
|
+
@x_ipe_tracing()
|
|
440
452
|
def get_active_id(self) -> int:
|
|
441
453
|
"""
|
|
442
454
|
Get the active project ID from settings.
|
|
@@ -453,6 +465,7 @@ class ProjectFoldersService:
|
|
|
453
465
|
finally:
|
|
454
466
|
conn.close()
|
|
455
467
|
|
|
468
|
+
@x_ipe_tracing()
|
|
456
469
|
def set_active(self, project_id: int) -> Dict:
|
|
457
470
|
"""
|
|
458
471
|
Set the active project ID.
|
x_ipe/services/skills_service.py
CHANGED
|
@@ -5,6 +5,8 @@ import yaml
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, List, Optional
|
|
7
7
|
|
|
8
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class SkillsService:
|
|
10
12
|
"""
|
|
@@ -16,6 +18,7 @@ class SkillsService:
|
|
|
16
18
|
|
|
17
19
|
SKILLS_PATH = '.github/skills'
|
|
18
20
|
|
|
21
|
+
@x_ipe_tracing()
|
|
19
22
|
def __init__(self, project_root: str):
|
|
20
23
|
"""
|
|
21
24
|
Initialize SkillsService.
|
|
@@ -26,6 +29,7 @@ class SkillsService:
|
|
|
26
29
|
self.project_root = Path(project_root).resolve()
|
|
27
30
|
self.skills_dir = self.project_root / self.SKILLS_PATH
|
|
28
31
|
|
|
32
|
+
@x_ipe_tracing()
|
|
29
33
|
def get_all(self) -> List[Dict[str, str]]:
|
|
30
34
|
"""
|
|
31
35
|
Get all skills with their name and description.
|