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.
Files changed (146) hide show
  1. x_ipe/app.py +32 -1
  2. x_ipe/handlers/terminal_handlers.py +6 -0
  3. x_ipe/handlers/voice_handlers.py +5 -0
  4. x_ipe/resources/copilot-instructions.md +19 -6
  5. x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
  6. x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
  7. x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
  8. x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
  9. x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
  10. x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
  11. x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
  12. x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
  13. x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
  14. x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
  15. x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
  16. x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
  17. x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
  18. x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
  19. x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
  20. x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
  21. x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
  22. x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
  23. x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
  24. x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
  25. x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
  26. x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
  27. x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
  28. x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
  29. x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
  30. x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
  31. x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
  32. x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
  33. x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
  34. x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
  35. x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
  36. x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
  37. x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
  38. x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
  39. x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
  40. x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
  41. x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
  42. x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
  43. x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
  44. x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
  45. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
  46. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
  47. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
  48. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
  49. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
  50. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
  51. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
  52. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
  53. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
  54. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
  55. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
  56. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
  57. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
  58. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
  59. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
  60. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
  61. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
  62. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
  63. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
  64. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
  65. x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
  66. x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
  67. x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
  68. x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
  69. x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
  70. x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
  71. x_ipe/routes/__init__.py +2 -0
  72. x_ipe/routes/ideas_routes.py +289 -0
  73. x_ipe/routes/kb_routes.py +80 -0
  74. x_ipe/routes/main_routes.py +18 -0
  75. x_ipe/routes/project_routes.py +7 -0
  76. x_ipe/routes/proxy_routes.py +10 -2
  77. x_ipe/routes/quality_evaluation_routes.py +193 -0
  78. x_ipe/routes/settings_routes.py +6 -0
  79. x_ipe/routes/tools_routes.py +6 -0
  80. x_ipe/routes/tracing_routes.py +232 -0
  81. x_ipe/routes/uiux_feedback_routes.py +50 -0
  82. x_ipe/services/__init__.py +5 -0
  83. x_ipe/services/config_service.py +6 -0
  84. x_ipe/services/file_service.py +20 -0
  85. x_ipe/services/homepage_service.py +160 -0
  86. x_ipe/services/ideas_service.py +535 -2
  87. x_ipe/services/kb_service.py +378 -0
  88. x_ipe/services/proxy_service.py +37 -7
  89. x_ipe/services/settings_service.py +13 -0
  90. x_ipe/services/skills_service.py +4 -0
  91. x_ipe/services/terminal_service.py +24 -0
  92. x_ipe/services/themes_service.py +4 -0
  93. x_ipe/services/tools_config_service.py +4 -0
  94. x_ipe/services/tracing_service.py +333 -0
  95. x_ipe/services/uiux_feedback_service.py +148 -1
  96. x_ipe/services/voice_input_service_v2.py +11 -0
  97. x_ipe/static/css/base.css +7 -0
  98. x_ipe/static/css/homepage-infinity.css +330 -0
  99. x_ipe/static/css/kb-core.css +301 -0
  100. x_ipe/static/css/quality-evaluation.css +345 -0
  101. x_ipe/static/css/sidebar.css +14 -4
  102. x_ipe/static/css/terminal.css +23 -0
  103. x_ipe/static/css/tracing-dashboard.css +796 -0
  104. x_ipe/static/css/uiux-feedback.css +7 -1
  105. x_ipe/static/css/workplace.css +636 -0
  106. x_ipe/static/img/homepage-infinity-loop.png +0 -0
  107. x_ipe/static/js/features/confirm-dialog.js +169 -0
  108. x_ipe/static/js/features/folder-view.js +742 -0
  109. x_ipe/static/js/features/homepage-infinity.js +314 -0
  110. x_ipe/static/js/features/kb-core.js +371 -0
  111. x_ipe/static/js/features/quality-evaluation.js +387 -0
  112. x_ipe/static/js/features/sidebar.js +255 -12
  113. x_ipe/static/js/features/tracing-dashboard.js +855 -0
  114. x_ipe/static/js/features/tracing-graph.js +1031 -0
  115. x_ipe/static/js/features/tree-drag.js +227 -0
  116. x_ipe/static/js/features/tree-search.js +228 -0
  117. x_ipe/static/js/features/workplace.js +661 -33
  118. x_ipe/static/js/init.js +76 -0
  119. x_ipe/static/js/terminal-v2.js +45 -14
  120. x_ipe/static/js/terminal.js +50 -49
  121. x_ipe/static/js/uiux-feedback.js +75 -16
  122. x_ipe/templates/base.html +24 -0
  123. x_ipe/templates/index.html +10 -1
  124. x_ipe/templates/knowledge-base.html +110 -0
  125. x_ipe/templates/workplace.html +4 -0
  126. x_ipe/tracing/__init__.py +37 -0
  127. x_ipe/tracing/buffer.py +135 -0
  128. x_ipe/tracing/context.py +125 -0
  129. x_ipe/tracing/decorator.py +288 -0
  130. x_ipe/tracing/middleware.py +197 -0
  131. x_ipe/tracing/parser.py +235 -0
  132. x_ipe/tracing/redactor.py +111 -0
  133. x_ipe/tracing/writer.py +122 -0
  134. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
  135. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
  136. x_ipe/app.py.bak +0 -1333
  137. x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
  138. x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
  139. x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
  140. x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
  141. x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
  142. x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
  143. x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
  144. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
  145. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
  146. {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)
@@ -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 non-HTML content as-is (use response.content for binary safety)
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=content,
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.
@@ -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.