claude-memory-agent 3.0.3 → 3.2.0
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/bin/lib/installer.js +3 -1
- package/bin/lib/steps/advanced.js +1 -1
- package/config.py +10 -2
- package/dashboard.html +88 -38
- package/hooks/auto-detect-response.py +3 -6
- package/hooks/detect-correction.py +8 -7
- package/hooks/extract_memories.py +104 -0
- package/hooks/grounding-hook-v2.py +169 -33
- package/hooks/grounding-hook.py +19 -3
- package/hooks/log-tool-use.py +3 -6
- package/hooks/log-user-request.py +14 -6
- package/hooks/pre-tool-decision.py +3 -6
- package/hooks/pre_compact_hook.py +269 -5
- package/hooks/session_end_hook.py +37 -0
- package/hooks/session_start.py +10 -0
- package/hooks/stop_hook.py +315 -14
- package/install.py +141 -46
- package/main.py +522 -13
- package/mcp_proxy.py +93 -6
- package/package.json +2 -2
- package/services/agent_registry.py +260 -12
- package/services/database.py +453 -1
- package/services/embeddings.py +1 -1
- package/services/retry_queue.py +5 -1
- package/services/soul.py +791 -0
- package/services/vector_index.py +5 -1
- package/update_system.py +34 -8
package/bin/lib/installer.js
CHANGED
|
@@ -58,7 +58,9 @@ function runSync(cmd, cwd) {
|
|
|
58
58
|
*/
|
|
59
59
|
function buildEnvContent(config, agentDir) {
|
|
60
60
|
const timestamp = new Date().toISOString();
|
|
61
|
-
const
|
|
61
|
+
const homedir = require('os').homedir();
|
|
62
|
+
const defaultDbPath = path.join(homedir, '.claude-memory', 'memories.db').replace(/\\/g, '/');
|
|
63
|
+
const dbPath = config.dbPath || defaultDbPath;
|
|
62
64
|
const memoryUrl = 'http://' + (config.host === '0.0.0.0' ? 'localhost' : config.host) + ':' + config.port;
|
|
63
65
|
|
|
64
66
|
const lines = [
|
package/config.py
CHANGED
|
@@ -21,6 +21,9 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
AGENT_DIR = Path(__file__).parent.resolve()
|
|
22
22
|
load_dotenv(AGENT_DIR / ".env")
|
|
23
23
|
|
|
24
|
+
# User data directory — safe from code updates (zip, git pull, npm update)
|
|
25
|
+
USER_DATA_DIR = Path(os.getenv("USER_DATA_DIR", str(Path.home() / ".claude-memory")))
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
class Config:
|
|
26
29
|
"""Configuration singleton with environment variable loading."""
|
|
@@ -30,11 +33,15 @@ class Config:
|
|
|
30
33
|
self.AGENT_DIR = AGENT_DIR
|
|
31
34
|
self.DATABASE_PATH = Path(os.getenv(
|
|
32
35
|
"DATABASE_PATH",
|
|
33
|
-
str(
|
|
36
|
+
str(USER_DATA_DIR / "memories.db")
|
|
34
37
|
))
|
|
35
38
|
self.INDEX_DIR = Path(os.getenv(
|
|
36
39
|
"INDEX_DIR",
|
|
37
|
-
str(
|
|
40
|
+
str(USER_DATA_DIR / "indexes")
|
|
41
|
+
))
|
|
42
|
+
self.QUEUE_DB_PATH = Path(os.getenv(
|
|
43
|
+
"QUEUE_DB_PATH",
|
|
44
|
+
str(USER_DATA_DIR / "queue.db")
|
|
38
45
|
))
|
|
39
46
|
self.LOG_FILE = AGENT_DIR / "memory-agent.log"
|
|
40
47
|
self.LOCK_FILE = AGENT_DIR / "memory-agent.lock"
|
|
@@ -207,3 +214,4 @@ OLLAMA_HOST = config.OLLAMA_HOST
|
|
|
207
214
|
EMBEDDING_PROVIDER = config.EMBEDDING_PROVIDER
|
|
208
215
|
EMBEDDING_MODEL = config.EMBEDDING_MODEL
|
|
209
216
|
DATABASE_PATH = config.DATABASE_PATH
|
|
217
|
+
QUEUE_DB_PATH = config.QUEUE_DB_PATH
|
package/dashboard.html
CHANGED
|
@@ -466,6 +466,31 @@
|
|
|
466
466
|
top: 0; left: 0; right: 0;
|
|
467
467
|
height: 3px;
|
|
468
468
|
}
|
|
469
|
+
.agent-badges {
|
|
470
|
+
display: flex;
|
|
471
|
+
flex-wrap: wrap;
|
|
472
|
+
gap: 6px;
|
|
473
|
+
margin-bottom: 12px;
|
|
474
|
+
}
|
|
475
|
+
.agent-badge {
|
|
476
|
+
padding: 3px 10px;
|
|
477
|
+
border-radius: 12px;
|
|
478
|
+
font-size: 11px;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
letter-spacing: 0.3px;
|
|
481
|
+
}
|
|
482
|
+
.agent-badge.scope-global {
|
|
483
|
+
background: rgba(88, 166, 255, 0.15);
|
|
484
|
+
color: #58a6ff;
|
|
485
|
+
}
|
|
486
|
+
.agent-badge.scope-project {
|
|
487
|
+
background: rgba(63, 185, 80, 0.15);
|
|
488
|
+
color: #3fb950;
|
|
489
|
+
}
|
|
490
|
+
.agent-badge.category-badge {
|
|
491
|
+
background: var(--bg-tertiary);
|
|
492
|
+
color: var(--text-secondary);
|
|
493
|
+
}
|
|
469
494
|
/* Config Cards */
|
|
470
495
|
.config-card {
|
|
471
496
|
background: var(--bg-card);
|
|
@@ -2205,6 +2230,10 @@
|
|
|
2205
2230
|
|
|
2206
2231
|
<!-- Agents Tab -->
|
|
2207
2232
|
<div class="tab-content active" id="agents-tab">
|
|
2233
|
+
<div style="background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.2); border-radius: 12px; padding: 12px 20px; margin-bottom: 20px; display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--text-secondary);">
|
|
2234
|
+
<i class="fas fa-info-circle" style="color: #58a6ff; font-size: 16px;"></i>
|
|
2235
|
+
<span>Agents discovered from <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">~/.claude/agents/</code>. Toggle moves files between <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">agents/</code> and <code style="background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px;">agents/_disabled/</code>. Global agents are available across all projects.</span>
|
|
2236
|
+
</div>
|
|
2208
2237
|
<div class="section-header">
|
|
2209
2238
|
<div class="search-bar">
|
|
2210
2239
|
<i class="fas fa-search"></i>
|
|
@@ -3056,8 +3085,9 @@
|
|
|
3056
3085
|
|
|
3057
3086
|
async function loadAgentData() {
|
|
3058
3087
|
try {
|
|
3088
|
+
const agentParams = currentProject ? `?project_path=${encodeURIComponent(currentProject)}` : '';
|
|
3059
3089
|
const [agentsRes, mcpsRes, hooksRes] = await Promise.all([
|
|
3060
|
-
fetch(`${API_URL}/api/agents`),
|
|
3090
|
+
fetch(`${API_URL}/api/agents${agentParams}`),
|
|
3061
3091
|
fetch(`${API_URL}/api/mcps`),
|
|
3062
3092
|
fetch(`${API_URL}/api/hooks`)
|
|
3063
3093
|
]);
|
|
@@ -3071,12 +3101,14 @@
|
|
|
3071
3101
|
document.getElementById('agentsTabCount').textContent = allAgents.length;
|
|
3072
3102
|
document.getElementById('mcpsTabCount').textContent = allMcps.length;
|
|
3073
3103
|
document.getElementById('hooksTabCount').textContent = allHooks.length;
|
|
3104
|
+
// Update agent stats from discovery
|
|
3105
|
+
const enabledCount = allAgents.filter(a => a.enabled).length;
|
|
3106
|
+
document.getElementById('enabledAgentsCount').textContent = enabledCount;
|
|
3107
|
+
document.getElementById('agentsTotalCount').textContent = `/ ${allAgents.length} total`;
|
|
3074
3108
|
renderCategoryFilter();
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
renderHooks();
|
|
3079
|
-
}
|
|
3109
|
+
renderAgents();
|
|
3110
|
+
renderMcps();
|
|
3111
|
+
renderHooks();
|
|
3080
3112
|
} catch (e) {
|
|
3081
3113
|
console.error('Error loading agent data:', e);
|
|
3082
3114
|
}
|
|
@@ -3107,21 +3139,26 @@
|
|
|
3107
3139
|
// Rendering
|
|
3108
3140
|
function renderCategoryFilter() {
|
|
3109
3141
|
const container = document.getElementById('categoryFilter');
|
|
3110
|
-
|
|
3142
|
+
const allCount = allAgents.length;
|
|
3143
|
+
container.innerHTML = `<div class="category-chip active" data-category="all" onclick="filterByCategory('all')"><i class="fas fa-th"></i> All (${allCount})</div>`;
|
|
3144
|
+
// Count agents per category
|
|
3145
|
+
const catCounts = {};
|
|
3146
|
+
allAgents.forEach(a => { catCounts[a.category] = (catCounts[a.category] || 0) + 1; });
|
|
3111
3147
|
Object.entries(categories).forEach(([key, cat]) => {
|
|
3148
|
+
const count = catCounts[key] || 0;
|
|
3112
3149
|
const chip = document.createElement('div');
|
|
3113
3150
|
chip.className = 'category-chip';
|
|
3114
3151
|
chip.dataset.category = key;
|
|
3115
|
-
chip.style.setProperty('--chip-color', cat.color);
|
|
3152
|
+
chip.style.setProperty('--chip-color', cat.color || '#8b949e');
|
|
3116
3153
|
chip.onclick = () => filterByCategory(key);
|
|
3117
|
-
chip.innerHTML = `<i class="fas fa-${getIconForCategory(cat.icon)}"></i> ${cat.name}`;
|
|
3154
|
+
chip.innerHTML = `<i class="fas fa-${getIconForCategory(cat.icon)}"></i> ${cat.name} (${count})`;
|
|
3118
3155
|
container.appendChild(chip);
|
|
3119
3156
|
});
|
|
3120
3157
|
}
|
|
3121
3158
|
|
|
3122
3159
|
function getIconForCategory(icon) {
|
|
3123
|
-
const iconMap = { 'code': 'code', 'shield-check': 'shield-alt', 'palette': 'palette', 'lightbulb': 'lightbulb', 'megaphone': 'bullhorn', 'settings': 'cog', 'chart-bar': 'chart-bar', 'cube': 'cube', 'search': 'search' };
|
|
3124
|
-
return iconMap[icon] || 'circle';
|
|
3160
|
+
const iconMap = { 'code': 'code', 'shield-check': 'shield-alt', 'palette': 'palette', 'lightbulb': 'lightbulb', 'megaphone': 'bullhorn', 'settings': 'cog', 'chart-bar': 'chart-bar', 'cube': 'cube', 'search': 'search', 'tasks': 'tasks', 'star': 'star', 'headset': 'headset', 'vial': 'vial', 'bullhorn': 'bullhorn', 'circle': 'circle' };
|
|
3161
|
+
return iconMap[icon] || icon || 'circle';
|
|
3125
3162
|
}
|
|
3126
3163
|
|
|
3127
3164
|
function renderAgents() {
|
|
@@ -3129,25 +3166,31 @@
|
|
|
3129
3166
|
const searchTerm = document.getElementById('agentSearch').value.toLowerCase();
|
|
3130
3167
|
let filteredAgents = allAgents;
|
|
3131
3168
|
if (activeCategory !== 'all') filteredAgents = filteredAgents.filter(a => a.category === activeCategory);
|
|
3132
|
-
if (searchTerm) filteredAgents = filteredAgents.filter(a => a.name.toLowerCase().includes(searchTerm) || a.description.toLowerCase().includes(searchTerm) || a.
|
|
3169
|
+
if (searchTerm) filteredAgents = filteredAgents.filter(a => a.name.toLowerCase().includes(searchTerm) || (a.description || '').toLowerCase().includes(searchTerm) || a.category.includes(searchTerm));
|
|
3133
3170
|
if (!filteredAgents.length) {
|
|
3134
|
-
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-secondary);">No agents found</div>';
|
|
3171
|
+
grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-secondary);"><i class="fas fa-robot" style="font-size: 48px; margin-bottom: 16px; display: block; opacity: 0.3;"></i>No agents found</div>';
|
|
3135
3172
|
return;
|
|
3136
3173
|
}
|
|
3137
3174
|
grid.innerHTML = filteredAgents.map(agent => {
|
|
3138
|
-
const
|
|
3139
|
-
const
|
|
3140
|
-
const
|
|
3175
|
+
const isEnabled = agent.enabled;
|
|
3176
|
+
const catMeta = categories[agent.category] || {};
|
|
3177
|
+
const catColor = catMeta.color || agent.color || '#8b949e';
|
|
3178
|
+
const scopeClass = agent.scope === 'global' ? 'scope-global' : 'scope-project';
|
|
3179
|
+
const scopeLabel = agent.scope === 'global' ? 'Global' : 'Project';
|
|
3180
|
+
const catLabel = (catMeta.name || agent.category || '').replace(/-/g, ' ');
|
|
3141
3181
|
return `
|
|
3142
3182
|
<div class="agent-card ${isEnabled ? '' : 'disabled'}">
|
|
3143
|
-
<div class="agent-category-label" style="background: ${
|
|
3183
|
+
<div class="agent-category-label" style="background: ${catColor}"></div>
|
|
3144
3184
|
<div class="agent-card-header">
|
|
3145
|
-
<div class="agent-icon" style="background: ${
|
|
3185
|
+
<div class="agent-icon" style="background: ${catColor}20; color: ${catColor}"><i class="fas fa-${getIconForCategory(catMeta.icon)}"></i></div>
|
|
3146
3186
|
<div class="agent-toggle ${isEnabled ? 'active' : ''}" onclick="toggleAgent('${agent.id}', ${!isEnabled})"></div>
|
|
3147
3187
|
</div>
|
|
3148
3188
|
<div class="agent-name">${agent.name}</div>
|
|
3149
|
-
<div class="agent-description">${agent.description}</div>
|
|
3150
|
-
<div class="agent-
|
|
3189
|
+
<div class="agent-description">${agent.description || 'No description'}</div>
|
|
3190
|
+
<div class="agent-badges">
|
|
3191
|
+
<span class="agent-badge ${scopeClass}">${scopeLabel}</span>
|
|
3192
|
+
<span class="agent-badge category-badge">${catLabel}</span>
|
|
3193
|
+
</div>
|
|
3151
3194
|
</div>
|
|
3152
3195
|
`;
|
|
3153
3196
|
}).join('');
|
|
@@ -3197,18 +3240,18 @@
|
|
|
3197
3240
|
|
|
3198
3241
|
// Toggle actions
|
|
3199
3242
|
async function toggleAgent(agentId, enabled) {
|
|
3200
|
-
if (!currentProject) return;
|
|
3201
3243
|
try {
|
|
3202
|
-
const response = await fetch(`${API_URL}/api/
|
|
3244
|
+
const response = await fetch(`${API_URL}/api/agents/${agentId}/toggle`, {
|
|
3203
3245
|
method: 'POST',
|
|
3204
3246
|
headers: { 'Content-Type': 'application/json' },
|
|
3205
|
-
body: JSON.stringify({ enabled })
|
|
3247
|
+
body: JSON.stringify({ enabled, project_path: currentProject || undefined })
|
|
3206
3248
|
});
|
|
3207
3249
|
const result = await response.json();
|
|
3208
3250
|
if (result.success) {
|
|
3209
|
-
await
|
|
3210
|
-
renderAgents();
|
|
3251
|
+
await loadAgentData();
|
|
3211
3252
|
showToast(`${agentId} ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
|
3253
|
+
} else {
|
|
3254
|
+
showToast(result.error || 'Failed to toggle agent', 'error');
|
|
3212
3255
|
}
|
|
3213
3256
|
} catch (e) {
|
|
3214
3257
|
showToast('Failed to update agent', 'error');
|
|
@@ -3254,21 +3297,28 @@
|
|
|
3254
3297
|
}
|
|
3255
3298
|
|
|
3256
3299
|
async function enableAllAgents(enabled) {
|
|
3257
|
-
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3300
|
+
// Filter to only agents that need toggling
|
|
3301
|
+
const toToggle = allAgents.filter(a => a.enabled !== enabled);
|
|
3302
|
+
if (!toToggle.length) {
|
|
3303
|
+
showToast(`All agents already ${enabled ? 'enabled' : 'disabled'}`, 'info');
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3260
3306
|
try {
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3307
|
+
let succeeded = 0;
|
|
3308
|
+
// Toggle in batches of 5 for reasonable concurrency
|
|
3309
|
+
for (let i = 0; i < toToggle.length; i += 5) {
|
|
3310
|
+
const batch = toToggle.slice(i, i + 5);
|
|
3311
|
+
const results = await Promise.all(batch.map(agent =>
|
|
3312
|
+
fetch(`${API_URL}/api/agents/${agent.id}/toggle`, {
|
|
3313
|
+
method: 'POST',
|
|
3314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3315
|
+
body: JSON.stringify({ enabled, project_path: currentProject || undefined })
|
|
3316
|
+
}).then(r => r.json())
|
|
3317
|
+
));
|
|
3318
|
+
succeeded += results.filter(r => r.success).length;
|
|
3271
3319
|
}
|
|
3320
|
+
await loadAgentData();
|
|
3321
|
+
showToast(`${succeeded} agents ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
|
3272
3322
|
} catch (e) {
|
|
3273
3323
|
showToast('Failed to update agents', 'error');
|
|
3274
3324
|
}
|
|
@@ -261,12 +261,9 @@ def main():
|
|
|
261
261
|
if not response_text or len(response_text) < 50:
|
|
262
262
|
sys.exit(0)
|
|
263
263
|
|
|
264
|
-
# Load session data
|
|
265
|
-
session_data = load_session_data()
|
|
266
|
-
|
|
267
|
-
sys.exit(0)
|
|
268
|
-
|
|
269
|
-
session_id = session_data.get("session_id")
|
|
264
|
+
# Load session data, prefer session_id from stdin
|
|
265
|
+
session_data = load_session_data() or {}
|
|
266
|
+
session_id = hook_input.get("session_id") or session_data.get("session_id")
|
|
270
267
|
if not session_id:
|
|
271
268
|
sys.exit(0)
|
|
272
269
|
|
|
@@ -110,8 +110,8 @@ def main():
|
|
|
110
110
|
except (json.JSONDecodeError, ValueError, EOFError):
|
|
111
111
|
sys.exit(0)
|
|
112
112
|
|
|
113
|
-
# Get user message
|
|
114
|
-
user_message = hook_input.get("user_prompt", "")
|
|
113
|
+
# Get user message (Claude Code sends "prompt", legacy uses "user_prompt")
|
|
114
|
+
user_message = hook_input.get("prompt", "") or hook_input.get("user_prompt", "")
|
|
115
115
|
if not user_message:
|
|
116
116
|
session_messages = hook_input.get("session_messages", [])
|
|
117
117
|
if session_messages:
|
|
@@ -128,14 +128,15 @@ def main():
|
|
|
128
128
|
if not is_correction:
|
|
129
129
|
sys.exit(0)
|
|
130
130
|
|
|
131
|
-
# Load session data
|
|
132
|
-
session_data = load_session_data()
|
|
133
|
-
|
|
131
|
+
# Load session data, prefer session_id from stdin
|
|
132
|
+
session_data = load_session_data() or {}
|
|
133
|
+
stdin_session_id = hook_input.get("session_id")
|
|
134
|
+
if stdin_session_id and session_data.get("session_id") != stdin_session_id:
|
|
135
|
+
session_data["session_id"] = stdin_session_id
|
|
136
|
+
if not session_data.get("session_id"):
|
|
134
137
|
sys.exit(0)
|
|
135
138
|
|
|
136
139
|
session_id = session_data.get("session_id")
|
|
137
|
-
if not session_id:
|
|
138
|
-
sys.exit(0)
|
|
139
140
|
|
|
140
141
|
# Get the current request ID for causal chain linking
|
|
141
142
|
root_event_id = session_data.get("current_request_id")
|
|
@@ -66,10 +66,23 @@ PATTERN_PATTERNS = [
|
|
|
66
66
|
re.compile(r"(?:^|\n)\s*(?:Always|Never|Should always|Should never|Must always|Must never) (.*?)(?:\.|$)", re.IGNORECASE | re.MULTILINE),
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
+
# Workflow/procedure patterns
|
|
70
|
+
WORKFLOW_PATTERNS = [
|
|
71
|
+
# "To build X, run Y" / "To deploy X, do Y"
|
|
72
|
+
re.compile(r"(?:^|\n)\s*(?:To|to) (\w[\w\s]{3,30}),\s*(?:run|do|use|execute|type) (.{10,}?)(?:\n|$)", re.IGNORECASE | re.MULTILINE),
|
|
73
|
+
# "learned how to..."
|
|
74
|
+
re.compile(r"(?:^|\n)\s*(?:I learned|We learned|learned how to|figured out how to) (.{20,}?)(?:\.|$)", re.IGNORECASE | re.MULTILINE),
|
|
75
|
+
# Step-by-step: "1. ...\n2. ...\n3. ..."
|
|
76
|
+
re.compile(r"(?:^|\n)\s*1[.)]\s+(.+)\n\s*2[.)]\s+(.+)\n\s*3[.)]\s+(.+)", re.MULTILINE),
|
|
77
|
+
# "The workflow is..." / "The process is..."
|
|
78
|
+
re.compile(r"(?:^|\n)\s*(?:The workflow|The process|The procedure|Steps to) (?:is|are|for)[:\s]+(.*?)(?:\n\n|\Z)", re.IGNORECASE | re.DOTALL),
|
|
79
|
+
]
|
|
80
|
+
|
|
69
81
|
# Broader keyword triggers (used for line-level scanning)
|
|
70
82
|
DECISION_KEYWORDS = {"decided", "let's use", "going with", "chose", "choosing", "will use", "the plan is", "approach is", "strategy is", "i'll implement", "we'll implement"}
|
|
71
83
|
ERROR_KEYWORDS = {"error", "bug", "fix", "issue", "traceback", "exception", "failed", "failure", "broken", "crash", "root cause"}
|
|
72
84
|
PATTERN_KEYWORDS = {"pattern", "approach", "architecture", "convention", "best practice", "always", "never", "rule"}
|
|
85
|
+
WORKFLOW_KEYWORDS = {"workflow", "procedure", "steps to", "how to", "process for", "pipeline", "build steps", "deploy steps"}
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
# ---------------------------------------------------------------------------
|
|
@@ -224,6 +237,13 @@ def extract_from_text(text: str, existing_hashes: set) -> List[Dict[str, Any]]:
|
|
|
224
237
|
if len(context) > 30:
|
|
225
238
|
add_extraction(context, "code", 6, ["pattern"])
|
|
226
239
|
|
|
240
|
+
# Workflows / Procedures
|
|
241
|
+
for pattern in WORKFLOW_PATTERNS:
|
|
242
|
+
for match in pattern.finditer(text):
|
|
243
|
+
context = extract_context_around(text, match.start(), match.end(), context_chars=300)
|
|
244
|
+
if len(context) > 40:
|
|
245
|
+
add_extraction(context, "code", 7, ["workflow", "procedure"])
|
|
246
|
+
|
|
227
247
|
# --- Line-level keyword scanning (fallback for cases regex misses) ---
|
|
228
248
|
# Only do this if we have not yet hit our cap
|
|
229
249
|
if len(extractions) < MAX_MEMORIES_PER_RUN:
|
|
@@ -256,6 +276,12 @@ def extract_from_text(text: str, existing_hashes: set) -> List[Dict[str, Any]]:
|
|
|
256
276
|
if len(block) > 30:
|
|
257
277
|
add_extraction(block, "code", 5, ["pattern", "keyword-match"])
|
|
258
278
|
|
|
279
|
+
# Check for workflow keywords
|
|
280
|
+
elif any(kw in line_lower for kw in WORKFLOW_KEYWORDS):
|
|
281
|
+
block = '\n'.join(lines[i:i+5]).strip() # Wider context for workflows
|
|
282
|
+
if len(block) > 40:
|
|
283
|
+
add_extraction(block, "code", 6, ["workflow", "keyword-match"])
|
|
284
|
+
|
|
259
285
|
i += 1
|
|
260
286
|
|
|
261
287
|
return extractions
|
|
@@ -313,6 +339,84 @@ def store_memory_sync(extraction: Dict[str, Any], project_path: Optional[str] =
|
|
|
313
339
|
return False
|
|
314
340
|
|
|
315
341
|
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Workflow / Bash command extraction from JSONL transcript
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
def extract_bash_commands(transcript_path: str, byte_offset: int = 0) -> List[str]:
|
|
347
|
+
"""Extract successful bash commands from JSONL transcript.
|
|
348
|
+
|
|
349
|
+
Looks for tool_use blocks with tool=Bash that were followed by success results.
|
|
350
|
+
Returns deduplicated command list.
|
|
351
|
+
"""
|
|
352
|
+
path = Path(transcript_path)
|
|
353
|
+
if not path.exists():
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
commands = []
|
|
357
|
+
seen = set()
|
|
358
|
+
try:
|
|
359
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
360
|
+
if byte_offset > 0:
|
|
361
|
+
f.seek(byte_offset)
|
|
362
|
+
f.readline() # skip partial line
|
|
363
|
+
for line in f:
|
|
364
|
+
line = line.strip()
|
|
365
|
+
if not line:
|
|
366
|
+
continue
|
|
367
|
+
try:
|
|
368
|
+
msg = json.loads(line)
|
|
369
|
+
content = msg.get("content", [])
|
|
370
|
+
if not isinstance(content, list):
|
|
371
|
+
continue
|
|
372
|
+
for part in content:
|
|
373
|
+
if not isinstance(part, dict):
|
|
374
|
+
continue
|
|
375
|
+
if part.get("type") == "tool_use" and part.get("name") == "Bash":
|
|
376
|
+
cmd = ""
|
|
377
|
+
inp = part.get("input", {})
|
|
378
|
+
if isinstance(inp, dict):
|
|
379
|
+
cmd = inp.get("command", "")
|
|
380
|
+
if cmd and len(cmd) > 5 and cmd not in seen:
|
|
381
|
+
# Skip trivial commands
|
|
382
|
+
if not cmd.strip().startswith(("ls", "pwd", "echo", "cat ")):
|
|
383
|
+
seen.add(cmd)
|
|
384
|
+
commands.append(cmd)
|
|
385
|
+
except (json.JSONDecodeError, TypeError):
|
|
386
|
+
continue
|
|
387
|
+
except OSError:
|
|
388
|
+
pass
|
|
389
|
+
return commands[-20:] # Keep last 20 commands
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def capture_workflow_sync(name: str, steps: List[str], commands: List[str],
|
|
393
|
+
project_path: Optional[str] = None) -> bool:
|
|
394
|
+
"""POST a captured workflow to /api/workflow/capture."""
|
|
395
|
+
import urllib.request
|
|
396
|
+
import urllib.error
|
|
397
|
+
|
|
398
|
+
payload = json.dumps({
|
|
399
|
+
"name": name,
|
|
400
|
+
"steps": steps,
|
|
401
|
+
"commands": commands,
|
|
402
|
+
"project_path": project_path or "",
|
|
403
|
+
}).encode("utf-8")
|
|
404
|
+
|
|
405
|
+
headers = {"Content-Type": "application/json"}
|
|
406
|
+
if API_KEY:
|
|
407
|
+
headers["X-Memory-Key"] = API_KEY
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
req = urllib.request.Request(
|
|
411
|
+
f"{MEMORY_AGENT_URL}/api/workflow/capture",
|
|
412
|
+
data=payload, headers=headers, method="POST",
|
|
413
|
+
)
|
|
414
|
+
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
415
|
+
return resp.status == 200
|
|
416
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError):
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
316
420
|
# ---------------------------------------------------------------------------
|
|
317
421
|
# Main entry point
|
|
318
422
|
# ---------------------------------------------------------------------------
|