x-ipe 1.0.24__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 +25 -3
- 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 +17 -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 +2 -0
- 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 +30 -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 +19 -0
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +4 -0
- 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 +32 -0
- 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 +1 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/workplace.css +20 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -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-search.js +6 -2
- x_ipe/static/js/features/workplace.js +200 -6
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/uiux-feedback.js +18 -2
- x_ipe/templates/base.html +19 -0
- x_ipe/templates/index.html +7 -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.24.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/RECORD +132 -62
- 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.24.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.24.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
|
|
@@ -142,6 +144,7 @@ def _is_binary_content_type(content_type: str) -> bool:
|
|
|
142
144
|
class ProxyService:
|
|
143
145
|
"""Service for proxying localhost URLs."""
|
|
144
146
|
|
|
147
|
+
@x_ipe_tracing()
|
|
145
148
|
def validate_url(self, url: str) -> Tuple[bool, str]:
|
|
146
149
|
"""
|
|
147
150
|
Validate URL is localhost or local file.
|
|
@@ -182,6 +185,7 @@ class ProxyService:
|
|
|
182
185
|
except Exception as e:
|
|
183
186
|
return False, f"Invalid URL format: {str(e)}"
|
|
184
187
|
|
|
188
|
+
@x_ipe_tracing()
|
|
185
189
|
def fetch_and_rewrite(self, url: str) -> ProxyResult:
|
|
186
190
|
"""
|
|
187
191
|
Fetch URL and rewrite asset paths for proxy.
|
|
@@ -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.
|
|
@@ -13,6 +13,8 @@ from collections import deque
|
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from typing import Dict, Optional, Any, Callable
|
|
15
15
|
|
|
16
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
# Constants for session management
|
|
18
20
|
BUFFER_MAX_CHARS = 10240 # 10KB limit for output buffer
|
|
@@ -30,15 +32,18 @@ class OutputBuffer:
|
|
|
30
32
|
def __init__(self, max_chars: int = BUFFER_MAX_CHARS):
|
|
31
33
|
self._buffer: deque = deque(maxlen=max_chars)
|
|
32
34
|
|
|
35
|
+
@x_ipe_tracing()
|
|
33
36
|
def append(self, data: str) -> None:
|
|
34
37
|
"""Append data character by character to maintain limit."""
|
|
35
38
|
for char in data:
|
|
36
39
|
self._buffer.append(char)
|
|
37
40
|
|
|
41
|
+
@x_ipe_tracing()
|
|
38
42
|
def get_contents(self) -> str:
|
|
39
43
|
"""Get all buffered content as string."""
|
|
40
44
|
return ''.join(self._buffer)
|
|
41
45
|
|
|
46
|
+
@x_ipe_tracing()
|
|
42
47
|
def clear(self) -> None:
|
|
43
48
|
"""Clear the buffer."""
|
|
44
49
|
self._buffer.clear()
|
|
@@ -68,6 +73,7 @@ class PersistentSession:
|
|
|
68
73
|
self.created_at = datetime.now()
|
|
69
74
|
self._lock = threading.Lock()
|
|
70
75
|
|
|
76
|
+
@x_ipe_tracing()
|
|
71
77
|
def start_pty(self, rows: int = 24, cols: int = 80) -> None:
|
|
72
78
|
"""Start the underlying PTY process."""
|
|
73
79
|
def buffered_emit(data: str) -> None:
|
|
@@ -80,6 +86,7 @@ class PersistentSession:
|
|
|
80
86
|
self.pty_session = PTYSession(self.session_id, buffered_emit)
|
|
81
87
|
self.pty_session.start(rows, cols)
|
|
82
88
|
|
|
89
|
+
@x_ipe_tracing()
|
|
83
90
|
def attach(self, socket_sid: str, emit_callback: Callable[[str], None]) -> None:
|
|
84
91
|
"""Attach a WebSocket connection to this session."""
|
|
85
92
|
with self._lock:
|
|
@@ -88,6 +95,7 @@ class PersistentSession:
|
|
|
88
95
|
self.state = 'connected'
|
|
89
96
|
self.disconnect_time = None
|
|
90
97
|
|
|
98
|
+
@x_ipe_tracing()
|
|
91
99
|
def detach(self) -> None:
|
|
92
100
|
"""Detach WebSocket, keeping PTY alive for reconnection."""
|
|
93
101
|
with self._lock:
|
|
@@ -96,20 +104,24 @@ class PersistentSession:
|
|
|
96
104
|
self.state = 'disconnected'
|
|
97
105
|
self.disconnect_time = datetime.now()
|
|
98
106
|
|
|
107
|
+
@x_ipe_tracing()
|
|
99
108
|
def get_buffer(self) -> str:
|
|
100
109
|
"""Get buffered output for replay."""
|
|
101
110
|
return self.output_buffer.get_contents()
|
|
102
111
|
|
|
112
|
+
@x_ipe_tracing()
|
|
103
113
|
def write(self, data: str) -> None:
|
|
104
114
|
"""Write input to PTY."""
|
|
105
115
|
if self.pty_session:
|
|
106
116
|
self.pty_session.write(data)
|
|
107
117
|
|
|
118
|
+
@x_ipe_tracing()
|
|
108
119
|
def resize(self, rows: int, cols: int) -> None:
|
|
109
120
|
"""Resize the PTY."""
|
|
110
121
|
if self.pty_session:
|
|
111
122
|
self.pty_session._set_size(rows, cols)
|
|
112
123
|
|
|
124
|
+
@x_ipe_tracing()
|
|
113
125
|
def is_expired(self, timeout_seconds: int = SESSION_TIMEOUT) -> bool:
|
|
114
126
|
"""Check if session has expired (1hr after disconnect)."""
|
|
115
127
|
if self.state == 'connected':
|
|
@@ -119,6 +131,7 @@ class PersistentSession:
|
|
|
119
131
|
elapsed = datetime.now() - self.disconnect_time
|
|
120
132
|
return elapsed.total_seconds() > timeout_seconds
|
|
121
133
|
|
|
134
|
+
@x_ipe_tracing()
|
|
122
135
|
def close(self) -> None:
|
|
123
136
|
"""Close session and cleanup resources."""
|
|
124
137
|
if self.pty_session:
|
|
@@ -139,6 +152,7 @@ class SessionManager:
|
|
|
139
152
|
self._cleanup_timer: Optional[threading.Timer] = None
|
|
140
153
|
self._running = False
|
|
141
154
|
|
|
155
|
+
@x_ipe_tracing()
|
|
142
156
|
def create_session(self, emit_callback: Callable[[str], None],
|
|
143
157
|
rows: int = 24, cols: int = 80) -> str:
|
|
144
158
|
"""Create new persistent session, returns session_id."""
|
|
@@ -152,16 +166,19 @@ class SessionManager:
|
|
|
152
166
|
|
|
153
167
|
return session_id
|
|
154
168
|
|
|
169
|
+
@x_ipe_tracing()
|
|
155
170
|
def get_session(self, session_id: str) -> Optional[PersistentSession]:
|
|
156
171
|
"""Get session by ID."""
|
|
157
172
|
with self._lock:
|
|
158
173
|
return self.sessions.get(session_id)
|
|
159
174
|
|
|
175
|
+
@x_ipe_tracing()
|
|
160
176
|
def has_session(self, session_id: str) -> bool:
|
|
161
177
|
"""Check if session exists."""
|
|
162
178
|
with self._lock:
|
|
163
179
|
return session_id in self.sessions
|
|
164
180
|
|
|
181
|
+
@x_ipe_tracing()
|
|
165
182
|
def remove_session(self, session_id: str) -> None:
|
|
166
183
|
"""Remove and close a session."""
|
|
167
184
|
with self._lock:
|
|
@@ -169,6 +186,7 @@ class SessionManager:
|
|
|
169
186
|
if session:
|
|
170
187
|
session.close()
|
|
171
188
|
|
|
189
|
+
@x_ipe_tracing()
|
|
172
190
|
def cleanup_expired(self) -> int:
|
|
173
191
|
"""Remove expired sessions. Returns count removed."""
|
|
174
192
|
expired_ids = []
|
|
@@ -182,11 +200,13 @@ class SessionManager:
|
|
|
182
200
|
|
|
183
201
|
return len(expired_ids)
|
|
184
202
|
|
|
203
|
+
@x_ipe_tracing()
|
|
185
204
|
def start_cleanup_task(self) -> None:
|
|
186
205
|
"""Start background cleanup task (every 5 minutes)."""
|
|
187
206
|
self._running = True
|
|
188
207
|
self._schedule_cleanup()
|
|
189
208
|
|
|
209
|
+
@x_ipe_tracing()
|
|
190
210
|
def stop_cleanup_task(self) -> None:
|
|
191
211
|
"""Stop the cleanup task."""
|
|
192
212
|
self._running = False
|
|
@@ -230,6 +250,7 @@ class PTYSession:
|
|
|
230
250
|
self.rows = 24
|
|
231
251
|
self.cols = 80
|
|
232
252
|
|
|
253
|
+
@x_ipe_tracing()
|
|
233
254
|
def start(self, rows: int = 24, cols: int = 80) -> None:
|
|
234
255
|
"""Spawn PTY with shell and start output reader."""
|
|
235
256
|
import pty
|
|
@@ -310,6 +331,7 @@ class PTYSession:
|
|
|
310
331
|
except Exception:
|
|
311
332
|
pass
|
|
312
333
|
|
|
334
|
+
@x_ipe_tracing()
|
|
313
335
|
def write(self, data: str) -> None:
|
|
314
336
|
"""Write input to PTY."""
|
|
315
337
|
if self.fd is not None:
|
|
@@ -328,6 +350,7 @@ class PTYSession:
|
|
|
328
350
|
winsize = struct.pack('HHHH', self.rows, self.cols, 0, 0)
|
|
329
351
|
fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
|
|
330
352
|
|
|
353
|
+
@x_ipe_tracing()
|
|
331
354
|
def close(self) -> None:
|
|
332
355
|
"""Terminate PTY session and cleanup."""
|
|
333
356
|
import signal
|
|
@@ -351,6 +374,7 @@ class PTYSession:
|
|
|
351
374
|
pass
|
|
352
375
|
self.pid = None
|
|
353
376
|
|
|
377
|
+
@x_ipe_tracing()
|
|
354
378
|
def isalive(self) -> bool:
|
|
355
379
|
"""Check if the PTY process is still running."""
|
|
356
380
|
return self._running and self.fd is not None
|
x_ipe/services/themes_service.py
CHANGED
|
@@ -10,6 +10,8 @@ import re
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Dict, List, Any, Optional
|
|
12
12
|
|
|
13
|
+
from x_ipe.tracing import x_ipe_tracing
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
THEMES_DIR = 'x-ipe-docs/themes'
|
|
15
17
|
THEME_PREFIX = 'theme-'
|
|
@@ -45,6 +47,7 @@ class ThemesService:
|
|
|
45
47
|
self.project_root = Path(project_root).resolve()
|
|
46
48
|
self.themes_dir = self.project_root / THEMES_DIR
|
|
47
49
|
|
|
50
|
+
@x_ipe_tracing()
|
|
48
51
|
def list_themes(self) -> List[Dict[str, Any]]:
|
|
49
52
|
"""
|
|
50
53
|
List all valid themes with metadata.
|
|
@@ -92,6 +95,7 @@ class ThemesService:
|
|
|
92
95
|
|
|
93
96
|
return themes
|
|
94
97
|
|
|
98
|
+
@x_ipe_tracing()
|
|
95
99
|
def get_theme(self, name: str) -> Optional[Dict[str, Any]]:
|
|
96
100
|
"""
|
|
97
101
|
Get detailed information about a specific theme.
|