monoco-toolkit 0.2.8__py3-none-any.whl → 0.3.0__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 (63) hide show
  1. monoco/cli/project.py +35 -31
  2. monoco/cli/workspace.py +26 -16
  3. monoco/core/agent/__init__.py +0 -2
  4. monoco/core/agent/action.py +44 -20
  5. monoco/core/agent/adapters.py +20 -16
  6. monoco/core/agent/protocol.py +5 -4
  7. monoco/core/agent/state.py +21 -21
  8. monoco/core/config.py +90 -33
  9. monoco/core/execution.py +21 -16
  10. monoco/core/feature.py +8 -5
  11. monoco/core/git.py +61 -30
  12. monoco/core/hooks.py +57 -0
  13. monoco/core/injection.py +47 -44
  14. monoco/core/integrations.py +50 -35
  15. monoco/core/lsp.py +12 -1
  16. monoco/core/output.py +35 -16
  17. monoco/core/registry.py +3 -2
  18. monoco/core/setup.py +190 -124
  19. monoco/core/skills.py +121 -107
  20. monoco/core/state.py +12 -10
  21. monoco/core/sync.py +85 -56
  22. monoco/core/telemetry.py +10 -6
  23. monoco/core/workspace.py +26 -19
  24. monoco/daemon/app.py +123 -79
  25. monoco/daemon/commands.py +14 -13
  26. monoco/daemon/models.py +11 -3
  27. monoco/daemon/reproduce_stats.py +8 -8
  28. monoco/daemon/services.py +32 -33
  29. monoco/daemon/stats.py +59 -40
  30. monoco/features/config/commands.py +38 -25
  31. monoco/features/i18n/adapter.py +4 -5
  32. monoco/features/i18n/commands.py +83 -49
  33. monoco/features/i18n/core.py +94 -54
  34. monoco/features/issue/adapter.py +6 -7
  35. monoco/features/issue/commands.py +468 -272
  36. monoco/features/issue/core.py +419 -312
  37. monoco/features/issue/domain/lifecycle.py +33 -23
  38. monoco/features/issue/domain/models.py +71 -38
  39. monoco/features/issue/domain/parser.py +92 -69
  40. monoco/features/issue/domain/workspace.py +19 -16
  41. monoco/features/issue/engine/__init__.py +3 -3
  42. monoco/features/issue/engine/config.py +18 -25
  43. monoco/features/issue/engine/machine.py +72 -39
  44. monoco/features/issue/engine/models.py +4 -2
  45. monoco/features/issue/linter.py +287 -157
  46. monoco/features/issue/lsp/definition.py +26 -19
  47. monoco/features/issue/migration.py +45 -34
  48. monoco/features/issue/models.py +29 -13
  49. monoco/features/issue/monitor.py +24 -8
  50. monoco/features/issue/resources/en/SKILL.md +6 -2
  51. monoco/features/issue/validator.py +383 -208
  52. monoco/features/skills/__init__.py +0 -1
  53. monoco/features/skills/core.py +24 -18
  54. monoco/features/spike/adapter.py +4 -5
  55. monoco/features/spike/commands.py +51 -38
  56. monoco/features/spike/core.py +24 -16
  57. monoco/main.py +34 -21
  58. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
  60. monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
  61. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
  62. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
  63. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/daemon/app.py CHANGED
@@ -16,7 +16,11 @@ logger = logging.getLogger("monoco.daemon")
16
16
  from pathlib import Path
17
17
  from monoco.core.config import get_config
18
18
  from monoco.features.issue.core import list_issues
19
- from monoco.core.execution import scan_execution_profiles, get_profile_detail, ExecutionProfile
19
+ from monoco.core.execution import (
20
+ scan_execution_profiles,
21
+ get_profile_detail,
22
+ ExecutionProfile,
23
+ )
20
24
 
21
25
  description = """
22
26
  Monoco Daemon Process
@@ -31,45 +35,46 @@ git_monitor: GitMonitor | None = None
31
35
  config_monitors: List[ConfigMonitor] = []
32
36
  project_manager: ProjectManager | None = None
33
37
 
38
+
34
39
  @asynccontextmanager
35
40
  async def lifespan(app: FastAPI):
36
41
  # Startup
37
42
  logger.info("Starting Monoco Daemon services...")
38
-
43
+
39
44
  global project_manager, git_monitor, config_monitors
40
45
  # Use MONOCO_SERVER_ROOT if set, otherwise CWD
41
46
  env_root = os.getenv("MONOCO_SERVER_ROOT")
42
47
  workspace_root = Path(env_root) if env_root else Path.cwd()
43
48
  logger.info(f"Workspace Root: {workspace_root}")
44
49
  project_manager = ProjectManager(workspace_root, broadcaster)
45
-
50
+
46
51
  async def on_git_change(new_hash: str):
47
- await broadcaster.broadcast("HEAD_UPDATED", {
48
- "ref": "HEAD",
49
- "hash": new_hash
50
- })
51
-
52
+ await broadcaster.broadcast("HEAD_UPDATED", {"ref": "HEAD", "hash": new_hash})
53
+
52
54
  async def on_config_change(path: str):
53
55
  logger.info(f"Config file changed: {path}, broadcasting update...")
54
- await broadcaster.broadcast("CONFIG_UPDATED", {
55
- "scope": "workspace",
56
- "path": path
57
- })
56
+ await broadcaster.broadcast(
57
+ "CONFIG_UPDATED", {"scope": "workspace", "path": path}
58
+ )
58
59
 
59
60
  git_monitor = GitMonitor(workspace_root, on_git_change)
60
-
61
+
61
62
  project_config_path = get_config_path(ConfigScope.PROJECT, str(workspace_root))
62
63
  workspace_config_path = get_config_path(ConfigScope.WORKSPACE, str(workspace_root))
63
-
64
+
64
65
  config_monitors = [
65
- ConfigMonitor(project_config_path, lambda: on_config_change(str(project_config_path))),
66
- ConfigMonitor(workspace_config_path, lambda: on_config_change(str(workspace_config_path)))
66
+ ConfigMonitor(
67
+ project_config_path, lambda: on_config_change(str(project_config_path))
68
+ ),
69
+ ConfigMonitor(
70
+ workspace_config_path, lambda: on_config_change(str(workspace_config_path))
71
+ ),
67
72
  ]
68
-
73
+
69
74
  await project_manager.start_all()
70
75
  git_task = asyncio.create_task(git_monitor.start())
71
76
  config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
72
-
77
+
73
78
  yield
74
79
  # Shutdown
75
80
  logger.info("Shutting down Monoco Daemon services...")
@@ -79,10 +84,11 @@ async def lifespan(app: FastAPI):
79
84
  m.stop()
80
85
  if project_manager:
81
86
  project_manager.stop_all()
82
-
87
+
83
88
  await git_task
84
89
  await asyncio.gather(*config_tasks)
85
-
90
+
91
+
86
92
  app = FastAPI(
87
93
  title="Monoco Daemon",
88
94
  description=description,
@@ -92,17 +98,18 @@ app = FastAPI(
92
98
  lifespan=lifespan,
93
99
  )
94
100
 
101
+
95
102
  def get_project_or_404(project_id: Optional[str] = None):
96
103
  if not project_manager:
97
104
  raise HTTPException(status_code=503, detail="Daemon not fully initialized")
98
-
105
+
99
106
  # If project_id is not provided, try to use the first available project (default behavior)
100
107
  if not project_id:
101
108
  projects = list(project_manager.projects.values())
102
109
  if not projects:
103
- # Fallback to legacy single-project mode if no sub-projects found?
104
- # Or maybe ProjectManager scan logic already covers CWD as a project.
105
- raise HTTPException(status_code=404, detail="No projects found")
110
+ # Fallback to legacy single-project mode if no sub-projects found?
111
+ # Or maybe ProjectManager scan logic already covers CWD as a project.
112
+ raise HTTPException(status_code=404, detail="No projects found")
106
113
  return projects[0]
107
114
 
108
115
  project = project_manager.get_project(project_id)
@@ -110,6 +117,7 @@ def get_project_or_404(project_id: Optional[str] = None):
110
117
  raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
111
118
  return project
112
119
 
120
+
113
121
  # CORS Configuration
114
122
  # Kanban may run on different ports (e.g. localhost:3000, tauri://localhost)
115
123
  app.add_middleware(
@@ -120,6 +128,7 @@ app.add_middleware(
120
128
  allow_headers=["*"],
121
129
  )
122
130
 
131
+
123
132
  @app.get("/health")
124
133
  async def health_check():
125
134
  """
@@ -127,6 +136,7 @@ async def health_check():
127
136
  """
128
137
  return {"status": "ok", "component": "monoco-daemon"}
129
138
 
139
+
130
140
  @app.get("/api/v1/projects")
131
141
  async def list_projects():
132
142
  """
@@ -136,6 +146,7 @@ async def list_projects():
136
146
  return []
137
147
  return project_manager.list_projects()
138
148
 
149
+
139
150
  @app.get("/api/v1/info")
140
151
  async def get_project_info(project_id: Optional[str] = None):
141
152
  """
@@ -148,9 +159,10 @@ async def get_project_info(project_id: Optional[str] = None):
148
159
  "id": project.id,
149
160
  "version": "0.1.0",
150
161
  "mode": "daemon",
151
- "head": current_hash
162
+ "head": current_hash,
152
163
  }
153
164
 
165
+
154
166
  @app.get("/api/v1/config/dictionary")
155
167
  async def get_ui_dictionary(project_id: Optional[str] = None) -> Dict[str, str]:
156
168
  """
@@ -161,29 +173,27 @@ async def get_ui_dictionary(project_id: Optional[str] = None) -> Dict[str, str]:
161
173
  config = get_config(str(project.path))
162
174
  return config.ui.dictionary
163
175
 
176
+
164
177
  @app.get("/api/v1/events")
165
178
  async def sse_endpoint(request: Request):
166
179
  """
167
180
  Server-Sent Events endpoint for real-time updates.
168
181
  """
169
182
  queue = await broadcaster.subscribe()
170
-
183
+
171
184
  async def event_generator():
172
185
  try:
173
186
  # Quick ping to confirm connection
174
- yield {
175
- "event": "connect",
176
- "data": "connected"
177
- }
178
-
187
+ yield {"event": "connect", "data": "connected"}
188
+
179
189
  while True:
180
190
  if await request.is_disconnected():
181
191
  break
182
-
192
+
183
193
  # Wait for new messages
184
194
  message = await queue.get()
185
195
  yield message
186
-
196
+
187
197
  except asyncio.CancelledError:
188
198
  logger.debug("SSE connection cancelled")
189
199
  finally:
@@ -191,18 +201,21 @@ async def sse_endpoint(request: Request):
191
201
 
192
202
  return EventSourceResponse(event_generator())
193
203
 
204
+
194
205
  @app.get("/api/v1/issues")
195
206
  async def get_issues(
196
207
  project_id: Optional[str] = None,
197
- path: Optional[str] = Query(None, description="Absolute file path for reverse lookup")
208
+ path: Optional[str] = Query(
209
+ None, description="Absolute file path for reverse lookup"
210
+ ),
198
211
  ):
199
212
  """
200
213
  List all issues in the project, or get a single issue by file path.
201
-
214
+
202
215
  Query Parameters:
203
216
  - project_id: Optional project filter
204
217
  - path: Optional absolute file path for reverse lookup (returns single issue)
205
-
218
+
206
219
  If 'path' is provided, returns a single IssueMetadata object.
207
220
  Otherwise, returns a list of all issues.
208
221
  """
@@ -211,27 +224,45 @@ async def get_issues(
211
224
  p = Path(path)
212
225
  if not p.exists():
213
226
  raise HTTPException(status_code=404, detail=f"File {path} not found")
214
-
227
+
215
228
  issue = parse_issue(p)
216
229
  if not issue:
217
- raise HTTPException(status_code=400, detail=f"File {path} is not a valid Monoco issue")
218
-
230
+ raise HTTPException(
231
+ status_code=400, detail=f"File {path} is not a valid Monoco issue"
232
+ )
233
+
219
234
  return issue
220
-
235
+
221
236
  # Standard list operation
222
237
  project = get_project_or_404(project_id)
223
238
  issues = list_issues(project.issues_root)
224
239
  return issues
225
240
 
226
- from monoco.features.issue.core import list_issues, create_issue_file, update_issue, delete_issue_file, find_issue_path, parse_issue, get_board_data, parse_issue_detail, update_issue_content
227
- from monoco.features.issue.models import IssueType, IssueStatus, IssueSolution, IssueStage, IssueMetadata, IssueDetail
228
- from monoco.daemon.models import CreateIssueRequest, UpdateIssueRequest, UpdateIssueContentRequest
241
+
242
+ from monoco.features.issue.core import (
243
+ list_issues,
244
+ create_issue_file,
245
+ update_issue,
246
+ delete_issue_file,
247
+ find_issue_path,
248
+ parse_issue,
249
+ get_board_data,
250
+ parse_issue_detail,
251
+ update_issue_content,
252
+ )
253
+ from monoco.features.issue.models import IssueMetadata, IssueDetail
254
+ from monoco.daemon.models import (
255
+ CreateIssueRequest,
256
+ UpdateIssueRequest,
257
+ UpdateIssueContentRequest,
258
+ )
229
259
  from monoco.daemon.stats import calculate_dashboard_stats, DashboardStats
230
260
  from fastapi import FastAPI, Request, HTTPException
231
261
  from typing import Optional, List, Dict
232
262
 
233
263
  # ... existing code ...
234
264
 
265
+
235
266
  @app.get("/api/v1/board")
236
267
  async def get_board_endpoint(project_id: Optional[str] = None):
237
268
  """
@@ -241,6 +272,7 @@ async def get_board_endpoint(project_id: Optional[str] = None):
241
272
  board = get_board_data(project.issues_root)
242
273
  return board
243
274
 
275
+
244
276
  @app.get("/api/v1/stats/dashboard", response_model=DashboardStats)
245
277
  async def get_dashboard_stats_endpoint(project_id: Optional[str] = None):
246
278
  """
@@ -256,23 +288,24 @@ async def create_issue_endpoint(payload: CreateIssueRequest):
256
288
  Create a new issue.
257
289
  """
258
290
  project = get_project_or_404(payload.project_id)
259
-
291
+
260
292
  try:
261
293
  issue, _ = create_issue_file(
262
- project.issues_root,
263
- payload.type,
264
- payload.title,
265
- parent=payload.parent,
266
- status=payload.status,
294
+ project.issues_root,
295
+ payload.type,
296
+ payload.title,
297
+ parent=payload.parent,
298
+ status=payload.status,
267
299
  stage=payload.stage,
268
- dependencies=payload.dependencies,
269
- related=payload.related,
270
- subdir=payload.subdir
300
+ dependencies=payload.dependencies,
301
+ related=payload.related,
302
+ subdir=payload.subdir,
271
303
  )
272
304
  return issue
273
305
  except Exception as e:
274
306
  raise HTTPException(status_code=400, detail=str(e))
275
307
 
308
+
276
309
  @app.get("/api/v1/issues/{issue_id}", response_model=IssueDetail)
277
310
  async def get_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
278
311
  """
@@ -286,19 +319,19 @@ async def get_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
286
319
  # Global Search across all projects in the workspace
287
320
  if not project_manager:
288
321
  raise HTTPException(status_code=503, detail="Daemon not fully initialized")
289
-
322
+
290
323
  for p_ctx in project_manager.projects.values():
291
324
  path = find_issue_path(p_ctx.issues_root, issue_id)
292
325
  if path:
293
326
  break
294
-
327
+
295
328
  if not path:
296
329
  raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
297
-
330
+
298
331
  issue = parse_issue_detail(path)
299
332
  if not issue:
300
333
  raise HTTPException(status_code=500, detail=f"Failed to parse issue {issue_id}")
301
-
334
+
302
335
  return issue
303
336
 
304
337
 
@@ -308,66 +341,68 @@ async def update_issue_endpoint(issue_id: str, payload: UpdateIssueRequest):
308
341
  Update an issue's metadata (Status, Stage, Solution, Parent, Dependencies, etc.).
309
342
  """
310
343
  project = get_project_or_404(payload.project_id)
311
-
344
+
312
345
  try:
313
346
  # Pre-lookup to get the current path for move detection
314
347
  old_path_obj = find_issue_path(project.issues_root, issue_id)
315
348
  old_path = str(old_path_obj.absolute()) if old_path_obj else None
316
349
 
317
350
  issue = update_issue(
318
- project.issues_root,
319
- issue_id,
320
- status=payload.status,
321
- stage=payload.stage,
351
+ project.issues_root,
352
+ issue_id,
353
+ status=payload.status,
354
+ stage=payload.stage,
322
355
  solution=payload.solution,
323
356
  parent=payload.parent,
324
357
  dependencies=payload.dependencies,
325
358
  related=payload.related,
326
- tags=payload.tags
359
+ tags=payload.tags,
327
360
  )
328
361
 
329
362
  # Post-update: check if path changed
330
363
  if old_path and issue.path != old_path:
331
364
  # Trigger a specialized move event to help editors redirect
332
- await project.notify_move(old_path, issue.path, issue.model_dump(mode='json'))
365
+ await project.notify_move(
366
+ old_path, issue.path, issue.model_dump(mode="json")
367
+ )
333
368
 
334
369
  return issue
335
370
  except FileNotFoundError:
336
371
  raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
337
372
  except ValueError as e:
338
- raise HTTPException(status_code=400, detail=str(e))
373
+ raise HTTPException(status_code=400, detail=str(e))
339
374
  except Exception as e:
340
375
  raise HTTPException(status_code=500, detail=str(e))
341
376
 
377
+
342
378
  @app.put("/api/v1/issues/{issue_id}/content", response_model=IssueMetadata)
343
- async def update_issue_content_endpoint(issue_id: str, payload: UpdateIssueContentRequest):
379
+ async def update_issue_content_endpoint(
380
+ issue_id: str, payload: UpdateIssueContentRequest
381
+ ):
344
382
  """
345
383
  Update raw content of an issue. Validates integrity before saving.
346
384
  """
347
385
  project = get_project_or_404(payload.project_id)
348
-
386
+
349
387
  try:
350
388
  # Note: We use PUT because we are replacing the content representation
351
- issue = update_issue_content(
352
- project.issues_root,
353
- issue_id,
354
- payload.content
355
- )
389
+ issue = update_issue_content(project.issues_root, issue_id, payload.content)
356
390
  return issue
357
391
  except FileNotFoundError:
358
392
  raise HTTPException(status_code=404, detail=f"Issue {issue_id} not found")
359
393
  except ValueError as e:
360
- raise HTTPException(status_code=400, detail=str(e))
394
+ raise HTTPException(status_code=400, detail=str(e))
361
395
  except Exception as e:
362
396
  raise HTTPException(status_code=500, detail=str(e))
363
397
 
398
+
364
399
  @app.delete("/api/v1/issues/{issue_id}")
365
400
  async def delete_issue_endpoint(issue_id: str, project_id: Optional[str] = None):
366
401
  """
367
402
  Delete an issue (physical removal).
368
403
  """
369
404
  project = get_project_or_404(project_id)
370
-
405
+
371
406
  try:
372
407
  delete_issue_file(project.issues_root, issue_id)
373
408
  return {"status": "deleted", "id": issue_id}
@@ -376,6 +411,7 @@ async def delete_issue_endpoint(issue_id: str, project_id: Optional[str] = None)
376
411
  except Exception as e:
377
412
  raise HTTPException(status_code=500, detail=str(e))
378
413
 
414
+
379
415
  @app.post("/api/v1/monitor/refresh")
380
416
  async def refresh_monitor():
381
417
  """
@@ -385,15 +421,17 @@ async def refresh_monitor():
385
421
  # In a real impl, we might force the monitor to wake up.
386
422
  # For now, just getting the hash is a good sanity check.
387
423
  current_hash = await git_monitor.get_head_hash()
388
-
424
+
389
425
  # If we wanted to FORCE broadcast, we could do it here,
390
426
  # but the monitor loop will pick it up in <2s anyway.
391
427
  # To be "instant", we can manually broadcast if we know it changed?
392
428
  # Or just returning the hash confirms the daemon sees it.
393
429
  return {"status": "refreshed", "head": current_hash}
394
430
 
431
+
395
432
  # --- Execution Profiles ---
396
433
 
434
+
397
435
  @app.get("/api/v1/execution/profiles", response_model=List[ExecutionProfile])
398
436
  async def get_execution_profiles(project_id: Optional[str] = None):
399
437
  """
@@ -405,9 +443,10 @@ async def get_execution_profiles(project_id: Optional[str] = None):
405
443
  elif project_manager and project_manager.projects:
406
444
  # Fallback to first project if none specified
407
445
  project = list(project_manager.projects.values())[0]
408
-
446
+
409
447
  return scan_execution_profiles(project.path if project else None)
410
448
 
449
+
411
450
  @app.get("/api/v1/execution/profiles/detail", response_model=ExecutionProfile)
412
451
  async def get_execution_profile_detail(path: str):
413
452
  """
@@ -418,30 +457,35 @@ async def get_execution_profile_detail(path: str):
418
457
  raise HTTPException(status_code=404, detail="Profile not found")
419
458
  return profile
420
459
 
460
+
421
461
  # --- Workspace State Management ---
422
462
  from monoco.core.state import WorkspaceState
423
463
 
464
+
424
465
  @app.get("/api/v1/workspace/state", response_model=WorkspaceState)
425
466
  async def get_workspace_state():
426
467
  """
427
468
  Get the persisted workspace state (e.g. last active project).
428
469
  """
429
470
  if not project_manager:
430
- raise HTTPException(status_code=503, detail="Daemon not initialized")
431
-
471
+ raise HTTPException(status_code=503, detail="Daemon not initialized")
472
+
432
473
  return WorkspaceState.load(project_manager.workspace_root)
433
474
 
475
+
434
476
  @app.post("/api/v1/workspace/state", response_model=WorkspaceState)
435
477
  async def update_workspace_state(state: WorkspaceState):
436
478
  """
437
479
  Update the workspace state.
438
480
  """
439
481
  if not project_manager:
440
- raise HTTPException(status_code=503, detail="Daemon not initialized")
482
+ raise HTTPException(status_code=503, detail="Daemon not initialized")
441
483
 
442
484
  try:
443
485
  state.save(project_manager.workspace_root)
444
486
  return state
445
487
  except Exception as e:
446
488
  logger.error(f"Failed to write state file: {e}")
447
- raise HTTPException(status_code=500, detail=f"Failed to persist state: {str(e)}")
489
+ raise HTTPException(
490
+ status_code=500, detail=f"Failed to persist state: {str(e)}"
491
+ )
monoco/daemon/commands.py CHANGED
@@ -7,30 +7,31 @@ from monoco.core.config import get_config
7
7
 
8
8
  from pathlib import Path
9
9
 
10
+
10
11
  def serve(
11
12
  host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host"),
12
13
  port: int = typer.Option(8642, "--port", "-p", help="Bind port"),
13
- reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload for dev"),
14
+ reload: bool = typer.Option(
15
+ False, "--reload", "-r", help="Enable auto-reload for dev"
16
+ ),
14
17
  root: Optional[str] = typer.Option(None, "--root", help="Workspace root directory"),
15
18
  ):
16
19
  """
17
20
  Start the Monoco Daemon server.
18
21
  """
19
22
  settings = get_config()
20
-
23
+
21
24
  if root:
22
25
  os.environ["MONOCO_SERVER_ROOT"] = str(Path(root).resolve())
23
- print_output(f"Workspace Root: {os.environ['MONOCO_SERVER_ROOT']}", title="Monoco Serve")
26
+ print_output(
27
+ f"Workspace Root: {os.environ['MONOCO_SERVER_ROOT']}", title="Monoco Serve"
28
+ )
29
+
30
+ print_output(
31
+ f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve"
32
+ )
24
33
 
25
- print_output(f"Starting Monoco Daemon on http://{host}:{port}", title="Monoco Serve")
26
-
27
34
  # We pass the import string to uvicorn to enable reload if needed
28
35
  app_str = "monoco.daemon.app:app"
29
-
30
- uvicorn.run(
31
- app_str,
32
- host=host,
33
- port=port,
34
- reload=reload,
35
- log_level="info"
36
- )
36
+
37
+ uvicorn.run(app_str, host=host, port=port, reload=reload, log_level="info")
monoco/daemon/models.py CHANGED
@@ -1,6 +1,12 @@
1
1
  from pydantic import BaseModel
2
2
  from typing import Optional, List
3
- from monoco.features.issue.models import IssueType, IssueStatus, IssueSolution, IssueStage
3
+ from monoco.features.issue.models import (
4
+ IssueType,
5
+ IssueStatus,
6
+ IssueSolution,
7
+ IssueStage,
8
+ )
9
+
4
10
 
5
11
  class CreateIssueRequest(BaseModel):
6
12
  type: IssueType
@@ -11,8 +17,9 @@ class CreateIssueRequest(BaseModel):
11
17
  dependencies: List[str] = []
12
18
  related: List[str] = []
13
19
  subdir: Optional[str] = None
14
- project_id: Optional[str] = None # Added for multi-project support
15
-
20
+ project_id: Optional[str] = None # Added for multi-project support
21
+
22
+
16
23
  class UpdateIssueRequest(BaseModel):
17
24
  status: Optional[IssueStatus] = None
18
25
  stage: Optional[IssueStage] = None
@@ -23,6 +30,7 @@ class UpdateIssueRequest(BaseModel):
23
30
  tags: Optional[List[str]] = None
24
31
  project_id: Optional[str] = None
25
32
 
33
+
26
34
  class UpdateIssueContentRequest(BaseModel):
27
35
  content: str
28
36
  project_id: Optional[str] = None
@@ -1,41 +1,41 @@
1
-
2
1
  import sys
3
2
  from pathlib import Path
4
- from datetime import datetime
5
3
 
6
4
  # Add Toolkit to sys.path
7
5
  sys.path.append("/Users/indenscale/Documents/Projects/Monoco/Toolkit")
8
6
 
9
7
  from monoco.daemon.stats import calculate_dashboard_stats
10
8
 
9
+
11
10
  def run():
12
11
  issues_root = Path("/Users/indenscale/Documents/Projects/Monoco/Toolkit/Issues")
13
12
  print(f"Scanning {issues_root}...")
14
-
13
+
15
14
  stats = calculate_dashboard_stats(issues_root)
16
-
15
+
17
16
  print(f"Found {len(stats.recent_activities)} activities.")
18
-
17
+
19
18
  seen_ids = set()
20
19
  for act in stats.recent_activities:
21
20
  print(f"[{act.type}] {act.id} - {act.issue_title} ({act.timestamp})")
22
21
  if act.id in seen_ids:
23
22
  print(f"!!! DUPLICATE ID FOUND: {act.id}")
24
23
  seen_ids.add(act.id)
25
-
24
+
26
25
  # Check for duplicate issue + type
27
26
  # e.g. multiple UPDATED for same issue
28
-
27
+
29
28
  # Group by issue
30
29
  by_issue = {}
31
30
  for act in stats.recent_activities:
32
31
  if act.issue_id not in by_issue:
33
32
  by_issue[act.issue_id] = []
34
33
  by_issue[act.issue_id].append(act)
35
-
34
+
36
35
  for issue_id, acts in by_issue.items():
37
36
  if len(acts) > 1:
38
37
  print(f"Issue {issue_id} has multiple activities: {[a.type for a in acts]}")
39
38
 
39
+
40
40
  if __name__ == "__main__":
41
41
  run()