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
x_ipe/app.py.bak DELETED
@@ -1,1333 +0,0 @@
1
- """
2
- Flask Application for Document Viewer
3
-
4
- FEATURE-001: Project Navigation
5
- - Provides API for project structure
6
- - HTTP polling for real-time updates (no WebSocket)
7
- - Serves frontend with sidebar navigation
8
-
9
- FEATURE-005: Interactive Console v4.0
10
- - WebSocket handlers for terminal I/O
11
- - Session persistence with automatic reconnection
12
- - xterm.js frontend integration
13
- """
14
- import os
15
- import sys
16
- import json
17
- from pathlib import Path
18
- from flask import Flask, render_template, jsonify, request, current_app, send_file
19
- from flask_socketio import SocketIO, emit
20
-
21
- # Load .env from config folder if it exists
22
- def load_env_file():
23
- """Load environment variables from x-ipe-docs/config/.env file."""
24
- env_path = Path(__file__).parent.parent.parent / 'x-ipe-docs' / 'config' / '.env'
25
- if env_path.exists():
26
- with open(env_path) as f:
27
- for line in f:
28
- line = line.strip()
29
- if line and not line.startswith('#') and '=' in line:
30
- key, value = line.split('=', 1)
31
- key = key.strip()
32
- value = value.strip()
33
- if value and key not in os.environ: # Don't override existing env vars
34
- os.environ[key] = value
35
-
36
- load_env_file()
37
-
38
- from x_ipe.services import ProjectService, ContentService, SettingsService, ProjectFoldersService, IdeasService, ConfigService, SkillsService, ToolsConfigService, ThemesService
39
- from x_ipe.services import session_manager
40
- from x_ipe.config import config_by_name
41
-
42
- # Global settings service instance
43
- settings_service = None
44
-
45
- # Global ideas service instance (FEATURE-008)
46
- ideas_service = None
47
-
48
- # Global project folders service instance (FEATURE-006 v2.0)
49
- project_folders_service = None
50
-
51
- # Global config service instance (FEATURE-010)
52
- config_service = None
53
-
54
- # Global tools config service instance (FEATURE-011)
55
- tools_config_service = None
56
-
57
- # Socket.IO instance with ping/pong for keep-alive
58
- # Increased timeouts for stability - session stays open for 1 hour regardless of focus
59
- socketio = SocketIO(
60
- cors_allowed_origins="*",
61
- async_mode='threading',
62
- ping_timeout=300, # Wait 5 minutes for pong response (was 60s)
63
- ping_interval=60, # Send ping every 60s (was 25s)
64
- max_http_buffer_size=1e8, # 100MB max message size
65
- always_connect=True, # Always emit connect even on reconnect
66
- logger=False,
67
- engineio_logger=False,
68
- # Improved stability settings
69
- http_compression=True, # Compress HTTP responses
70
- manage_session=True, # Let Socket.IO manage sessions
71
- )
72
-
73
- # Socket SID to Session ID mapping
74
- socket_to_session = {}
75
-
76
-
77
- def create_app(config=None):
78
- """
79
- Application factory for creating Flask app.
80
-
81
- Args:
82
- config: Configuration dict or config class name
83
- """
84
- app = Flask(__name__,
85
- static_folder='static',
86
- template_folder='templates')
87
-
88
- # Load configuration
89
- if config is None:
90
- config_name = os.environ.get('FLASK_ENV', 'development')
91
- app.config.from_object(config_by_name.get(config_name, config_by_name['default']))
92
- elif isinstance(config, dict):
93
- app.config.update(config)
94
- else:
95
- app.config.from_object(config)
96
-
97
- # Initialize settings service
98
- global settings_service, project_folders_service, ideas_service, config_service, tools_config_service
99
- db_path = app.config.get('SETTINGS_DB_PATH', app.config.get('SETTINGS_DB', os.path.join(app.instance_path, 'settings.db')))
100
- settings_service = SettingsService(db_path)
101
- project_folders_service = ProjectFoldersService(db_path)
102
-
103
- # Initialize config service and load .x-ipe.yaml (FEATURE-010)
104
- if not app.config.get('TESTING'):
105
- config_service = ConfigService()
106
- config_data = config_service.load()
107
- if config_data:
108
- app.config['X_IPE_CONFIG'] = config_data
109
- # Always use config's project_root when .x-ipe.yaml is detected
110
- app.config['PROJECT_ROOT'] = config_data.get_file_tree_path()
111
-
112
- # Apply project_root from settings only if no .x-ipe.yaml detected
113
- if not app.config.get('X_IPE_CONFIG'):
114
- saved_root = settings_service.get('project_root')
115
- if saved_root and saved_root != '.' and not app.config.get('TESTING'):
116
- # Only apply if it's a valid path
117
- if os.path.exists(saved_root) and os.path.isdir(saved_root):
118
- app.config['PROJECT_ROOT'] = saved_root
119
-
120
- # Register routes
121
- register_routes(app)
122
- register_settings_routes(app)
123
- register_project_routes(app)
124
- register_ideas_routes(app) # FEATURE-008
125
- register_tools_config_routes(app) # FEATURE-011
126
-
127
- # Initialize Socket.IO with the app
128
- socketio.init_app(app)
129
-
130
- # Register WebSocket handlers for terminal
131
- register_terminal_handlers()
132
-
133
- # Register WebSocket handlers for voice input (FEATURE-021)
134
- register_voice_handlers()
135
-
136
- return app
137
-
138
-
139
- def register_routes(app):
140
- """Register all application routes"""
141
-
142
- @app.route('/')
143
- def index():
144
- """Serve main page with sidebar navigation"""
145
- return render_template('index.html')
146
-
147
- @app.route('/uiux-feedbacks')
148
- def uiux_feedbacks():
149
- """
150
- GET /uiux-feedbacks
151
-
152
- FEATURE-022: UIUX Feedbacks placeholder page (CR-004)
153
- Shows Work in Progress banner.
154
- """
155
- return render_template('uiux-feedbacks.html')
156
-
157
- @app.route('/workplace')
158
- def workplace():
159
- """
160
- GET /workplace
161
-
162
- FEATURE-008: Workplace/Ideation page
163
- CR-004: Route preserved for backward compatibility, page renamed to Ideation.
164
- """
165
- return render_template('workplace.html')
166
-
167
- @app.route('/api/project/structure')
168
- def get_project_structure():
169
- """
170
- GET /api/project/structure
171
-
172
- Returns the project folder structure for sidebar navigation.
173
- """
174
- project_root = app.config.get('PROJECT_ROOT')
175
-
176
- if not project_root or not os.path.exists(project_root):
177
- return jsonify({
178
- 'error': 'Project root not configured or does not exist',
179
- 'project_root': project_root
180
- }), 400
181
-
182
- service = ProjectService(project_root)
183
- structure = service.get_structure()
184
-
185
- return jsonify(structure)
186
-
187
- @app.route('/api/file/content')
188
- def get_file_content():
189
- """
190
- GET /api/file/content?path=<relative_path>&raw=<true/false>
191
-
192
- Returns the content of a file with metadata for rendering.
193
- If raw=true or file is binary (images, etc.), serves the raw file content.
194
- """
195
- from pathlib import Path
196
-
197
- file_path = request.args.get('path')
198
- raw = request.args.get('raw', 'false').lower() == 'true'
199
-
200
- if not file_path:
201
- return jsonify({'error': 'Path parameter required'}), 400
202
-
203
- project_root = app.config.get('PROJECT_ROOT')
204
-
205
- # Binary file extensions that should always be served raw
206
- binary_extensions = {
207
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp',
208
- '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
209
- '.zip', '.rar', '.tar', '.gz', '.7z',
210
- '.mp3', '.mp4', '.wav', '.avi', '.mov',
211
- '.exe', '.dll', '.so', '.dylib',
212
- }
213
-
214
- try:
215
- # Resolve path
216
- full_path = (Path(project_root) / file_path).resolve()
217
-
218
- # Security check: ensure path is within project root
219
- if not str(full_path).startswith(str(Path(project_root).resolve())):
220
- return jsonify({'error': 'Access denied'}), 403
221
-
222
- if not full_path.exists():
223
- return jsonify({'error': 'File not found'}), 404
224
-
225
- ext = full_path.suffix.lower()
226
-
227
- # Auto-detect binary files or use raw parameter
228
- if raw or ext in binary_extensions:
229
- # Determine MIME type
230
- mime_types = {
231
- '.png': 'image/png',
232
- '.jpg': 'image/jpeg',
233
- '.jpeg': 'image/jpeg',
234
- '.gif': 'image/gif',
235
- '.bmp': 'image/bmp',
236
- '.ico': 'image/x-icon',
237
- '.svg': 'image/svg+xml',
238
- '.webp': 'image/webp',
239
- '.pdf': 'application/pdf',
240
- '.doc': 'application/msword',
241
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
242
- '.xls': 'application/vnd.ms-excel',
243
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
244
- '.ppt': 'application/vnd.ms-powerpoint',
245
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
246
- '.zip': 'application/zip',
247
- '.rar': 'application/vnd.rar',
248
- }
249
- mime_type = mime_types.get(ext, 'application/octet-stream')
250
-
251
- return send_file(full_path, mimetype=mime_type)
252
-
253
- service = ContentService(project_root)
254
- result = service.get_content(file_path)
255
- return jsonify(result)
256
- except FileNotFoundError:
257
- return jsonify({'error': 'File not found'}), 404
258
- except PermissionError:
259
- return jsonify({'error': 'Access denied'}), 403
260
- except Exception as e:
261
- return jsonify({'error': str(e)}), 500
262
-
263
- @app.route('/api/file/save', methods=['POST'])
264
- def save_file():
265
- """
266
- POST /api/file/save
267
-
268
- Save content to a file. Request body: {path: string, content: string}
269
-
270
- FEATURE-003: Content Editor
271
- """
272
- # Check for JSON body
273
- if not request.is_json:
274
- return jsonify({'success': False, 'error': 'JSON body required'}), 400
275
-
276
- data = request.get_json()
277
-
278
- # Validate required fields
279
- if not data:
280
- return jsonify({'success': False, 'error': 'Request body required'}), 400
281
-
282
- if 'path' not in data:
283
- return jsonify({'success': False, 'error': 'Path is required'}), 400
284
-
285
- if 'content' not in data:
286
- return jsonify({'success': False, 'error': 'Content is required'}), 400
287
-
288
- project_root = app.config.get('PROJECT_ROOT')
289
-
290
- if not project_root or not os.path.exists(project_root):
291
- return jsonify({'success': False, 'error': 'Project root not configured'}), 400
292
-
293
- service = ContentService(project_root)
294
- result = service.save_content(data['path'], data['content'])
295
-
296
- if result['success']:
297
- return jsonify(result), 200
298
- else:
299
- return jsonify(result), 400
300
-
301
-
302
- # =============================================================================
303
- # FEATURE-005: Interactive Console v4.0 - WebSocket Handlers
304
- # =============================================================================
305
-
306
- def register_terminal_handlers():
307
- """Register WebSocket event handlers for terminal."""
308
-
309
- @socketio.on('connect')
310
- def handle_connect():
311
- """Handle new WebSocket connection."""
312
- sid = request.sid
313
- print(f"[Terminal] Client connected: {sid}")
314
-
315
- @socketio.on('attach')
316
- def handle_attach(data):
317
- """
318
- Handle session attachment.
319
- Creates new session or reconnects to existing one.
320
- """
321
- try:
322
- sid = request.sid
323
- requested_session_id = data.get('session_id') if data else None
324
- # Ensure rows/cols are valid integers with defaults
325
- rows = data.get('rows') if data else None
326
- cols = data.get('cols') if data else None
327
- rows = int(rows) if rows is not None else 24
328
- cols = int(cols) if cols is not None else 80
329
-
330
- def emit_output(output_data):
331
- socketio.emit('output', output_data, room=sid)
332
-
333
- # Try to reconnect to existing session
334
- if requested_session_id and session_manager.has_session(requested_session_id):
335
- session = session_manager.get_session(requested_session_id)
336
-
337
- if session.is_expired():
338
- session_manager.remove_session(requested_session_id)
339
- else:
340
- # Reconnect to existing session
341
- session.attach(sid, emit_output)
342
- socket_to_session[sid] = requested_session_id
343
-
344
- # Replay buffered output
345
- buffer = session.get_buffer()
346
- if buffer:
347
- socketio.emit('output', buffer, room=sid)
348
-
349
- socketio.emit('reconnected', {'session_id': requested_session_id}, room=sid)
350
- return
351
-
352
- # Create new session
353
- session_id = session_manager.create_session(emit_output, rows, cols)
354
- session = session_manager.get_session(session_id)
355
- session.attach(sid, emit_output)
356
- socket_to_session[sid] = session_id
357
-
358
- socketio.emit('session_id', session_id, room=sid)
359
- socketio.emit('new_session', {'session_id': session_id}, room=sid)
360
- except Exception as e:
361
- print(f"[Terminal] Error in attach handler: {e}")
362
- socketio.emit('error', {'message': 'Failed to attach terminal session'}, room=sid)
363
-
364
- @socketio.on('disconnect')
365
- def handle_disconnect():
366
- """Handle WebSocket disconnection - keep session alive."""
367
- try:
368
- sid = request.sid
369
- session_id = socket_to_session.pop(sid, None)
370
-
371
- if session_id:
372
- session = session_manager.get_session(session_id)
373
- if session:
374
- session.detach() # Keep PTY alive for reconnection
375
-
376
- print(f"[Terminal] Client disconnected: {sid}")
377
- except Exception as e:
378
- print(f"[Terminal] Error in disconnect handler: {e}")
379
-
380
- @socketio.on('input')
381
- def handle_input(data):
382
- """Forward input to PTY."""
383
- try:
384
- sid = request.sid
385
- session_id = socket_to_session.get(sid)
386
-
387
- if session_id:
388
- session = session_manager.get_session(session_id)
389
- if session:
390
- session.write(data)
391
- except Exception as e:
392
- print(f"[Terminal] Error in input handler: {e}")
393
-
394
- @socketio.on('resize')
395
- def handle_resize(data):
396
- """Handle terminal resize."""
397
- try:
398
- sid = request.sid
399
- session_id = socket_to_session.get(sid)
400
-
401
- if session_id:
402
- session = session_manager.get_session(session_id)
403
- if session:
404
- rows = data.get('rows', 24)
405
- cols = data.get('cols', 80)
406
- session.resize(rows, cols)
407
- except Exception as e:
408
- print(f"[Terminal] Error in resize handler: {e}")
409
-
410
-
411
- # =============================================================================
412
- # FEATURE-021: Console Voice Input - Socket.IO Handlers
413
- # =============================================================================
414
-
415
- # Global voice service instance
416
- voice_service = None
417
-
418
- # Mapping: socket_sid -> voice_session_id
419
- socket_to_voice_session = {}
420
-
421
- def register_voice_handlers():
422
- """Register WebSocket event handlers for voice input."""
423
- from x_ipe.services.voice_input_service_v2 import VoiceInputService, is_voice_command
424
-
425
- global voice_service
426
-
427
- # Initialize voice service with API key from environment
428
- api_key = os.environ.get('ALIBABA_SPEECH_API_KEY', '')
429
- if api_key:
430
- voice_service = VoiceInputService(api_key=api_key)
431
- print("[Voice] Voice service initialized with API key")
432
- else:
433
- print("[Voice] No ALIBABA_SPEECH_API_KEY found, voice input disabled")
434
-
435
- @socketio.on('voice_start')
436
- def handle_voice_start(data=None):
437
- """Handle voice recording start request."""
438
- global voice_service
439
- sid = request.sid
440
-
441
- print(f"[Voice] 🎬 voice_start received from {sid}")
442
- print(f"[Voice] data: {data}")
443
-
444
- if not voice_service:
445
- print(f"[Voice] ❌ Voice service not configured!")
446
- emit('voice_error', {'message': 'Voice service not configured. Set ALIBABA_SPEECH_API_KEY in x-ipe-docs/config/.env'})
447
- return
448
-
449
- try:
450
- # Create voice session with callbacks for partial results
451
- def on_partial(text):
452
- print(f"[Voice] 📤 Emitting voice_partial: '{text}'")
453
- socketio.emit('voice_partial', {'text': text}, room=sid)
454
-
455
- session_id = voice_service.create_session(
456
- socket_sid=sid,
457
- on_partial=on_partial,
458
- )
459
- socket_to_voice_session[sid] = session_id
460
-
461
- # Start recognition
462
- print(f"[Voice] Starting recognition...")
463
- if voice_service.start_recognition(session_id):
464
- print(f"[Voice] ✅ Emitting voice_ready for session {session_id}")
465
- emit('voice_ready', {'session_id': session_id})
466
- else:
467
- print(f"[Voice] ❌ Failed to start recognition")
468
- emit('voice_error', {'message': 'Failed to start recognition'})
469
- voice_service.remove_session(session_id)
470
- del socket_to_voice_session[sid]
471
- except Exception as e:
472
- print(f"[Voice] ❌ Exception in voice_start: {e}")
473
- import traceback
474
- traceback.print_exc()
475
- emit('voice_error', {'message': str(e)})
476
-
477
- @socketio.on('voice_audio')
478
- def handle_voice_audio(data):
479
- """Handle incoming audio chunk from client."""
480
- global voice_service
481
- sid = request.sid
482
-
483
- if not voice_service:
484
- return
485
-
486
- session_id = socket_to_voice_session.get(sid)
487
- if not session_id:
488
- print(f"[Voice] ⚠️ voice_audio received but no session for {sid}")
489
- return
490
-
491
- try:
492
- # Get audio data from message
493
- audio_chunk = data.get('audio', b'') if isinstance(data, dict) else data
494
- if isinstance(audio_chunk, list):
495
- audio_chunk = bytes(audio_chunk)
496
-
497
- # Forward to voice service (sync now with dashscope SDK)
498
- voice_service.send_audio(session_id, audio_chunk)
499
- except Exception as e:
500
- print(f"[Voice] ❌ Exception in voice_audio: {e}")
501
- emit('voice_error', {'message': f'Audio error: {e}'})
502
-
503
- @socketio.on('voice_stop')
504
- def handle_voice_stop(data=None):
505
- """Handle voice recording stop request - finalize and get transcription."""
506
- global voice_service
507
- sid = request.sid
508
-
509
- print(f"[Voice] 🛑 voice_stop received from {sid}")
510
-
511
- if not voice_service:
512
- print(f"[Voice] ❌ Voice service not available")
513
- emit('voice_error', {'message': 'Voice service not available'})
514
- return
515
-
516
- session_id = socket_to_voice_session.get(sid)
517
- if not session_id:
518
- print(f"[Voice] ❌ No active voice session for {sid}")
519
- emit('voice_error', {'message': 'No active voice session'})
520
- return
521
-
522
- try:
523
- print(f"[Voice] Stopping recognition for session {session_id}...")
524
- # Stop recognition and get result (sync with dashscope SDK)
525
- result = voice_service.stop_recognition(session_id)
526
-
527
- print(f"[Voice] Result: '{result}'")
528
-
529
- # Check if result is a voice command
530
- command = is_voice_command(result)
531
-
532
- if command:
533
- print(f"[Voice] 📤 Emitting voice_command: {command}")
534
- emit('voice_command', {'command': command, 'text': result})
535
- else:
536
- print(f"[Voice] 📤 Emitting voice_result: '{result}'")
537
- emit('voice_result', {'text': result})
538
-
539
- # Cleanup session
540
- voice_service.remove_session(session_id)
541
- if sid in socket_to_voice_session:
542
- del socket_to_voice_session[sid]
543
-
544
- print(f"[Voice] ✅ Session completed: {session_id}")
545
- except Exception as e:
546
- print(f"[Voice] ❌ Exception in voice_stop: {e}")
547
- import traceback
548
- traceback.print_exc()
549
- emit('voice_error', {'message': str(e)})
550
-
551
- @socketio.on('voice_cancel')
552
- def handle_voice_cancel(data=None):
553
- """Handle voice recording cancel request - abort without transcription."""
554
- global voice_service
555
- sid = request.sid
556
-
557
- print(f"[Voice] ⚠️ voice_cancel received from {sid}")
558
-
559
- session_id = socket_to_voice_session.get(sid)
560
- if not session_id:
561
- print(f"[Voice] No session to cancel")
562
- return
563
-
564
- try:
565
- # Cancel recognition (sync with dashscope SDK)
566
- if voice_service:
567
- voice_service.cancel_recognition(session_id)
568
- voice_service.remove_session(session_id)
569
-
570
- if sid in socket_to_voice_session:
571
- del socket_to_voice_session[sid]
572
-
573
- emit('voice_cancelled', {})
574
- print(f"[Voice] ✅ Session cancelled: {session_id}")
575
- except Exception as e:
576
- print(f"[Voice] ❌ Exception in voice_cancel: {e}")
577
- emit('voice_error', {'message': f'Cancel error: {e}'})
578
-
579
-
580
- # =============================================================================
581
- # FEATURE-006: Settings & Configuration - Routes
582
- # =============================================================================
583
-
584
- def register_settings_routes(app):
585
- """Register settings API and page routes."""
586
-
587
- @app.route('/settings')
588
- def settings_page():
589
- """
590
- GET /settings
591
-
592
- Render the settings page.
593
- """
594
- global settings_service
595
- current_settings = settings_service.get_all() if settings_service else {'project_root': '.'}
596
- return render_template('settings.html', settings=current_settings)
597
-
598
- @app.route('/api/settings', methods=['GET'])
599
- def get_settings():
600
- """
601
- GET /api/settings
602
-
603
- Get all current settings.
604
-
605
- Response:
606
- - project_root: string - Current project root path
607
- """
608
- global settings_service
609
- if not settings_service:
610
- return jsonify({'project_root': app.config.get('PROJECT_ROOT', '.')}), 200
611
-
612
- return jsonify(settings_service.get_all())
613
-
614
- @app.route('/api/settings', methods=['POST'])
615
- def save_settings():
616
- """
617
- POST /api/settings
618
-
619
- Save settings.
620
-
621
- Request body:
622
- - project_root: string (optional) - New project root path
623
-
624
- Response (success):
625
- - success: true
626
- - message: string
627
-
628
- Response (error):
629
- - success: false
630
- - errors: object with field-specific error messages
631
- """
632
- global settings_service, file_watcher
633
-
634
- data = request.get_json() or {}
635
- errors = {}
636
-
637
- # Validate project_root if provided
638
- if 'project_root' in data:
639
- path = data['project_root']
640
- path_errors = settings_service.validate_project_root(path)
641
- errors.update(path_errors)
642
-
643
- # Return errors if any
644
- if errors:
645
- return jsonify({'success': False, 'errors': errors}), 400
646
-
647
- # Save settings
648
- for key, value in data.items():
649
- if key in ['project_root']: # Allowed settings
650
- settings_service.set(key, value)
651
-
652
- # Apply project_root change
653
- if 'project_root' in data:
654
- new_path = data['project_root']
655
- app.config['PROJECT_ROOT'] = new_path
656
-
657
- return jsonify({'success': True, 'message': 'Settings saved successfully'})
658
-
659
- @app.route('/api/config', methods=['GET'])
660
- def get_config():
661
- """
662
- GET /api/config
663
-
664
- Get current project configuration from .x-ipe.yaml.
665
-
666
- FEATURE-010: Project Root Configuration
667
-
668
- Response (config detected):
669
- - detected: true
670
- - config_file: string - Path to .x-ipe.yaml
671
- - version: int
672
- - project_root: string
673
- - x_ipe_app: string
674
- - file_tree_scope: string
675
- - terminal_cwd: string
676
-
677
- Response (no config):
678
- - detected: false
679
- - config_file: null
680
- - using_defaults: true
681
- - project_root: string - Current project root
682
- - message: string
683
- """
684
- config_data = app.config.get('X_IPE_CONFIG')
685
-
686
- if config_data:
687
- return jsonify({
688
- 'detected': True,
689
- **config_data.to_dict()
690
- })
691
- else:
692
- return jsonify({
693
- 'detected': False,
694
- 'config_file': None,
695
- 'using_defaults': True,
696
- 'project_root': app.config.get('PROJECT_ROOT', os.getcwd()),
697
- 'message': 'No .x-ipe.yaml found. Using default paths.'
698
- })
699
-
700
-
701
- def register_project_routes(app):
702
- """
703
- Register project folder management routes.
704
-
705
- FEATURE-006 v2.0: Multi-Project Folder Support
706
- """
707
-
708
- @app.route('/api/projects', methods=['GET'])
709
- def get_projects():
710
- """
711
- GET /api/projects
712
-
713
- Get all project folders and active project ID.
714
-
715
- Response:
716
- - projects: array of {id, name, path}
717
- - active_project_id: number
718
- """
719
- global project_folders_service
720
- if not project_folders_service:
721
- return jsonify({'projects': [], 'active_project_id': 1}), 200
722
-
723
- return jsonify({
724
- 'projects': project_folders_service.get_all(),
725
- 'active_project_id': project_folders_service.get_active_id()
726
- })
727
-
728
- @app.route('/api/projects', methods=['POST'])
729
- def add_project():
730
- """
731
- POST /api/projects
732
-
733
- Add a new project folder.
734
-
735
- Request body:
736
- - name: string - Project name
737
- - path: string - Project path
738
-
739
- Response (success):
740
- - success: true
741
- - project: {id, name, path}
742
-
743
- Response (error):
744
- - success: false
745
- - errors: object with field-specific error messages
746
- """
747
- global project_folders_service
748
-
749
- if not request.is_json:
750
- return jsonify({'success': False, 'error': 'JSON required'}), 400
751
-
752
- data = request.get_json()
753
- name = data.get('name', '').strip()
754
- path = data.get('path', '').strip()
755
-
756
- result = project_folders_service.add(name, path)
757
-
758
- if result['success']:
759
- return jsonify(result), 201
760
- return jsonify(result), 400
761
-
762
- @app.route('/api/projects/<int:project_id>', methods=['PUT'])
763
- def update_project(project_id):
764
- """
765
- PUT /api/projects/<id>
766
-
767
- Update an existing project folder.
768
-
769
- Request body:
770
- - name: string (optional) - New project name
771
- - path: string (optional) - New project path
772
-
773
- Response (success):
774
- - success: true
775
- - project: {id, name, path}
776
-
777
- Response (error):
778
- - success: false
779
- - errors: object with field-specific error messages
780
- """
781
- global project_folders_service
782
-
783
- if not request.is_json:
784
- return jsonify({'success': False, 'error': 'JSON required'}), 400
785
-
786
- data = request.get_json()
787
- name = data.get('name')
788
- path = data.get('path')
789
-
790
- result = project_folders_service.update(project_id, name=name, path=path)
791
-
792
- if result['success']:
793
- return jsonify(result)
794
- return jsonify(result), 400
795
-
796
- @app.route('/api/projects/<int:project_id>', methods=['DELETE'])
797
- def delete_project(project_id):
798
- """
799
- DELETE /api/projects/<id>
800
-
801
- Delete a project folder.
802
-
803
- Response (success):
804
- - success: true
805
-
806
- Response (error):
807
- - success: false
808
- - error: string error message
809
- """
810
- global project_folders_service
811
-
812
- active_id = project_folders_service.get_active_id()
813
- result = project_folders_service.delete(project_id, active_project_id=active_id)
814
-
815
- if result['success']:
816
- return jsonify(result)
817
- return jsonify(result), 400
818
-
819
- @app.route('/api/projects/switch', methods=['POST'])
820
- def switch_project():
821
- """
822
- POST /api/projects/switch
823
-
824
- Switch the active project.
825
-
826
- Request body:
827
- - project_id: number - Project ID to switch to
828
-
829
- Response (success):
830
- - success: true
831
- - active_project_id: number
832
- - project: {id, name, path}
833
-
834
- Response (error):
835
- - success: false
836
- - error: string error message
837
- """
838
- global project_folders_service
839
-
840
- if not request.is_json:
841
- return jsonify({'success': False, 'error': 'JSON required'}), 400
842
-
843
- data = request.get_json()
844
- project_id = data.get('project_id')
845
-
846
- if not project_id:
847
- return jsonify({'success': False, 'error': 'project_id required'}), 400
848
-
849
- result = project_folders_service.set_active(project_id)
850
-
851
- if result['success']:
852
- # Update app config with new project root
853
- project = result['project']
854
- project_path = project['path']
855
-
856
- # If config is detected, project folders are relative to config's project_root
857
- config_data = app.config.get('X_IPE_CONFIG')
858
- if config_data:
859
- # Config always takes precedence - use its project_root
860
- app.config['PROJECT_ROOT'] = config_data.project_root
861
- elif project_path == '.':
862
- # No config, default project folder - use cwd where x-ipe was run
863
- app.config['PROJECT_ROOT'] = os.environ.get('X_IPE_PROJECT_ROOT', os.getcwd())
864
- else:
865
- # Absolute path from project folder
866
- app.config['PROJECT_ROOT'] = project_path
867
-
868
- return jsonify(result)
869
- return jsonify(result), 400
870
-
871
-
872
- def register_ideas_routes(app):
873
- """
874
- Register idea management routes.
875
-
876
- FEATURE-008: Workplace (Idea Management)
877
- """
878
-
879
- @app.route('/api/ideas/tree', methods=['GET'])
880
- def get_ideas_tree():
881
- """
882
- GET /api/ideas/tree
883
-
884
- Get tree structure of x-ipe-docs/ideas/ directory.
885
-
886
- Response:
887
- - success: true
888
- - tree: array of folder/file objects
889
- """
890
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
891
- service = IdeasService(project_root)
892
-
893
- try:
894
- tree = service.get_tree()
895
- return jsonify({
896
- 'success': True,
897
- 'tree': tree
898
- })
899
- except Exception as e:
900
- return jsonify({
901
- 'success': False,
902
- 'error': str(e)
903
- }), 500
904
-
905
- @app.route('/api/ideas/upload', methods=['POST'])
906
- def upload_ideas():
907
- """
908
- POST /api/ideas/upload
909
-
910
- Upload files to a new or existing idea folder.
911
-
912
- Request: multipart/form-data with 'files' field
913
- Optional: 'target_folder' field to upload to existing folder (CR-002)
914
-
915
- Response:
916
- - success: true/false
917
- - folder_name: string
918
- - folder_path: string
919
- - files_uploaded: array of filenames
920
- """
921
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
922
- service = IdeasService(project_root)
923
-
924
- if 'files' not in request.files:
925
- return jsonify({
926
- 'success': False,
927
- 'error': 'No files provided'
928
- }), 400
929
-
930
- uploaded_files = request.files.getlist('files')
931
- if not uploaded_files or all(f.filename == '' for f in uploaded_files):
932
- return jsonify({
933
- 'success': False,
934
- 'error': 'No files provided'
935
- }), 400
936
-
937
- # CR-002: Get optional target_folder from form data
938
- target_folder = request.form.get('target_folder', None)
939
-
940
- # Convert to (filename, content) tuples
941
- files = [(f.filename, f.read()) for f in uploaded_files if f.filename]
942
-
943
- result = service.upload(files, target_folder=target_folder)
944
-
945
- if result['success']:
946
- return jsonify(result)
947
- return jsonify(result), 400
948
-
949
- @app.route('/api/ideas/rename', methods=['POST'])
950
- def rename_idea_folder():
951
- """
952
- POST /api/ideas/rename
953
-
954
- Rename an idea folder.
955
-
956
- Request body:
957
- - old_name: string - Current folder name
958
- - new_name: string - New folder name
959
-
960
- Response:
961
- - success: true/false
962
- - old_name: string
963
- - new_name: string
964
- - new_path: string
965
- """
966
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
967
- service = IdeasService(project_root)
968
-
969
- if not request.is_json:
970
- return jsonify({
971
- 'success': False,
972
- 'error': 'JSON required'
973
- }), 400
974
-
975
- data = request.get_json()
976
- old_name = data.get('old_name')
977
- new_name = data.get('new_name')
978
-
979
- if not old_name or not new_name:
980
- return jsonify({
981
- 'success': False,
982
- 'error': 'old_name and new_name are required'
983
- }), 400
984
-
985
- result = service.rename_folder(old_name, new_name)
986
-
987
- if result['success']:
988
- return jsonify(result)
989
- return jsonify(result), 400
990
-
991
- @app.route('/api/ideas/rename-file', methods=['POST'])
992
- def rename_idea_file():
993
- """
994
- POST /api/ideas/rename-file
995
-
996
- Rename a file within x-ipe-docs/ideas/.
997
-
998
- Request body:
999
- - path: string - Current file path (relative to project root)
1000
- - new_name: string - New file name (with extension)
1001
-
1002
- Response:
1003
- - success: true/false
1004
- - old_path: string
1005
- - new_path: string
1006
- - new_name: string
1007
- """
1008
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1009
- service = IdeasService(project_root)
1010
-
1011
- if not request.is_json:
1012
- return jsonify({
1013
- 'success': False,
1014
- 'error': 'JSON required'
1015
- }), 400
1016
-
1017
- data = request.get_json()
1018
- path = data.get('path')
1019
- new_name = data.get('new_name')
1020
-
1021
- if not path or not new_name:
1022
- return jsonify({
1023
- 'success': False,
1024
- 'error': 'path and new_name are required'
1025
- }), 400
1026
-
1027
- result = service.rename_file(path, new_name)
1028
-
1029
- if result['success']:
1030
- return jsonify(result)
1031
- return jsonify(result), 400
1032
-
1033
- @app.route('/api/ideas/delete', methods=['POST'])
1034
- def delete_idea_item():
1035
- """
1036
- POST /api/ideas/delete
1037
-
1038
- Delete an idea file or folder.
1039
-
1040
- Request body:
1041
- - path: string - Relative path to file/folder within x-ipe-docs/ideas/
1042
-
1043
- Response:
1044
- - success: true/false
1045
- - path: string
1046
- - type: 'file' | 'folder'
1047
- """
1048
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1049
- service = IdeasService(project_root)
1050
-
1051
- if not request.is_json:
1052
- return jsonify({
1053
- 'success': False,
1054
- 'error': 'JSON required'
1055
- }), 400
1056
-
1057
- data = request.get_json()
1058
- path = data.get('path')
1059
-
1060
- if not path:
1061
- return jsonify({
1062
- 'success': False,
1063
- 'error': 'path is required'
1064
- }), 400
1065
-
1066
- result = service.delete_item(path)
1067
-
1068
- if result['success']:
1069
- return jsonify(result)
1070
- return jsonify(result), 400
1071
-
1072
- @app.route('/api/ideas/toolbox', methods=['GET'])
1073
- def get_ideas_toolbox():
1074
- """
1075
- GET /api/ideas/toolbox
1076
-
1077
- Get ideation toolbox configuration.
1078
-
1079
- Response:
1080
- - version: string
1081
- - ideation: {antv-infographic: bool, mermaid: bool}
1082
- - mockup: {frontend-design: bool}
1083
- - sharing: {}
1084
- """
1085
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1086
- service = IdeasService(project_root)
1087
-
1088
- config = service.get_toolbox()
1089
- return jsonify(config)
1090
-
1091
- @app.route('/api/ideas/toolbox', methods=['POST'])
1092
- def save_ideas_toolbox():
1093
- """
1094
- POST /api/ideas/toolbox
1095
-
1096
- Save ideation toolbox configuration.
1097
-
1098
- Request body:
1099
- - version: string
1100
- - ideation: {antv-infographic: bool, mermaid: bool}
1101
- - mockup: {frontend-design: bool}
1102
- - sharing: {}
1103
-
1104
- Response:
1105
- - success: true/false
1106
- """
1107
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1108
- service = IdeasService(project_root)
1109
-
1110
- config = request.get_json()
1111
- result = service.save_toolbox(config)
1112
-
1113
- if result['success']:
1114
- return jsonify(result)
1115
- return jsonify(result), 400
1116
-
1117
- """
1118
- ==========================================================================
1119
- SKILLS API
1120
-
1121
- Read-only API for skills defined in .github/skills/
1122
- ==========================================================================
1123
- """
1124
-
1125
- @app.route('/api/skills', methods=['GET'])
1126
- def get_skills():
1127
- """
1128
- GET /api/skills
1129
-
1130
- Get list of all skills with name and description.
1131
-
1132
- Response:
1133
- - success: true
1134
- - skills: array of {name, description}
1135
- """
1136
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1137
- service = SkillsService(project_root)
1138
-
1139
- try:
1140
- skills = service.get_all()
1141
- return jsonify({
1142
- 'success': True,
1143
- 'skills': skills
1144
- })
1145
- except Exception as e:
1146
- return jsonify({
1147
- 'success': False,
1148
- 'error': str(e)
1149
- }), 500
1150
-
1151
-
1152
- # Entry point for running with `python -m src.app`
1153
- def register_tools_config_routes(app):
1154
- """
1155
- Register tools configuration routes.
1156
-
1157
- FEATURE-011: Stage Toolbox
1158
- """
1159
-
1160
- def _get_tools_service():
1161
- """Get ToolsConfigService instance for current project root."""
1162
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1163
- return ToolsConfigService(project_root)
1164
-
1165
- @app.route('/api/config/tools', methods=['GET'])
1166
- def get_tools_config():
1167
- """
1168
- GET /api/config/tools
1169
-
1170
- Get current tools configuration.
1171
-
1172
- Response:
1173
- - success: true
1174
- - config: tools configuration object
1175
- """
1176
- try:
1177
- service = _get_tools_service()
1178
- config = service.load()
1179
- return jsonify({
1180
- 'success': True,
1181
- 'config': config
1182
- })
1183
- except Exception as e:
1184
- return jsonify({
1185
- 'success': False,
1186
- 'error': str(e)
1187
- }), 500
1188
-
1189
- @app.route('/api/config/tools', methods=['POST'])
1190
- def save_tools_config():
1191
- """
1192
- POST /api/config/tools
1193
-
1194
- Save tools configuration.
1195
-
1196
- Request Body: JSON with 'stages' key
1197
-
1198
- Response:
1199
- - success: true/false
1200
- - error: string (on failure)
1201
- """
1202
- try:
1203
- config = request.get_json(force=True, silent=True)
1204
- if config is None:
1205
- return jsonify({
1206
- 'success': False,
1207
- 'error': 'Invalid JSON or empty body'
1208
- }), 400
1209
-
1210
- if 'stages' not in config:
1211
- return jsonify({
1212
- 'success': False,
1213
- 'error': 'Invalid config format: missing stages key'
1214
- }), 400
1215
-
1216
- service = _get_tools_service()
1217
- service.save(config)
1218
- return jsonify({'success': True})
1219
- except Exception as e:
1220
- return jsonify({
1221
- 'success': False,
1222
- 'error': str(e)
1223
- }), 500
1224
-
1225
- @app.route('/api/config/copilot-prompt', methods=['GET'])
1226
- def get_copilot_prompt_config():
1227
- """
1228
- GET /api/config/copilot-prompt
1229
-
1230
- Get Copilot prompt configuration for the Copilot button dropdown.
1231
-
1232
- Response:
1233
- - prompts: List of prompt objects with id, label, icon, command
1234
- - placeholder: Dictionary of placeholder descriptions
1235
- """
1236
- try:
1237
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1238
- config_path = os.path.join(project_root, 'x-ipe-docs', 'config', 'copilot-prompt.json')
1239
-
1240
- if os.path.exists(config_path):
1241
- with open(config_path, 'r', encoding='utf-8') as f:
1242
- config = json.load(f)
1243
- return jsonify(config)
1244
- else:
1245
- # Return empty prompts if config doesn't exist
1246
- return jsonify({
1247
- 'version': '1.0',
1248
- 'prompts': [],
1249
- 'placeholder': {}
1250
- })
1251
- except Exception as e:
1252
- return jsonify({
1253
- 'prompts': [],
1254
- 'error': str(e)
1255
- }), 500
1256
-
1257
- # ============================================================
1258
- # FEATURE-012: Design Themes API
1259
- # ============================================================
1260
-
1261
- def _get_themes_service():
1262
- """Get ThemesService instance for current project root."""
1263
- project_root = app.config.get('PROJECT_ROOT', os.getcwd())
1264
- return ThemesService(project_root)
1265
-
1266
- @app.route('/api/themes', methods=['GET'])
1267
- def list_themes():
1268
- """
1269
- GET /api/themes
1270
-
1271
- List all themes with metadata.
1272
-
1273
- Response:
1274
- - themes: List of theme objects with name, description, colors, files, path
1275
- - selected: Currently selected theme name from config (null if none)
1276
- """
1277
- try:
1278
- themes_service = _get_themes_service()
1279
- themes = themes_service.list_themes()
1280
-
1281
- # Get selected theme from tools config (new format)
1282
- tools_service = _get_tools_service()
1283
- config = tools_service.load()
1284
- selected_theme_config = config.get('selected-theme', {})
1285
- selected = selected_theme_config.get('theme-name') if selected_theme_config else None
1286
-
1287
- return jsonify({
1288
- 'themes': themes,
1289
- 'selected': selected
1290
- })
1291
- except Exception as e:
1292
- return jsonify({
1293
- 'themes': [],
1294
- 'selected': 'theme-default',
1295
- 'error': str(e)
1296
- }), 500
1297
-
1298
- @app.route('/api/themes/<name>', methods=['GET'])
1299
- def get_theme_detail(name):
1300
- """
1301
- GET /api/themes/<name>
1302
-
1303
- Get detailed information about a specific theme.
1304
-
1305
- Args:
1306
- name: Theme folder name (e.g., "theme-default")
1307
-
1308
- Response:
1309
- - Theme detail object with design_system content
1310
- - 404 if theme not found
1311
- """
1312
- try:
1313
- themes_service = _get_themes_service()
1314
- theme = themes_service.get_theme(name)
1315
-
1316
- if theme is None:
1317
- return jsonify({
1318
- 'error': f'Theme not found: {name}'
1319
- }), 404
1320
-
1321
- return jsonify(theme)
1322
- except Exception as e:
1323
- return jsonify({
1324
- 'error': str(e)
1325
- }), 500
1326
-
1327
-
1328
- if __name__ == '__main__':
1329
- app = create_app()
1330
- # Start session cleanup task
1331
- session_manager.start_cleanup_task()
1332
- # Use socketio.run for WebSocket support
1333
- socketio.run(app, debug=True, host='0.0.0.0', port=5858)