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.
- package/.claude/CLAUDE.md +7 -1
- package/.claude/agents/action-planning-agent.md +1 -0
- package/.claude/agents/cli-discuss-agent.md +391 -0
- package/.claude/agents/cli-execution-agent.md +2 -0
- package/.claude/agents/cli-explore-agent.md +2 -1
- package/.claude/agents/cli-lite-planning-agent.md +1 -0
- package/.claude/agents/cli-planning-agent.md +1 -0
- package/.claude/agents/code-developer.md +1 -0
- package/.claude/agents/conceptual-planning-agent.md +2 -0
- package/.claude/agents/context-search-agent.md +1 -0
- package/.claude/agents/debug-explore-agent.md +2 -0
- package/.claude/agents/doc-generator.md +1 -0
- package/.claude/agents/issue-plan-agent.md +2 -1
- package/.claude/agents/issue-queue-agent.md +2 -1
- package/.claude/agents/memory-bridge.md +2 -0
- package/.claude/agents/test-context-search-agent.md +2 -0
- package/.claude/agents/test-fix-agent.md +1 -0
- package/.claude/agents/ui-design-agent.md +2 -0
- package/.claude/agents/universal-executor.md +1 -0
- package/.claude/commands/issue/execute.md +141 -163
- package/.claude/commands/workflow/lite-lite-lite.md +798 -0
- package/.claude/commands/workflow/multi-cli-plan.md +510 -0
- package/.claude/skills/ccw/SKILL.md +262 -372
- package/.claude/skills/ccw/command.json +547 -0
- package/.claude/skills/ccw-help/SKILL.md +46 -107
- package/.claude/skills/ccw-help/command.json +511 -0
- package/.claude/skills/skill-tuning/SKILL.md +303 -0
- package/.claude/skills/skill-tuning/phases/actions/action-abort.md +164 -0
- package/.claude/skills/skill-tuning/phases/actions/action-analyze-requirements.md +406 -0
- package/.claude/skills/skill-tuning/phases/actions/action-apply-fix.md +206 -0
- package/.claude/skills/skill-tuning/phases/actions/action-complete.md +195 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-agent.md +317 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-context.md +243 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-dataflow.md +318 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-docs.md +299 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-memory.md +269 -0
- package/.claude/skills/skill-tuning/phases/actions/action-diagnose-token-consumption.md +200 -0
- package/.claude/skills/skill-tuning/phases/actions/action-gemini-analysis.md +322 -0
- package/.claude/skills/skill-tuning/phases/actions/action-generate-report.md +228 -0
- package/.claude/skills/skill-tuning/phases/actions/action-init.md +149 -0
- package/.claude/skills/skill-tuning/phases/actions/action-propose-fixes.md +317 -0
- package/.claude/skills/skill-tuning/phases/actions/action-verify.md +222 -0
- package/.claude/skills/skill-tuning/phases/orchestrator.md +377 -0
- package/.claude/skills/skill-tuning/phases/state-schema.md +378 -0
- package/.claude/skills/skill-tuning/specs/category-mappings.json +284 -0
- package/.claude/skills/skill-tuning/specs/dimension-mapping.md +212 -0
- package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +318 -0
- package/.claude/skills/skill-tuning/specs/quality-gates.md +263 -0
- package/.claude/skills/skill-tuning/specs/skill-authoring-principles.md +189 -0
- package/.claude/skills/skill-tuning/specs/tuning-strategies.md +1537 -0
- package/.claude/skills/skill-tuning/templates/diagnosis-report.md +153 -0
- package/.claude/skills/skill-tuning/templates/fix-proposal.md +204 -0
- package/.claude/workflows/cli-templates/schemas/multi-cli-discussion-schema.json +421 -0
- package/.claude/workflows/cli-tools-usage.md +0 -41
- package/ccw/dist/core/auth/csrf-middleware.d.ts.map +1 -1
- package/ccw/dist/core/auth/csrf-middleware.js +3 -1
- package/ccw/dist/core/auth/csrf-middleware.js.map +1 -1
- package/ccw/dist/core/data-aggregator.d.ts +2 -0
- package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
- package/ccw/dist/core/data-aggregator.js +5 -2
- package/ccw/dist/core/data-aggregator.js.map +1 -1
- package/ccw/dist/core/lite-scanner.d.ts +2 -1
- package/ccw/dist/core/lite-scanner.d.ts.map +1 -1
- package/ccw/dist/core/lite-scanner.js +295 -6
- package/ccw/dist/core/lite-scanner.js.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.js +5 -5
- package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
- package/ccw/dist/core/routes/session-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/session-routes.js +166 -48
- package/ccw/dist/core/routes/session-routes.js.map +1 -1
- package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/system-routes.js +87 -0
- package/ccw/dist/core/routes/system-routes.js.map +1 -1
- package/ccw/dist/core/server.js +2 -2
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/scripts/IMPLEMENTATION-SUMMARY.md +226 -0
- package/ccw/scripts/QUICK-REFERENCE.md +135 -0
- package/ccw/scripts/README-memory-embedder.md +157 -0
- package/ccw/scripts/__pycache__/memory_embedder.cpython-313.pyc +0 -0
- package/ccw/scripts/__pycache__/test_memory_embedder.cpython-313-pytest-8.4.2.pyc +0 -0
- package/ccw/scripts/memory-embedder-example.ts +184 -0
- package/ccw/scripts/memory_embedder.py +428 -0
- package/ccw/scripts/test_memory_embedder.py +245 -0
- package/ccw/src/core/auth/csrf-middleware.ts +3 -1
- package/ccw/src/core/data-aggregator.ts +7 -2
- package/ccw/src/core/lite-scanner.ts +440 -6
- package/ccw/src/core/routes/codexlens/config-handlers.ts +12 -9
- package/ccw/src/core/routes/session-routes.ts +201 -48
- package/ccw/src/core/routes/system-routes.ts +102 -0
- package/ccw/src/core/server.ts +2 -2
- package/ccw/src/templates/dashboard-css/01-base.css +8 -0
- package/ccw/src/templates/dashboard-css/02-session.css +81 -0
- package/ccw/src/templates/dashboard-css/04-lite-tasks.css +2442 -0
- package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +157 -0
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +23 -0
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +38 -4
- package/ccw/src/templates/dashboard-js/components/hook-manager.js +38 -13
- package/ccw/src/templates/dashboard-js/components/navigation.js +24 -4
- package/ccw/src/templates/dashboard-js/i18n.js +194 -6
- package/ccw/src/templates/dashboard-js/views/api-settings.js +32 -0
- package/ccw/src/templates/dashboard-js/views/claude-manager.js +44 -3
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +303 -31
- package/ccw/src/templates/dashboard-js/views/history.js +44 -6
- package/ccw/src/templates/dashboard-js/views/home.js +1 -0
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +54 -7
- package/ccw/src/templates/dashboard-js/views/lite-tasks.js +1817 -4
- package/ccw/src/templates/dashboard.html +5 -0
- package/package.json +2 -1
- package/.claude/skills/ccw/index/command-capabilities.json +0 -127
- package/.claude/skills/ccw/index/intent-rules.json +0 -136
- package/.claude/skills/ccw/index/workflow-chains.json +0 -451
- package/.claude/skills/ccw/phases/actions/bugfix.md +0 -218
- package/.claude/skills/ccw/phases/actions/coupled.md +0 -194
- package/.claude/skills/ccw/phases/actions/docs.md +0 -93
- package/.claude/skills/ccw/phases/actions/full.md +0 -154
- package/.claude/skills/ccw/phases/actions/issue.md +0 -201
- package/.claude/skills/ccw/phases/actions/rapid.md +0 -104
- package/.claude/skills/ccw/phases/actions/review-fix.md +0 -84
- package/.claude/skills/ccw/phases/actions/tdd.md +0 -66
- package/.claude/skills/ccw/phases/actions/ui.md +0 -79
- package/.claude/skills/ccw/phases/orchestrator.md +0 -435
- package/.claude/skills/ccw/specs/intent-classification.md +0 -336
- package/.claude/skills/ccw-help/index/all-agents.json +0 -82
- package/.claude/skills/ccw-help/index/all-commands.json +0 -882
- package/.claude/skills/ccw-help/index/by-category.json +0 -914
- package/.claude/skills/ccw-help/index/by-use-case.json +0 -896
- package/.claude/skills/ccw-help/index/command-relationships.json +0 -160
- 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
|
-
|
|
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;
|