utim-cli 1.0.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.
@@ -0,0 +1,629 @@
1
+ """
2
+ Semantic Vector Memory — Enhanced RAG using ChromaDB with all-MiniLM-L6-v2 embeddings.
3
+
4
+ This module provides semantic search capabilities that understand meaning and context,
5
+ going beyond exact keyword matching to find conceptually related code.
6
+ """
7
+
8
+ import os
9
+ import warnings
10
+
11
+ try:
12
+ from dotenv import load_dotenv
13
+ load_dotenv()
14
+ except ImportError:
15
+ pass
16
+
17
+ # Suppress HuggingFace and SentenceTransformers warnings and progress bars
18
+ os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
19
+ os.environ["TRANSFORMERS_VERBOSITY"] = "error"
20
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
21
+
22
+ import logging
23
+ logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
24
+ logging.getLogger("httpx").setLevel(logging.ERROR)
25
+ logging.getLogger("httpcore").setLevel(logging.ERROR)
26
+ logging.getLogger("sentence_transformers").setLevel(logging.ERROR)
27
+ logging.getLogger("chromadb").setLevel(logging.ERROR)
28
+ warnings.filterwarnings("ignore", message=".*unauthenticated requests.*")
29
+ warnings.filterwarnings("ignore", module="huggingface_hub.*")
30
+
31
+ import json
32
+ import hashlib
33
+ from typing import List, Dict, Optional, Tuple
34
+ import numpy as np
35
+
36
+ # Enable ChromaDB and Hugging Face vector memory embeddings
37
+ try:
38
+ import chromadb
39
+ import chromadb.utils.embedding_functions as embedding_functions
40
+ CHROMA_AVAILABLE = True
41
+ except ImportError:
42
+ CHROMA_AVAILABLE = False
43
+
44
+ # Vector DB path
45
+ VECTOR_DB_PATH = ".utim_tmp/vector_db"
46
+ METADATA_FILE = ".utim_tmp/vector_meta.json"
47
+
48
+
49
+ class DeterministicMockEmbeddingFunction:
50
+ """Fallback 100% offline mock embedding function."""
51
+ def __init__(self):
52
+ pass
53
+
54
+ @classmethod
55
+ def name(cls) -> str:
56
+ return "DeterministicMockEmbeddingFunction"
57
+
58
+ def get_config(self) -> dict:
59
+ return {}
60
+
61
+ @staticmethod
62
+ def build_from_config(config: dict) -> 'DeterministicMockEmbeddingFunction':
63
+ return DeterministicMockEmbeddingFunction()
64
+
65
+ def __call__(self, input: List[str]) -> List[List[float]]:
66
+ import hashlib
67
+ embeddings = []
68
+ for text in input:
69
+ words = text.lower().split()
70
+ vec = [0.0] * 384
71
+ if words:
72
+ for w in words:
73
+ h = int(hashlib.md5(w.encode('utf-8')).hexdigest(), 16)
74
+ for i in range(4):
75
+ idx = (h >> (i * 8)) % 384
76
+ vec[idx] += 1.0
77
+ norm = sum(x*x for x in vec) ** 0.5
78
+ if norm > 0:
79
+ vec = [x / norm for x in vec]
80
+ embeddings.append(vec)
81
+ return embeddings
82
+
83
+
84
+ class VectorMemory:
85
+ """
86
+ Semantic vector memory using ChromaDB with Hugging Face embeddings (all-MiniLM-L6-v2).
87
+ Provides meaning-based search over codebase content, task reflections, and experiences.
88
+ """
89
+
90
+ def __init__(self, collection_name: str = "codebase"):
91
+ self.client = None
92
+ self.collection = None
93
+ self.embedding_func = None
94
+ self.file_metadata: Dict[str, Dict] = {}
95
+ self.collection_name = collection_name
96
+ self.metadata_file = f".utim_tmp/vector_meta_{collection_name}.json"
97
+
98
+ if not CHROMA_AVAILABLE:
99
+ raise ImportError("chromadb is required for vector memory")
100
+
101
+ self._initialize()
102
+
103
+ def _initialize(self):
104
+ """Initialize ChromaDB client and collection with Hugging Face embedding model."""
105
+ os.makedirs(VECTOR_DB_PATH, exist_ok=True)
106
+
107
+ # Create persistent client
108
+ self.client = chromadb.PersistentClient(path=VECTOR_DB_PATH)
109
+
110
+ # Initialize Hugging Face embeddings
111
+ try:
112
+ self.embedding_func = embedding_functions.DefaultEmbeddingFunction()
113
+ except Exception:
114
+ try:
115
+ self.embedding_func = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
116
+ except Exception:
117
+ self.embedding_func = DeterministicMockEmbeddingFunction()
118
+
119
+ # Get or create collection
120
+ try:
121
+ self.collection = self.client.get_collection(
122
+ name=self.collection_name,
123
+ embedding_function=self.embedding_func
124
+ )
125
+ except Exception:
126
+ self.collection = self.client.create_collection(
127
+ name=self.collection_name,
128
+ embedding_function=self.embedding_func
129
+ )
130
+
131
+ # Load metadata
132
+ self._load_metadata()
133
+
134
+ def _load_metadata(self):
135
+ """Load file metadata from disk."""
136
+ if os.path.exists(self.metadata_file):
137
+ try:
138
+ with open(self.metadata_file, "r", encoding="utf-8") as f:
139
+ self.file_metadata = json.load(f)
140
+ except Exception as e:
141
+ from utim_cli.logger import log_error
142
+ log_error("vector_memory", f"Failed to load vector metadata from {self.metadata_file}. Resetting metadata.", e)
143
+ self.file_metadata = {}
144
+
145
+ def _save_metadata(self):
146
+ """Save file metadata to disk."""
147
+ os.makedirs(".utim_tmp", exist_ok=True)
148
+ with open(self.metadata_file, "w", encoding="utf-8") as f:
149
+ json.dump(self.file_metadata, f, indent=2)
150
+
151
+ def _get_file_hash(self, filepath: str) -> str:
152
+ """Get MD5 hash of file for change detection."""
153
+ try:
154
+ with open(filepath, "rb") as f:
155
+ return hashlib.md5(f.read()).hexdigest()
156
+ except Exception as e:
157
+ from utim_cli.logger import log_warning
158
+ log_warning("vector_memory", f"Failed to get file hash for {filepath}", e)
159
+ return ""
160
+
161
+ def sync_files(self, paths: List[str] = None, exclude_dirs: List[str] = None) -> int:
162
+ """
163
+ Sync files to vector database.
164
+
165
+ Args:
166
+ paths: List of file paths to sync. If None, walks current directory.
167
+ exclude_dirs: Directories to exclude from sync.
168
+
169
+ Returns:
170
+ Number of files indexed/updated.
171
+ """
172
+ if exclude_dirs is None:
173
+ exclude_dirs = {".git", "node_modules", "dist", "build", "__pycache__", ".venv", "venv", ".utim_tmp"}
174
+ else:
175
+ exclude_dirs = set(exclude_dirs)
176
+
177
+ files_to_sync = []
178
+
179
+ if paths:
180
+ files_to_sync = [(p, os.path.getmtime(p)) for p in paths if os.path.exists(p)]
181
+ else:
182
+ for root, dirs, files in os.walk("."):
183
+ dirs[:] = [d for d in dirs if d not in exclude_dirs]
184
+ for f in files:
185
+ ext = os.path.splitext(f)[1].lower()
186
+ if ext in ['.png', '.jpg', '.jpeg', '.gif', '.mp4', '.pdf', '.zip', '.exe', '.dll', '.pyc']:
187
+ continue
188
+ p = os.path.join(root, f)
189
+ try:
190
+ files_to_sync.append((p, os.path.getmtime(p)))
191
+ except Exception as e:
192
+ from utim_cli.logger import log_warning
193
+ log_warning("vector_memory", f"Failed to get modification time for {p}", e)
194
+
195
+ # Determine which files need updating
196
+ to_index = []
197
+ to_delete_ids = []
198
+
199
+ current_hashes = {}
200
+ for filepath, mtime in files_to_sync:
201
+ file_hash = self._get_file_hash(filepath)
202
+ current_hashes[filepath] = file_hash
203
+
204
+ existing = self.file_metadata.get(filepath, {})
205
+ if (filepath not in self.file_metadata or
206
+ existing.get("hash") != file_hash):
207
+ to_index.append(filepath)
208
+
209
+ # Check for deleted files
210
+ for filepath in list(self.file_metadata.keys()):
211
+ if filepath not in current_hashes:
212
+ to_delete_ids.append(self.file_metadata[filepath].get("chunk_ids", []))
213
+ del self.file_metadata[filepath]
214
+
215
+ # Delete removed chunks
216
+ if to_delete_ids:
217
+ flat_ids = [id for ids in to_delete_ids for id in ids]
218
+ if flat_ids:
219
+ try:
220
+ self.collection.delete(ids=flat_ids)
221
+ except Exception as e:
222
+ from utim_cli.logger import log_error
223
+ log_error("vector_memory", f"Failed to delete chunks from collection {self.collection_name}", e)
224
+
225
+ # Index new/updated files
226
+ indexed_count = 0
227
+ new_metadata = {}
228
+
229
+ for filepath in to_index:
230
+ try:
231
+ with open(filepath, "r", encoding="utf-8") as f:
232
+ content = f.read()
233
+
234
+ file_hash = self._get_file_hash(filepath)
235
+
236
+ # Chunk content for better retrieval
237
+ chunks = self._chunk_content(content, filepath)
238
+
239
+ if chunks:
240
+ ids = []
241
+ documents = []
242
+ metadatas = []
243
+
244
+ for i, (chunk_text, line_start, line_end) in enumerate(chunks):
245
+ chunk_id = f"{filepath}:{line_start}-{line_end}"
246
+ ids.append(chunk_id)
247
+ documents.append(chunk_text)
248
+ metadatas.append({
249
+ "filepath": filepath,
250
+ "line_start": line_start,
251
+ "line_end": line_end,
252
+ "chunk_index": i
253
+ })
254
+
255
+ self.collection.add(
256
+ ids=ids,
257
+ documents=documents,
258
+ metadatas=metadatas
259
+ )
260
+
261
+ new_metadata[filepath] = {
262
+ "hash": file_hash,
263
+ "mtime": os.path.getmtime(filepath),
264
+ "chunk_ids": ids
265
+ }
266
+ indexed_count += 1
267
+
268
+ except Exception as e:
269
+ continue
270
+
271
+ self.file_metadata.update(new_metadata)
272
+ self._save_metadata()
273
+
274
+ return indexed_count
275
+
276
+ def _chunk_content(self, content: str, filepath: str, chunk_size: int = 1000, overlap: int = 100) -> List[Tuple[str, int, int]]:
277
+ """
278
+ Chunk content into overlapping segments with line number tracking.
279
+
280
+ Returns list of (chunk_text, start_line, end_line) tuples.
281
+ """
282
+ lines = content.split('\n')
283
+ chunks = []
284
+
285
+ if len(lines) <= chunk_size:
286
+ return [(content, 1, len(lines))]
287
+
288
+ for i in range(0, len(lines), chunk_size - overlap):
289
+ end = min(i + chunk_size, len(lines))
290
+ chunk_text = '\n'.join(lines[i:end])
291
+ chunks.append((chunk_text, i + 1, end))
292
+
293
+ return chunks
294
+
295
+ def query(self, query_text: str, n_results: int = 5, where: Dict = None) -> List[Dict]:
296
+ """
297
+ Query the vector database for semantically similar content.
298
+
299
+ Args:
300
+ query_text: Natural language query
301
+ n_results: Number of results to return
302
+ where: Optional metadata filter
303
+
304
+ Returns:
305
+ List of result dictionaries with content, filepath, and similarity info.
306
+ """
307
+ if not self.collection:
308
+ return []
309
+
310
+ try:
311
+ results = self.collection.query(
312
+ query_texts=[query_text],
313
+ n_results=n_results,
314
+ where=where
315
+ )
316
+
317
+ formatted_results = []
318
+ if results["documents"] and results["documents"][0]:
319
+ for i, doc in enumerate(results["documents"][0]):
320
+ formatted_results.append({
321
+ "id": results["ids"][0][i] if results["ids"] else None,
322
+ "content": doc,
323
+ "filepath": results["metadatas"][0][i].get("filepath", ""),
324
+ "line_start": results["metadatas"][0][i].get("line_start", 0),
325
+ "line_end": results["metadatas"][0][i].get("line_end", 0),
326
+ "metadata": results["metadatas"][0][i],
327
+ "distance": results["distances"][0][i] if results["distances"] else None
328
+ })
329
+
330
+ return formatted_results
331
+
332
+ except Exception as e:
333
+ return []
334
+
335
+ def add_text(self, text_id: str, content: str, metadata: Dict = None) -> bool:
336
+ """Add a single piece of text directly to the vector database."""
337
+ if not self.collection:
338
+ return False
339
+
340
+ try:
341
+ # Try to update if it exists
342
+ existing = self.collection.get(ids=[text_id])
343
+ if existing and existing["ids"]:
344
+ self.collection.update(
345
+ ids=[text_id],
346
+ documents=[content],
347
+ metadatas=[metadata or {}]
348
+ )
349
+ else:
350
+ self.collection.add(
351
+ ids=[text_id],
352
+ documents=[content],
353
+ metadatas=[metadata or {}]
354
+ )
355
+ return True
356
+ except Exception as e:
357
+ return False
358
+
359
+ def get_stats(self) -> Dict:
360
+ """Get statistics about the vector database."""
361
+ if not self.collection:
362
+ return {"total_files": 0, "total_chunks": 0}
363
+
364
+ try:
365
+ count = self.collection.count()
366
+ return {
367
+ "total_files": len(self.file_metadata),
368
+ "total_chunks": count,
369
+ "db_path": VECTOR_DB_PATH
370
+ }
371
+ except Exception as e:
372
+ from utim_cli.logger import log_error
373
+ log_error("vector_memory", f"Failed to get stats for collection {self.collection_name}", e)
374
+ return {"total_files": 0, "total_chunks": 0}
375
+
376
+
377
+ # Global instance
378
+ _vector_memory: Optional[VectorMemory] = None
379
+ _experiences_memory: Optional[VectorMemory] = None
380
+ _skills_memory: Optional[VectorMemory] = None
381
+
382
+ # Sub-agent global instances
383
+ _project_res_experiences: Optional[VectorMemory] = None
384
+ _project_res_skills: Optional[VectorMemory] = None
385
+ _plan_project_experiences: Optional[VectorMemory] = None
386
+ _plan_project_skills: Optional[VectorMemory] = None
387
+ _web_search_experiences: Optional[VectorMemory] = None
388
+ _web_search_skills: Optional[VectorMemory] = None
389
+ _generate_image_experiences: Optional[VectorMemory] = None
390
+ _generate_image_skills: Optional[VectorMemory] = None
391
+
392
+ # Time memory global instance
393
+ _time_memory: Optional[VectorMemory] = None
394
+ _user_memories: Optional[VectorMemory] = None
395
+
396
+ def get_vector_memory() -> VectorMemory:
397
+ """Get or create the global vector memory instance for the codebase."""
398
+ global _vector_memory
399
+ if _vector_memory is None:
400
+ try:
401
+ _vector_memory = VectorMemory(collection_name="codebase")
402
+ except ImportError:
403
+ pass
404
+ return _vector_memory
405
+
406
+ def get_experiences_memory() -> VectorMemory:
407
+ """Get or create the global vector memory instance for the experiences."""
408
+ global _experiences_memory
409
+ if _experiences_memory is None:
410
+ try:
411
+ _experiences_memory = VectorMemory(collection_name="experiences")
412
+ except ImportError:
413
+ pass
414
+ return _experiences_memory
415
+
416
+ def get_skills_memory() -> VectorMemory:
417
+ """Get or create the global vector memory instance for skills/rules."""
418
+ global _skills_memory
419
+ if _skills_memory is None:
420
+ try:
421
+ _skills_memory = VectorMemory(collection_name="skills")
422
+ except ImportError:
423
+ pass
424
+ return _skills_memory
425
+
426
+ # Sub-agent getters
427
+ def get_project_res_experiences_memory() -> VectorMemory:
428
+ """Get or create the experiences vector memory instance for the project_res sub-agent."""
429
+ global _project_res_experiences
430
+ if _project_res_experiences is None:
431
+ try:
432
+ _project_res_experiences = VectorMemory(collection_name="project_res_experiences")
433
+ except ImportError:
434
+ pass
435
+ return _project_res_experiences
436
+
437
+ def get_project_res_skills_memory() -> VectorMemory:
438
+ """Get or create the skills vector memory instance for the project_res sub-agent."""
439
+ global _project_res_skills
440
+ if _project_res_skills is None:
441
+ try:
442
+ _project_res_skills = VectorMemory(collection_name="project_res_skills")
443
+ except ImportError:
444
+ pass
445
+ return _project_res_skills
446
+
447
+ def get_plan_project_experiences_memory() -> VectorMemory:
448
+ """Get or create the experiences vector memory instance for the plan_project sub-agent."""
449
+ global _plan_project_experiences
450
+ if _plan_project_experiences is None:
451
+ try:
452
+ _plan_project_experiences = VectorMemory(collection_name="plan_project_experiences")
453
+ except ImportError:
454
+ pass
455
+ return _plan_project_experiences
456
+
457
+ def get_plan_project_skills_memory() -> VectorMemory:
458
+ """Get or create the skills vector memory instance for the plan_project sub-agent."""
459
+ global _plan_project_skills
460
+ if _plan_project_skills is None:
461
+ try:
462
+ _plan_project_skills = VectorMemory(collection_name="plan_project_skills")
463
+ except ImportError:
464
+ pass
465
+ return _plan_project_skills
466
+
467
+ def get_web_search_experiences_memory() -> VectorMemory:
468
+ """Get or create the experiences vector memory instance for the web_search sub-agent."""
469
+ global _web_search_experiences
470
+ if _web_search_experiences is None:
471
+ try:
472
+ _web_search_experiences = VectorMemory(collection_name="web_search_experiences")
473
+ except ImportError:
474
+ pass
475
+ return _web_search_experiences
476
+
477
+ def get_web_search_skills_memory() -> VectorMemory:
478
+ """Get or create the skills vector memory instance for the web_search sub-agent."""
479
+ global _web_search_skills
480
+ if _web_search_skills is None:
481
+ try:
482
+ _web_search_skills = VectorMemory(collection_name="web_search_skills")
483
+ except ImportError:
484
+ pass
485
+ return _web_search_skills
486
+
487
+ def get_generate_image_experiences_memory() -> VectorMemory:
488
+ """Get or create the experiences vector memory instance for the generate_image sub-agent."""
489
+ global _generate_image_experiences
490
+ if _generate_image_experiences is None:
491
+ try:
492
+ _generate_image_experiences = VectorMemory(collection_name="generate_image_experiences")
493
+ except ImportError:
494
+ pass
495
+ return _generate_image_experiences
496
+
497
+ def get_generate_image_skills_memory() -> VectorMemory:
498
+ """Get or create the skills vector memory instance for the generate_image sub-agent."""
499
+ global _generate_image_skills
500
+ if _generate_image_skills is None:
501
+ try:
502
+ _generate_image_skills = VectorMemory(collection_name="generate_image_skills")
503
+ except ImportError:
504
+ pass
505
+ return _generate_image_skills
506
+
507
+ def get_time_memory() -> VectorMemory:
508
+ """Get or create the vector memory instance for time tracking and performance history."""
509
+ global _time_memory
510
+ if _time_memory is None:
511
+ try:
512
+ _time_memory = VectorMemory(collection_name="time_memory")
513
+ except ImportError:
514
+ pass
515
+ return _time_memory
516
+
517
+ # Global reflection memory instance
518
+ _reflections_memory: Optional[VectorMemory] = None
519
+
520
+ def get_reflections_memory() -> VectorMemory:
521
+ """Get or create the global vector memory instance for task reflections."""
522
+ global _reflections_memory
523
+ if _reflections_memory is None:
524
+ try:
525
+ _reflections_memory = VectorMemory(collection_name="reflections")
526
+ except Exception:
527
+ pass
528
+ return _reflections_memory
529
+
530
+ def warmup_embedding_model() -> bool:
531
+ """
532
+ Eagerly load the Hugging Face all-MiniLM-L6-v2 embedding model into memory.
533
+ Call this at startup in a background thread so the first real encode is instant.
534
+ Returns True if model was successfully loaded, False otherwise.
535
+ """
536
+ try:
537
+ vm = get_reflections_memory()
538
+ if vm and vm.embedding_func:
539
+ # Encode a dummy string — this forces the ONNX/SentenceTransformer
540
+ # model weights to be downloaded (if not cached) and loaded into RAM.
541
+ _ = vm.embedding_func(["utim warmup ping"])
542
+ return True
543
+ except Exception:
544
+ pass
545
+ # Also try to pre-warm situational scoring's shared embedding function
546
+ try:
547
+ from utim_cli.situational_scoring import get_embedding_fn
548
+ fn = get_embedding_fn()
549
+ if fn:
550
+ _ = fn(["utim warmup ping"])
551
+ except Exception:
552
+ pass
553
+ return False
554
+
555
+ def store_reflection(content: str, category: str = "general_reflection", task_prompt: str = "") -> bool:
556
+ """Store a reflection or learned insight into the Hugging Face Reflection Vector DB."""
557
+ vm = get_reflections_memory()
558
+ if not vm:
559
+ return False
560
+ import uuid
561
+ from datetime import datetime
562
+ text_id = f"refl_{uuid.uuid4().hex[:12]}"
563
+ return vm.add_text(
564
+ text_id=text_id,
565
+ content=content,
566
+ metadata={
567
+ "category": category,
568
+ "timestamp": datetime.now().isoformat(),
569
+ "task_prompt": task_prompt[:150]
570
+ }
571
+ )
572
+
573
+ def fetch_relevant_experiences(query_text: str, top_k: int = 2) -> List[Dict]:
574
+ """
575
+ Fetch semantically relevant reflections and learned rules using Hugging Face embeddings.
576
+ Applies situational parameter scoring to prioritize operational rules (e.g. command operators) matching the task.
577
+ """
578
+ results = []
579
+ # 1. Query reflections vector collection
580
+ vm_ref = get_reflections_memory()
581
+ if vm_ref:
582
+ res = vm_ref.query(query_text=query_text, n_results=5)
583
+ results.extend([r for r in res if r.get("distance") is None or r.get("distance") < 1.2])
584
+
585
+ # 2. Query general experiences vector collection
586
+ vm_exp = get_experiences_memory()
587
+ if vm_exp:
588
+ res = vm_exp.query(query_text=query_text, n_results=5)
589
+ results.extend([r for r in res if r.get("distance") is None or r.get("distance") < 1.2])
590
+
591
+ # Deduplicate by content
592
+ seen = set()
593
+ unique_results = []
594
+ for item in results:
595
+ text = item.get("content", "").strip()
596
+ if text and text not in seen:
597
+ seen.add(text)
598
+ # Invert distance into base_score (closer distance = higher score)
599
+ dist = item.get("distance")
600
+ item["base_score"] = 1.0 / (1.0 + dist) if dist is not None else 1.0
601
+ unique_results.append(item)
602
+
603
+ # Apply situational scoring and filtering
604
+ try:
605
+ from utim_cli.situational_scoring import score_and_filter_context
606
+ scored_results = score_and_filter_context(unique_results, query_text, limit=top_k)
607
+ return scored_results
608
+ except Exception:
609
+ return unique_results[:top_k]
610
+
611
+ def reset_vector_memory():
612
+ """Reset the global vector memory instances."""
613
+ global _vector_memory, _experiences_memory, _skills_memory, _reflections_memory
614
+ global _project_res_experiences, _project_res_skills, _plan_project_experiences, _plan_project_skills, _web_search_experiences, _web_search_skills, _generate_image_experiences, _generate_image_skills
615
+ global _time_memory, _user_memories
616
+ _vector_memory = None
617
+ _experiences_memory = None
618
+ _skills_memory = None
619
+ _reflections_memory = None
620
+ _project_res_experiences = None
621
+ _project_res_skills = None
622
+ _plan_project_experiences = None
623
+ _plan_project_skills = None
624
+ _web_search_experiences = None
625
+ _web_search_skills = None
626
+ _generate_image_experiences = None
627
+ _generate_image_skills = None
628
+ _time_memory = None
629
+ _user_memories = None
utim_cli/workspace.py ADDED
@@ -0,0 +1,33 @@
1
+ import os
2
+
3
+ class WorkspaceManager:
4
+ def __init__(self, root_dir="."):
5
+ self.root_dir = root_dir
6
+
7
+ def list_files(self):
8
+ file_tree = []
9
+ for root, dirs, files in os.walk(self.root_dir):
10
+ if ".git" in dirs:
11
+ dirs.remove(".git")
12
+ for file in files:
13
+ file_tree.append(os.path.join(root, file))
14
+ return file_tree
15
+
16
+ def read_file(self, file_path):
17
+ full_path = os.path.join(self.root_dir, file_path)
18
+ if os.path.exists(full_path):
19
+ with open(full_path, "r") as f:
20
+ return f.read()
21
+ return None
22
+
23
+ def write_file(self, file_path, content):
24
+ full_path = os.path.join(self.root_dir, file_path)
25
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
26
+ with open(full_path, "w") as f:
27
+ f.write(content)
28
+ return True
29
+
30
+ def get_context(self):
31
+ """Build a context string summarizing the workspace."""
32
+ files = self.list_files()
33
+ return f"Current Workspace Files: {', '.join(files[:20])}" # Limit for context size