claude-code-workflow 6.3.26 → 6.3.28

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 (129) hide show
  1. package/.claude/CLAUDE.md +7 -1
  2. package/.claude/agents/action-planning-agent.md +1 -0
  3. package/.claude/agents/cli-discuss-agent.md +391 -0
  4. package/.claude/agents/cli-execution-agent.md +2 -0
  5. package/.claude/agents/cli-explore-agent.md +2 -1
  6. package/.claude/agents/cli-lite-planning-agent.md +1 -0
  7. package/.claude/agents/cli-planning-agent.md +1 -0
  8. package/.claude/agents/code-developer.md +1 -0
  9. package/.claude/agents/conceptual-planning-agent.md +2 -0
  10. package/.claude/agents/context-search-agent.md +1 -0
  11. package/.claude/agents/debug-explore-agent.md +2 -0
  12. package/.claude/agents/doc-generator.md +1 -0
  13. package/.claude/agents/issue-plan-agent.md +2 -1
  14. package/.claude/agents/issue-queue-agent.md +2 -1
  15. package/.claude/agents/memory-bridge.md +2 -0
  16. package/.claude/agents/test-context-search-agent.md +2 -0
  17. package/.claude/agents/test-fix-agent.md +1 -0
  18. package/.claude/agents/ui-design-agent.md +2 -0
  19. package/.claude/agents/universal-executor.md +1 -0
  20. package/.claude/commands/issue/execute.md +141 -163
  21. package/.claude/commands/workflow/lite-lite-lite.md +798 -0
  22. package/.claude/commands/workflow/multi-cli-plan.md +510 -0
  23. package/.claude/skills/ccw/SKILL.md +262 -372
  24. package/.claude/skills/ccw/command.json +547 -0
  25. package/.claude/skills/ccw-help/SKILL.md +46 -107
  26. package/.claude/skills/ccw-help/command.json +511 -0
  27. package/.claude/skills/skill-tuning/SKILL.md +303 -0
  28. package/.claude/skills/skill-tuning/phases/actions/action-abort.md +164 -0
  29. package/.claude/skills/skill-tuning/phases/actions/action-analyze-requirements.md +406 -0
  30. package/.claude/skills/skill-tuning/phases/actions/action-apply-fix.md +206 -0
  31. package/.claude/skills/skill-tuning/phases/actions/action-complete.md +195 -0
  32. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-agent.md +317 -0
  33. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-context.md +243 -0
  34. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-dataflow.md +318 -0
  35. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-docs.md +299 -0
  36. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-memory.md +269 -0
  37. package/.claude/skills/skill-tuning/phases/actions/action-diagnose-token-consumption.md +200 -0
  38. package/.claude/skills/skill-tuning/phases/actions/action-gemini-analysis.md +322 -0
  39. package/.claude/skills/skill-tuning/phases/actions/action-generate-report.md +228 -0
  40. package/.claude/skills/skill-tuning/phases/actions/action-init.md +149 -0
  41. package/.claude/skills/skill-tuning/phases/actions/action-propose-fixes.md +317 -0
  42. package/.claude/skills/skill-tuning/phases/actions/action-verify.md +222 -0
  43. package/.claude/skills/skill-tuning/phases/orchestrator.md +377 -0
  44. package/.claude/skills/skill-tuning/phases/state-schema.md +378 -0
  45. package/.claude/skills/skill-tuning/specs/category-mappings.json +284 -0
  46. package/.claude/skills/skill-tuning/specs/dimension-mapping.md +212 -0
  47. package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +318 -0
  48. package/.claude/skills/skill-tuning/specs/quality-gates.md +263 -0
  49. package/.claude/skills/skill-tuning/specs/skill-authoring-principles.md +189 -0
  50. package/.claude/skills/skill-tuning/specs/tuning-strategies.md +1537 -0
  51. package/.claude/skills/skill-tuning/templates/diagnosis-report.md +153 -0
  52. package/.claude/skills/skill-tuning/templates/fix-proposal.md +204 -0
  53. package/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json +421 -0
  54. package/.claude/workflows/cli-tools-usage.md +0 -41
  55. package/ccw/dist/core/auth/csrf-middleware.d.ts.map +1 -1
  56. package/ccw/dist/core/auth/csrf-middleware.js +3 -1
  57. package/ccw/dist/core/auth/csrf-middleware.js.map +1 -1
  58. package/ccw/dist/core/data-aggregator.d.ts +2 -0
  59. package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
  60. package/ccw/dist/core/data-aggregator.js +5 -2
  61. package/ccw/dist/core/data-aggregator.js.map +1 -1
  62. package/ccw/dist/core/lite-scanner.d.ts +2 -1
  63. package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
  64. package/ccw/dist/core/lite-scanner.js +295 -6
  65. package/ccw/dist/core/lite-scanner.js.map +1 -1
  66. package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
  67. package/ccw/dist/core/routes/codexlens/config-handlers.js +5 -5
  68. package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
  69. package/ccw/dist/core/routes/session-routes.d.ts.map +1 -1
  70. package/ccw/dist/core/routes/session-routes.js +166 -48
  71. package/ccw/dist/core/routes/session-routes.js.map +1 -1
  72. package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
  73. package/ccw/dist/core/routes/system-routes.js +87 -0
  74. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  75. package/ccw/dist/core/server.js +2 -2
  76. package/ccw/dist/core/server.js.map +1 -1
  77. package/ccw/scripts/IMPLEMENTATION-SUMMARY.md +226 -0
  78. package/ccw/scripts/QUICK-REFERENCE.md +135 -0
  79. package/ccw/scripts/README-memory-embedder.md +157 -0
  80. package/ccw/scripts/__pycache__/memory_embedder.cpython-313.pyc +0 -0
  81. package/ccw/scripts/__pycache__/test_memory_embedder.cpython-313-pytest-8.4.2.pyc +0 -0
  82. package/ccw/scripts/memory-embedder-example.ts +184 -0
  83. package/ccw/scripts/memory_embedder.py +428 -0
  84. package/ccw/scripts/test_memory_embedder.py +245 -0
  85. package/ccw/src/core/auth/csrf-middleware.ts +3 -1
  86. package/ccw/src/core/data-aggregator.ts +7 -2
  87. package/ccw/src/core/lite-scanner.ts +440 -6
  88. package/ccw/src/core/routes/codexlens/config-handlers.ts +12 -9
  89. package/ccw/src/core/routes/session-routes.ts +201 -48
  90. package/ccw/src/core/routes/system-routes.ts +102 -0
  91. package/ccw/src/core/server.ts +2 -2
  92. package/ccw/src/templates/dashboard-css/01-base.css +8 -0
  93. package/ccw/src/templates/dashboard-css/02-session.css +81 -0
  94. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +2442 -0
  95. package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +157 -0
  96. package/ccw/src/templates/dashboard-css/32-issue-manager.css +23 -0
  97. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +38 -4
  98. package/ccw/src/templates/dashboard-js/components/hook-manager.js +38 -13
  99. package/ccw/src/templates/dashboard-js/components/navigation.js +24 -4
  100. package/ccw/src/templates/dashboard-js/i18n.js +194 -6
  101. package/ccw/src/templates/dashboard-js/views/api-settings.js +32 -0
  102. package/ccw/src/templates/dashboard-js/views/claude-manager.js +44 -3
  103. package/ccw/src/templates/dashboard-js/views/cli-manager.js +303 -31
  104. package/ccw/src/templates/dashboard-js/views/history.js +44 -6
  105. package/ccw/src/templates/dashboard-js/views/home.js +1 -0
  106. package/ccw/src/templates/dashboard-js/views/issue-manager.js +54 -7
  107. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +1817 -4
  108. package/ccw/src/templates/dashboard.html +5 -0
  109. package/package.json +2 -1
  110. package/.claude/skills/ccw/index/command-capabilities.json +0 -127
  111. package/.claude/skills/ccw/index/intent-rules.json +0 -136
  112. package/.claude/skills/ccw/index/workflow-chains.json +0 -451
  113. package/.claude/skills/ccw/phases/actions/bugfix.md +0 -218
  114. package/.claude/skills/ccw/phases/actions/coupled.md +0 -194
  115. package/.claude/skills/ccw/phases/actions/docs.md +0 -93
  116. package/.claude/skills/ccw/phases/actions/full.md +0 -154
  117. package/.claude/skills/ccw/phases/actions/issue.md +0 -201
  118. package/.claude/skills/ccw/phases/actions/rapid.md +0 -104
  119. package/.claude/skills/ccw/phases/actions/review-fix.md +0 -84
  120. package/.claude/skills/ccw/phases/actions/tdd.md +0 -66
  121. package/.claude/skills/ccw/phases/actions/ui.md +0 -79
  122. package/.claude/skills/ccw/phases/orchestrator.md +0 -435
  123. package/.claude/skills/ccw/specs/intent-classification.md +0 -336
  124. package/.claude/skills/ccw-help/index/all-agents.json +0 -82
  125. package/.claude/skills/ccw-help/index/all-commands.json +0 -882
  126. package/.claude/skills/ccw-help/index/by-category.json +0 -914
  127. package/.claude/skills/ccw-help/index/by-use-case.json +0 -896
  128. package/.claude/skills/ccw-help/index/command-relationships.json +0 -160
  129. package/.claude/skills/ccw-help/index/essential-commands.json +0 -112
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Memory Embedder - Bridge CCW to CodexLens semantic search
4
+
5
+ This script generates and searches embeddings for memory chunks stored in CCW's
6
+ SQLite database using CodexLens's embedder.
7
+
8
+ Usage:
9
+ python memory_embedder.py embed <db_path> [--source-id ID] [--batch-size N] [--force]
10
+ python memory_embedder.py search <db_path> <query> [--top-k N] [--min-score F] [--type TYPE]
11
+ python memory_embedder.py status <db_path>
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sqlite3
17
+ import sys
18
+ import time
19
+ from pathlib import Path
20
+ from typing import List, Dict, Any, Optional, Tuple
21
+
22
+ try:
23
+ import numpy as np
24
+ except ImportError:
25
+ print("Error: numpy is required. Install with: pip install numpy", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ try:
29
+ from codexlens.semantic.factory import get_embedder as get_embedder_factory
30
+ from codexlens.semantic.factory import clear_embedder_cache
31
+ from codexlens.config import Config as CodexLensConfig
32
+ except ImportError:
33
+ print("Error: CodexLens not found. Install with: pip install codexlens[semantic]", file=sys.stderr)
34
+ sys.exit(1)
35
+
36
+
37
+ class MemoryEmbedder:
38
+ """Generate and search embeddings for memory chunks."""
39
+
40
+ def __init__(self, db_path: str):
41
+ """Initialize embedder with database path."""
42
+ self.db_path = Path(db_path)
43
+ if not self.db_path.exists():
44
+ raise FileNotFoundError(f"Database not found: {db_path}")
45
+
46
+ self.conn = sqlite3.connect(str(self.db_path))
47
+ self.conn.row_factory = sqlite3.Row
48
+
49
+ # Load CodexLens configuration for embedding settings
50
+ try:
51
+ self._config = CodexLensConfig.load()
52
+ except Exception as e:
53
+ print(f"Warning: Could not load CodexLens config, using defaults. Error: {e}", file=sys.stderr)
54
+ self._config = CodexLensConfig() # Use default config
55
+
56
+ # Lazy-load embedder to avoid ~0.8s model loading for status command
57
+ self._embedder = None
58
+ self._embedding_dim = None
59
+
60
+ @property
61
+ def embedding_dim(self) -> int:
62
+ """Get embedding dimension from the embedder."""
63
+ if self._embedding_dim is None:
64
+ # Access embedder to get its dimension
65
+ self._embedding_dim = self.embedder.embedding_dim
66
+ return self._embedding_dim
67
+
68
+ @property
69
+ def embedder(self):
70
+ """Lazy-load the embedder on first access using CodexLens config."""
71
+ if self._embedder is None:
72
+ # Use CodexLens configuration settings
73
+ backend = self._config.embedding_backend
74
+ model = self._config.embedding_model
75
+ use_gpu = self._config.embedding_use_gpu
76
+
77
+ # Use factory to create embedder based on backend type
78
+ if backend == "fastembed":
79
+ self._embedder = get_embedder_factory(
80
+ backend="fastembed",
81
+ profile=model,
82
+ use_gpu=use_gpu
83
+ )
84
+ elif backend == "litellm":
85
+ # For litellm backend, also pass endpoints if configured
86
+ endpoints = self._config.embedding_endpoints
87
+ strategy = self._config.embedding_strategy
88
+ cooldown = self._config.embedding_cooldown
89
+
90
+ self._embedder = get_embedder_factory(
91
+ backend="litellm",
92
+ model=model,
93
+ endpoints=endpoints if endpoints else None,
94
+ strategy=strategy,
95
+ cooldown=cooldown,
96
+ )
97
+ else:
98
+ # Fallback to fastembed with code profile
99
+ self._embedder = get_embedder_factory(
100
+ backend="fastembed",
101
+ profile="code",
102
+ use_gpu=True
103
+ )
104
+ return self._embedder
105
+
106
+ def close(self):
107
+ """Close database connection."""
108
+ if self.conn:
109
+ self.conn.close()
110
+
111
+ def embed_chunks(
112
+ self,
113
+ source_id: Optional[str] = None,
114
+ batch_size: int = 8,
115
+ force: bool = False
116
+ ) -> Dict[str, Any]:
117
+ """
118
+ Generate embeddings for unembedded chunks.
119
+
120
+ Args:
121
+ source_id: Only process chunks from this source
122
+ batch_size: Number of chunks to process in each batch
123
+ force: Re-embed chunks that already have embeddings
124
+
125
+ Returns:
126
+ Result dict with success, chunks_processed, chunks_failed, elapsed_time
127
+ """
128
+ start_time = time.time()
129
+
130
+ # Build query
131
+ query = "SELECT id, source_id, source_type, chunk_index, content FROM memory_chunks"
132
+ params = []
133
+
134
+ if force:
135
+ # Process all chunks (with optional source filter)
136
+ if source_id:
137
+ query += " WHERE source_id = ?"
138
+ params.append(source_id)
139
+ else:
140
+ # Only process chunks without embeddings
141
+ query += " WHERE embedding IS NULL"
142
+ if source_id:
143
+ query += " AND source_id = ?"
144
+ params.append(source_id)
145
+
146
+ query += " ORDER BY id"
147
+
148
+ cursor = self.conn.cursor()
149
+ cursor.execute(query, params)
150
+
151
+ chunks_processed = 0
152
+ chunks_failed = 0
153
+ batch = []
154
+ batch_ids = []
155
+
156
+ for row in cursor:
157
+ batch.append(row["content"])
158
+ batch_ids.append(row["id"])
159
+
160
+ # Process batch when full
161
+ if len(batch) >= batch_size:
162
+ processed, failed = self._process_batch(batch, batch_ids)
163
+ chunks_processed += processed
164
+ chunks_failed += failed
165
+ batch = []
166
+ batch_ids = []
167
+
168
+ # Process remaining chunks
169
+ if batch:
170
+ processed, failed = self._process_batch(batch, batch_ids)
171
+ chunks_processed += processed
172
+ chunks_failed += failed
173
+
174
+ elapsed_time = time.time() - start_time
175
+
176
+ return {
177
+ "success": chunks_failed == 0,
178
+ "chunks_processed": chunks_processed,
179
+ "chunks_failed": chunks_failed,
180
+ "elapsed_time": round(elapsed_time, 2)
181
+ }
182
+
183
+ def _process_batch(self, texts: List[str], ids: List[int]) -> Tuple[int, int]:
184
+ """Process a batch of texts and update embeddings."""
185
+ try:
186
+ # Generate embeddings for batch
187
+ embeddings = self.embedder.embed(texts)
188
+
189
+ processed = 0
190
+ failed = 0
191
+
192
+ # Update database
193
+ cursor = self.conn.cursor()
194
+ for chunk_id, embedding in zip(ids, embeddings):
195
+ try:
196
+ # Convert to numpy array and store as bytes
197
+ emb_array = np.array(embedding, dtype=np.float32)
198
+ emb_bytes = emb_array.tobytes()
199
+
200
+ cursor.execute(
201
+ "UPDATE memory_chunks SET embedding = ? WHERE id = ?",
202
+ (emb_bytes, chunk_id)
203
+ )
204
+ processed += 1
205
+ except Exception as e:
206
+ print(f"Error updating chunk {chunk_id}: {e}", file=sys.stderr)
207
+ failed += 1
208
+
209
+ self.conn.commit()
210
+ return processed, failed
211
+
212
+ except Exception as e:
213
+ print(f"Error processing batch: {e}", file=sys.stderr)
214
+ return 0, len(ids)
215
+
216
+ def search(
217
+ self,
218
+ query: str,
219
+ top_k: int = 10,
220
+ min_score: float = 0.3,
221
+ source_type: Optional[str] = None
222
+ ) -> Dict[str, Any]:
223
+ """
224
+ Perform semantic search on memory chunks.
225
+
226
+ Args:
227
+ query: Search query text
228
+ top_k: Number of results to return
229
+ min_score: Minimum similarity score (0-1)
230
+ source_type: Filter by source type (core_memory, workflow, cli_history)
231
+
232
+ Returns:
233
+ Result dict with success and matches list
234
+ """
235
+ try:
236
+ # Generate query embedding
237
+ query_embedding = self.embedder.embed_single(query)
238
+ query_array = np.array(query_embedding, dtype=np.float32)
239
+
240
+ # Build database query
241
+ sql = """
242
+ SELECT id, source_id, source_type, chunk_index, content, embedding
243
+ FROM memory_chunks
244
+ WHERE embedding IS NOT NULL
245
+ """
246
+ params = []
247
+
248
+ if source_type:
249
+ sql += " AND source_type = ?"
250
+ params.append(source_type)
251
+
252
+ cursor = self.conn.cursor()
253
+ cursor.execute(sql, params)
254
+
255
+ # Calculate similarities
256
+ matches = []
257
+ for row in cursor:
258
+ # Load embedding from bytes
259
+ emb_bytes = row["embedding"]
260
+ emb_array = np.frombuffer(emb_bytes, dtype=np.float32)
261
+
262
+ # Cosine similarity
263
+ score = float(
264
+ np.dot(query_array, emb_array) /
265
+ (np.linalg.norm(query_array) * np.linalg.norm(emb_array))
266
+ )
267
+
268
+ if score >= min_score:
269
+ # Generate restore command
270
+ restore_command = self._get_restore_command(
271
+ row["source_id"],
272
+ row["source_type"]
273
+ )
274
+
275
+ matches.append({
276
+ "source_id": row["source_id"],
277
+ "source_type": row["source_type"],
278
+ "chunk_index": row["chunk_index"],
279
+ "content": row["content"],
280
+ "score": round(score, 4),
281
+ "restore_command": restore_command
282
+ })
283
+
284
+ # Sort by score and limit
285
+ matches.sort(key=lambda x: x["score"], reverse=True)
286
+ matches = matches[:top_k]
287
+
288
+ return {
289
+ "success": True,
290
+ "matches": matches
291
+ }
292
+
293
+ except Exception as e:
294
+ return {
295
+ "success": False,
296
+ "error": str(e),
297
+ "matches": []
298
+ }
299
+
300
+ def _get_restore_command(self, source_id: str, source_type: str) -> str:
301
+ """Generate restore command for a source."""
302
+ if source_type in ("core_memory", "cli_history"):
303
+ return f"ccw memory export {source_id}"
304
+ elif source_type == "workflow":
305
+ return f"ccw session resume {source_id}"
306
+ else:
307
+ return f"# Unknown source type: {source_type}"
308
+
309
+ def get_status(self) -> Dict[str, Any]:
310
+ """Get embedding status statistics."""
311
+ cursor = self.conn.cursor()
312
+
313
+ # Total chunks
314
+ cursor.execute("SELECT COUNT(*) as count FROM memory_chunks")
315
+ total_chunks = cursor.fetchone()["count"]
316
+
317
+ # Embedded chunks
318
+ cursor.execute("SELECT COUNT(*) as count FROM memory_chunks WHERE embedding IS NOT NULL")
319
+ embedded_chunks = cursor.fetchone()["count"]
320
+
321
+ # By type
322
+ cursor.execute("""
323
+ SELECT
324
+ source_type,
325
+ COUNT(*) as total,
326
+ SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) as embedded
327
+ FROM memory_chunks
328
+ GROUP BY source_type
329
+ """)
330
+
331
+ by_type = {}
332
+ for row in cursor:
333
+ by_type[row["source_type"]] = {
334
+ "total": row["total"],
335
+ "embedded": row["embedded"],
336
+ "pending": row["total"] - row["embedded"]
337
+ }
338
+
339
+ return {
340
+ "total_chunks": total_chunks,
341
+ "embedded_chunks": embedded_chunks,
342
+ "pending_chunks": total_chunks - embedded_chunks,
343
+ "by_type": by_type
344
+ }
345
+
346
+
347
+ def main():
348
+ """Main entry point."""
349
+ parser = argparse.ArgumentParser(
350
+ description="Memory Embedder - Bridge CCW to CodexLens semantic search"
351
+ )
352
+
353
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
354
+ subparsers.required = True
355
+
356
+ # Embed command
357
+ embed_parser = subparsers.add_parser("embed", help="Generate embeddings for chunks")
358
+ embed_parser.add_argument("db_path", help="Path to SQLite database")
359
+ embed_parser.add_argument("--source-id", help="Only process chunks from this source")
360
+ embed_parser.add_argument("--batch-size", type=int, default=8, help="Batch size (default: 8)")
361
+ embed_parser.add_argument("--force", action="store_true", help="Re-embed existing chunks")
362
+
363
+ # Search command
364
+ search_parser = subparsers.add_parser("search", help="Semantic search")
365
+ search_parser.add_argument("db_path", help="Path to SQLite database")
366
+ search_parser.add_argument("query", help="Search query")
367
+ search_parser.add_argument("--top-k", type=int, default=10, help="Number of results (default: 10)")
368
+ search_parser.add_argument("--min-score", type=float, default=0.3, help="Minimum score (default: 0.3)")
369
+ search_parser.add_argument("--type", dest="source_type", help="Filter by source type")
370
+
371
+ # Status command
372
+ status_parser = subparsers.add_parser("status", help="Get embedding status")
373
+ status_parser.add_argument("db_path", help="Path to SQLite database")
374
+
375
+ args = parser.parse_args()
376
+
377
+ try:
378
+ embedder = MemoryEmbedder(args.db_path)
379
+
380
+ if args.command == "embed":
381
+ result = embedder.embed_chunks(
382
+ source_id=args.source_id,
383
+ batch_size=args.batch_size,
384
+ force=args.force
385
+ )
386
+ print(json.dumps(result, indent=2))
387
+
388
+ elif args.command == "search":
389
+ result = embedder.search(
390
+ query=args.query,
391
+ top_k=args.top_k,
392
+ min_score=args.min_score,
393
+ source_type=args.source_type
394
+ )
395
+ print(json.dumps(result, indent=2))
396
+
397
+ elif args.command == "status":
398
+ result = embedder.get_status()
399
+ print(json.dumps(result, indent=2))
400
+
401
+ embedder.close()
402
+
403
+ # Exit with error code if operation failed
404
+ if "success" in result and not result["success"]:
405
+ # Clean up ONNX resources before exit
406
+ clear_embedder_cache()
407
+ sys.exit(1)
408
+
409
+ # Clean up ONNX resources to ensure process can exit cleanly
410
+ # This releases fastembed/ONNX Runtime threads that would otherwise
411
+ # prevent the Python interpreter from shutting down
412
+ clear_embedder_cache()
413
+
414
+ except Exception as e:
415
+ # Clean up ONNX resources even on error
416
+ try:
417
+ clear_embedder_cache()
418
+ except Exception:
419
+ pass
420
+ print(json.dumps({
421
+ "success": False,
422
+ "error": str(e)
423
+ }, indent=2), file=sys.stderr)
424
+ sys.exit(1)
425
+
426
+
427
+ if __name__ == "__main__":
428
+ main()
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for memory_embedder.py
4
+
5
+ Creates a temporary database with test data and verifies all commands work.
6
+ """
7
+
8
+ import json
9
+ import sqlite3
10
+ import tempfile
11
+ import subprocess
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+
15
+
16
+ def create_test_database():
17
+ """Create a temporary database with test chunks."""
18
+ # Create temp file
19
+ temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
20
+ temp_db.close()
21
+
22
+ conn = sqlite3.connect(temp_db.name)
23
+ cursor = conn.cursor()
24
+
25
+ # Create schema
26
+ cursor.execute("""
27
+ CREATE TABLE memory_chunks (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ source_id TEXT NOT NULL,
30
+ source_type TEXT NOT NULL,
31
+ chunk_index INTEGER NOT NULL,
32
+ content TEXT NOT NULL,
33
+ embedding BLOB,
34
+ metadata TEXT,
35
+ created_at TEXT NOT NULL,
36
+ UNIQUE(source_id, chunk_index)
37
+ )
38
+ """)
39
+
40
+ # Insert test data
41
+ test_chunks = [
42
+ ("CMEM-20250101-001", "core_memory", 0, "Implemented authentication using JWT tokens with refresh mechanism"),
43
+ ("CMEM-20250101-001", "core_memory", 1, "Added rate limiting to API endpoints using Redis"),
44
+ ("WFS-20250101-auth", "workflow", 0, "Created login endpoint with password hashing"),
45
+ ("WFS-20250101-auth", "workflow", 1, "Implemented session management with token rotation"),
46
+ ("CLI-20250101-001", "cli_history", 0, "Executed database migration for user table"),
47
+ ]
48
+
49
+ now = datetime.now().isoformat()
50
+ for source_id, source_type, chunk_index, content in test_chunks:
51
+ cursor.execute(
52
+ """
53
+ INSERT INTO memory_chunks (source_id, source_type, chunk_index, content, created_at)
54
+ VALUES (?, ?, ?, ?, ?)
55
+ """,
56
+ (source_id, source_type, chunk_index, content, now)
57
+ )
58
+
59
+ conn.commit()
60
+ conn.close()
61
+
62
+ return temp_db.name
63
+
64
+
65
+ def run_command(args):
66
+ """Run memory_embedder.py with given arguments."""
67
+ script = Path(__file__).parent / "memory_embedder.py"
68
+ cmd = ["python", str(script)] + args
69
+
70
+ result = subprocess.run(
71
+ cmd,
72
+ capture_output=True,
73
+ text=True
74
+ )
75
+
76
+ return result.returncode, result.stdout, result.stderr
77
+
78
+
79
+ def test_status(db_path):
80
+ """Test status command."""
81
+ print("Testing status command...")
82
+ returncode, stdout, stderr = run_command(["status", db_path])
83
+
84
+ if returncode != 0:
85
+ print(f"[FAIL] Status failed: {stderr}")
86
+ return False
87
+
88
+ result = json.loads(stdout)
89
+ expected_total = 5
90
+
91
+ if result["total_chunks"] != expected_total:
92
+ print(f"[FAIL] Expected {expected_total} chunks, got {result['total_chunks']}")
93
+ return False
94
+
95
+ if result["embedded_chunks"] != 0:
96
+ print(f"[FAIL] Expected 0 embedded chunks, got {result['embedded_chunks']}")
97
+ return False
98
+
99
+ print(f"[PASS] Status OK: {result['total_chunks']} total, {result['embedded_chunks']} embedded")
100
+ return True
101
+
102
+
103
+ def test_embed(db_path):
104
+ """Test embed command."""
105
+ print("\nTesting embed command...")
106
+ returncode, stdout, stderr = run_command(["embed", db_path, "--batch-size", "2"])
107
+
108
+ if returncode != 0:
109
+ print(f"[FAIL] Embed failed: {stderr}")
110
+ return False
111
+
112
+ result = json.loads(stdout)
113
+
114
+ if not result["success"]:
115
+ print(f"[FAIL] Embed unsuccessful")
116
+ return False
117
+
118
+ if result["chunks_processed"] != 5:
119
+ print(f"[FAIL] Expected 5 processed, got {result['chunks_processed']}")
120
+ return False
121
+
122
+ if result["chunks_failed"] != 0:
123
+ print(f"[FAIL] Expected 0 failed, got {result['chunks_failed']}")
124
+ return False
125
+
126
+ print(f"[PASS] Embed OK: {result['chunks_processed']} processed in {result['elapsed_time']}s")
127
+ return True
128
+
129
+
130
+ def test_search(db_path):
131
+ """Test search command."""
132
+ print("\nTesting search command...")
133
+ returncode, stdout, stderr = run_command([
134
+ "search", db_path, "authentication JWT",
135
+ "--top-k", "3",
136
+ "--min-score", "0.3"
137
+ ])
138
+
139
+ if returncode != 0:
140
+ print(f"[FAIL] Search failed: {stderr}")
141
+ return False
142
+
143
+ result = json.loads(stdout)
144
+
145
+ if not result["success"]:
146
+ print(f"[FAIL] Search unsuccessful: {result.get('error', 'Unknown error')}")
147
+ return False
148
+
149
+ if len(result["matches"]) == 0:
150
+ print(f"[FAIL] Expected at least 1 match, got 0")
151
+ return False
152
+
153
+ print(f"[PASS] Search OK: {len(result['matches'])} matches found")
154
+
155
+ # Show top match
156
+ top_match = result["matches"][0]
157
+ print(f" Top match: {top_match['source_id']} (score: {top_match['score']})")
158
+ print(f" Content: {top_match['content'][:60]}...")
159
+
160
+ return True
161
+
162
+
163
+ def test_source_filter(db_path):
164
+ """Test search with source type filter."""
165
+ print("\nTesting source type filter...")
166
+ returncode, stdout, stderr = run_command([
167
+ "search", db_path, "authentication",
168
+ "--type", "workflow"
169
+ ])
170
+
171
+ if returncode != 0:
172
+ print(f"[FAIL] Filtered search failed: {stderr}")
173
+ return False
174
+
175
+ result = json.loads(stdout)
176
+
177
+ if not result["success"]:
178
+ print(f"[FAIL] Filtered search unsuccessful")
179
+ return False
180
+
181
+ # Verify all matches are workflow type
182
+ for match in result["matches"]:
183
+ if match["source_type"] != "workflow":
184
+ print(f"[FAIL] Expected workflow type, got {match['source_type']}")
185
+ return False
186
+
187
+ print(f"[PASS] Filter OK: {len(result['matches'])} workflow matches")
188
+ return True
189
+
190
+
191
+ def main():
192
+ """Run all tests."""
193
+ print("Memory Embedder Test Suite")
194
+ print("=" * 60)
195
+
196
+ # Create test database
197
+ print("\nCreating test database...")
198
+ db_path = create_test_database()
199
+ print(f"[PASS] Database created: {db_path}")
200
+
201
+ try:
202
+ # Run tests
203
+ tests = [
204
+ ("Status", test_status),
205
+ ("Embed", test_embed),
206
+ ("Search", test_search),
207
+ ("Source Filter", test_source_filter),
208
+ ]
209
+
210
+ passed = 0
211
+ failed = 0
212
+
213
+ for name, test_func in tests:
214
+ try:
215
+ if test_func(db_path):
216
+ passed += 1
217
+ else:
218
+ failed += 1
219
+ except Exception as e:
220
+ print(f"[FAIL] {name} crashed: {e}")
221
+ failed += 1
222
+
223
+ # Summary
224
+ print("\n" + "=" * 60)
225
+ print(f"Results: {passed} passed, {failed} failed")
226
+
227
+ if failed == 0:
228
+ print("[PASS] All tests passed!")
229
+ return 0
230
+ else:
231
+ print("[FAIL] Some tests failed")
232
+ return 1
233
+
234
+ finally:
235
+ # Cleanup
236
+ import os
237
+ try:
238
+ os.unlink(db_path)
239
+ print(f"\n[PASS] Cleaned up test database")
240
+ except:
241
+ pass
242
+
243
+
244
+ if __name__ == "__main__":
245
+ exit(main())
@@ -113,7 +113,9 @@ export async function csrfValidation(ctx: CsrfMiddlewareContext): Promise<boolea
113
113
  const { pathname, req, res } = ctx;
114
114
 
115
115
  if (!pathname.startsWith('/api/')) return true;
116
- if (envFlagEnabled('CCW_DISABLE_CSRF')) return true;
116
+ // CSRF is disabled by default for local deployment scenarios.
117
+ // Set CCW_ENABLE_CSRF=1 to enable CSRF protection.
118
+ if (!envFlagEnabled('CCW_ENABLE_CSRF')) return true;
117
119
 
118
120
  const method = (req.method || 'GET').toUpperCase();
119
121
  if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;