@sparkleideas/browser 3.0.0-alpha.18
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/README.md +730 -0
- package/agents/architect.yaml +11 -0
- package/agents/coder.yaml +11 -0
- package/agents/reviewer.yaml +10 -0
- package/agents/security-architect.yaml +10 -0
- package/agents/tester.yaml +10 -0
- package/docker/Dockerfile +22 -0
- package/docker/docker-compose.yml +52 -0
- package/docker/test-fixtures/index.html +61 -0
- package/package.json +56 -0
- package/skills/browser/SKILL.md +204 -0
- package/src/agent/index.ts +35 -0
- package/src/application/browser-service.ts +570 -0
- package/src/domain/types.ts +324 -0
- package/src/index.ts +156 -0
- package/src/infrastructure/agent-browser-adapter.ts +654 -0
- package/src/infrastructure/hooks-integration.ts +170 -0
- package/src/infrastructure/memory-integration.ts +449 -0
- package/src/infrastructure/reasoningbank-adapter.ts +282 -0
- package/src/infrastructure/security-integration.ts +528 -0
- package/src/infrastructure/workflow-templates.ts +479 -0
- package/src/mcp-tools/browser-tools.ts +1210 -0
- package/src/mcp-tools/index.ts +6 -0
- package/src/skill/index.ts +24 -0
- package/tests/agent-browser-adapter.test.ts +328 -0
- package/tests/browser-service.test.ts +137 -0
- package/tests/e2e/browser-e2e.test.ts +175 -0
- package/tests/memory-integration.test.ts +277 -0
- package/tests/reasoningbank-adapter.test.ts +219 -0
- package/tests/security-integration.test.ts +194 -0
- package/tests/workflow-templates.test.ts +231 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - Hooks Integration
|
|
3
|
+
* pre-browse and post-browse hooks for claude-flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getReasoningBank } from './reasoningbank-adapter.js';
|
|
7
|
+
import type { BrowserTrajectory, ActionResult } from '../domain/types.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Hook Handlers
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface PreBrowseInput {
|
|
14
|
+
goal: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
context?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PreBrowseResult {
|
|
20
|
+
recommendedSteps: Array<{
|
|
21
|
+
action: string;
|
|
22
|
+
selector?: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
}>;
|
|
25
|
+
similarPatterns: number;
|
|
26
|
+
suggestedModel: 'haiku' | 'sonnet' | 'opus';
|
|
27
|
+
estimatedDuration: number;
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PostBrowseInput {
|
|
32
|
+
trajectoryId: string;
|
|
33
|
+
success: boolean;
|
|
34
|
+
verdict?: string;
|
|
35
|
+
duration: number;
|
|
36
|
+
stepsCompleted: number;
|
|
37
|
+
errors?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PostBrowseResult {
|
|
41
|
+
patternStored: boolean;
|
|
42
|
+
patternId?: string;
|
|
43
|
+
learnedFrom: boolean;
|
|
44
|
+
statsUpdated: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Pre-Browse Hook
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pre-browse hook - called before starting browser automation
|
|
53
|
+
* Returns recommendations based on learned patterns
|
|
54
|
+
*/
|
|
55
|
+
export async function preBrowseHook(input: PreBrowseInput): Promise<PreBrowseResult> {
|
|
56
|
+
const reasoningBank = getReasoningBank();
|
|
57
|
+
const warnings: string[] = [];
|
|
58
|
+
|
|
59
|
+
// Find similar patterns
|
|
60
|
+
const similarPatterns = await reasoningBank.findSimilarPatterns(input.goal);
|
|
61
|
+
|
|
62
|
+
// Get recommended steps
|
|
63
|
+
const recommendedSteps = await reasoningBank.getRecommendedSteps(input.goal);
|
|
64
|
+
|
|
65
|
+
// Suggest model based on complexity
|
|
66
|
+
let suggestedModel: 'haiku' | 'sonnet' | 'opus' = 'sonnet';
|
|
67
|
+
if (recommendedSteps.length <= 3) {
|
|
68
|
+
suggestedModel = 'haiku';
|
|
69
|
+
} else if (recommendedSteps.length > 10 || input.goal.toLowerCase().includes('complex')) {
|
|
70
|
+
suggestedModel = 'opus';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Estimate duration based on patterns
|
|
74
|
+
let estimatedDuration = 5000; // Default 5s
|
|
75
|
+
if (similarPatterns.length > 0) {
|
|
76
|
+
estimatedDuration = Math.round(
|
|
77
|
+
similarPatterns.reduce((sum, p) => sum + p.avgDuration * p.steps.length, 0) / similarPatterns.length
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Generate warnings
|
|
82
|
+
if (input.url && !input.url.startsWith('https://')) {
|
|
83
|
+
warnings.push('URL is not HTTPS - authentication data may be at risk');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (input.goal.toLowerCase().includes('login') && !input.goal.toLowerCase().includes('test')) {
|
|
87
|
+
warnings.push('Login detected - consider using state-save/state-load for session persistence');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (similarPatterns.length === 0) {
|
|
91
|
+
warnings.push('No similar patterns found - this is a new workflow');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
recommendedSteps,
|
|
96
|
+
similarPatterns: similarPatterns.length,
|
|
97
|
+
suggestedModel,
|
|
98
|
+
estimatedDuration,
|
|
99
|
+
warnings,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Post-Browse Hook
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Post-browse hook - called after browser automation completes
|
|
109
|
+
* Stores patterns and records learning feedback
|
|
110
|
+
*/
|
|
111
|
+
export async function postBrowseHook(input: PostBrowseInput): Promise<PostBrowseResult> {
|
|
112
|
+
const reasoningBank = getReasoningBank();
|
|
113
|
+
|
|
114
|
+
// Record verdict for learning
|
|
115
|
+
await reasoningBank.recordVerdict(input.trajectoryId, input.success, input.verdict);
|
|
116
|
+
|
|
117
|
+
// If there were errors, analyze them
|
|
118
|
+
if (input.errors && input.errors.length > 0) {
|
|
119
|
+
console.log(`[post-browse] Errors to learn from: ${input.errors.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const stats = reasoningBank.getStats();
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
patternStored: input.success,
|
|
126
|
+
patternId: input.success ? `pattern-${input.trajectoryId}` : undefined,
|
|
127
|
+
learnedFrom: true,
|
|
128
|
+
statsUpdated: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Hook Registration for CLI
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
export const browserHooks = {
|
|
137
|
+
'pre-browse': {
|
|
138
|
+
name: 'pre-browse',
|
|
139
|
+
description: 'Get recommendations before browser automation',
|
|
140
|
+
handler: preBrowseHook,
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
goal: { type: 'string', description: 'What you want to accomplish' },
|
|
145
|
+
url: { type: 'string', description: 'Target URL (optional)' },
|
|
146
|
+
context: { type: 'string', description: 'Additional context' },
|
|
147
|
+
},
|
|
148
|
+
required: ['goal'],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
'post-browse': {
|
|
152
|
+
name: 'post-browse',
|
|
153
|
+
description: 'Record browser automation outcome for learning',
|
|
154
|
+
handler: postBrowseHook,
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
trajectoryId: { type: 'string', description: 'Trajectory ID from browser service' },
|
|
159
|
+
success: { type: 'boolean', description: 'Whether the automation succeeded' },
|
|
160
|
+
verdict: { type: 'string', description: 'Human feedback on quality' },
|
|
161
|
+
duration: { type: 'number', description: 'Total duration in ms' },
|
|
162
|
+
stepsCompleted: { type: 'number', description: 'Number of steps completed' },
|
|
163
|
+
errors: { type: 'array', items: { type: 'string' }, description: 'Error messages if any' },
|
|
164
|
+
},
|
|
165
|
+
required: ['trajectoryId', 'success', 'duration', 'stepsCompleted'],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default browserHooks;
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - Memory Integration
|
|
3
|
+
* Persistent memory storage with HNSW semantic search for browser patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { BrowserTrajectory, BrowserTrajectoryStep, Snapshot, ActionResult } from '../domain/types.js';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Memory Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface BrowserMemoryEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'trajectory' | 'pattern' | 'snapshot' | 'session' | 'error';
|
|
15
|
+
key: string;
|
|
16
|
+
value: Record<string, unknown>;
|
|
17
|
+
metadata: {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
goal?: string;
|
|
21
|
+
success?: boolean;
|
|
22
|
+
duration?: number;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
embedding?: number[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MemorySearchResult {
|
|
29
|
+
entry: BrowserMemoryEntry;
|
|
30
|
+
score: number;
|
|
31
|
+
distance: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MemoryStats {
|
|
35
|
+
totalEntries: number;
|
|
36
|
+
byType: Record<string, number>;
|
|
37
|
+
bySession: Record<string, number>;
|
|
38
|
+
avgEmbeddingDim: number;
|
|
39
|
+
indexSize: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Memory Adapter Interface
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
export interface IMemoryAdapter {
|
|
47
|
+
store(entry: BrowserMemoryEntry): Promise<void>;
|
|
48
|
+
retrieve(key: string): Promise<BrowserMemoryEntry | null>;
|
|
49
|
+
search(query: string, options?: MemorySearchOptions): Promise<MemorySearchResult[]>;
|
|
50
|
+
delete(key: string): Promise<boolean>;
|
|
51
|
+
list(filter?: MemoryFilter): Promise<BrowserMemoryEntry[]>;
|
|
52
|
+
getStats(): Promise<MemoryStats>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MemorySearchOptions {
|
|
56
|
+
topK?: number;
|
|
57
|
+
minScore?: number;
|
|
58
|
+
type?: BrowserMemoryEntry['type'];
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
namespace?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MemoryFilter {
|
|
64
|
+
type?: BrowserMemoryEntry['type'];
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
startTime?: string;
|
|
67
|
+
endTime?: string;
|
|
68
|
+
success?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Claude Flow Memory Adapter
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Adapter for claude-flow memory system with HNSW indexing
|
|
77
|
+
*/
|
|
78
|
+
export class ClaudeFlowMemoryAdapter implements IMemoryAdapter {
|
|
79
|
+
private namespace: string;
|
|
80
|
+
private cache: Map<string, BrowserMemoryEntry> = new Map();
|
|
81
|
+
private embeddingCache: Map<string, number[]> = new Map();
|
|
82
|
+
|
|
83
|
+
constructor(namespace = 'browser') {
|
|
84
|
+
this.namespace = namespace;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Store a browser memory entry with optional embedding
|
|
89
|
+
*/
|
|
90
|
+
async store(entry: BrowserMemoryEntry): Promise<void> {
|
|
91
|
+
const key = `${this.namespace}:${entry.type}:${entry.key}`;
|
|
92
|
+
|
|
93
|
+
// Generate text for embedding
|
|
94
|
+
const embeddingText = this.generateEmbeddingText(entry);
|
|
95
|
+
|
|
96
|
+
// Store in memory via MCP (when available)
|
|
97
|
+
try {
|
|
98
|
+
// This would call claude-flow memory_store MCP tool
|
|
99
|
+
// For now, store in local cache
|
|
100
|
+
this.cache.set(key, {
|
|
101
|
+
...entry,
|
|
102
|
+
metadata: {
|
|
103
|
+
...entry.metadata,
|
|
104
|
+
timestamp: entry.metadata.timestamp || new Date().toISOString(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Store embedding text for search
|
|
109
|
+
if (embeddingText) {
|
|
110
|
+
this.embeddingCache.set(key, this.simpleHash(embeddingText));
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[memory] Failed to store entry:', error);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Retrieve a specific memory entry
|
|
120
|
+
*/
|
|
121
|
+
async retrieve(key: string): Promise<BrowserMemoryEntry | null> {
|
|
122
|
+
const fullKey = key.includes(':') ? key : `${this.namespace}:${key}`;
|
|
123
|
+
return this.cache.get(fullKey) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Semantic search using HNSW index (falls back to keyword search)
|
|
128
|
+
*/
|
|
129
|
+
async search(query: string, options: MemorySearchOptions = {}): Promise<MemorySearchResult[]> {
|
|
130
|
+
const { topK = 10, minScore = 0.3, type, sessionId } = options;
|
|
131
|
+
|
|
132
|
+
const results: MemorySearchResult[] = [];
|
|
133
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
134
|
+
|
|
135
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
136
|
+
// Apply filters
|
|
137
|
+
if (type && entry.type !== type) continue;
|
|
138
|
+
if (sessionId && entry.metadata.sessionId !== sessionId) continue;
|
|
139
|
+
|
|
140
|
+
// Calculate relevance score
|
|
141
|
+
const entryText = this.generateEmbeddingText(entry).toLowerCase();
|
|
142
|
+
let matches = 0;
|
|
143
|
+
for (const term of queryTerms) {
|
|
144
|
+
if (entryText.includes(term)) matches++;
|
|
145
|
+
}
|
|
146
|
+
const score = matches / queryTerms.length;
|
|
147
|
+
|
|
148
|
+
if (score >= minScore) {
|
|
149
|
+
results.push({
|
|
150
|
+
entry,
|
|
151
|
+
score,
|
|
152
|
+
distance: 1 - score,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Sort by score and limit
|
|
158
|
+
return results
|
|
159
|
+
.sort((a, b) => b.score - a.score)
|
|
160
|
+
.slice(0, topK);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Delete a memory entry
|
|
165
|
+
*/
|
|
166
|
+
async delete(key: string): Promise<boolean> {
|
|
167
|
+
const fullKey = key.includes(':') ? key : `${this.namespace}:${key}`;
|
|
168
|
+
const deleted = this.cache.delete(fullKey);
|
|
169
|
+
this.embeddingCache.delete(fullKey);
|
|
170
|
+
return deleted;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* List entries with optional filters
|
|
175
|
+
*/
|
|
176
|
+
async list(filter: MemoryFilter = {}): Promise<BrowserMemoryEntry[]> {
|
|
177
|
+
const entries: BrowserMemoryEntry[] = [];
|
|
178
|
+
|
|
179
|
+
for (const entry of this.cache.values()) {
|
|
180
|
+
if (filter.type && entry.type !== filter.type) continue;
|
|
181
|
+
if (filter.sessionId && entry.metadata.sessionId !== filter.sessionId) continue;
|
|
182
|
+
if (filter.success !== undefined && entry.metadata.success !== filter.success) continue;
|
|
183
|
+
if (filter.startTime && entry.metadata.timestamp < filter.startTime) continue;
|
|
184
|
+
if (filter.endTime && entry.metadata.timestamp > filter.endTime) continue;
|
|
185
|
+
|
|
186
|
+
entries.push(entry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return entries;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get memory statistics
|
|
194
|
+
*/
|
|
195
|
+
async getStats(): Promise<MemoryStats> {
|
|
196
|
+
const byType: Record<string, number> = {};
|
|
197
|
+
const bySession: Record<string, number> = {};
|
|
198
|
+
|
|
199
|
+
for (const entry of this.cache.values()) {
|
|
200
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
201
|
+
bySession[entry.metadata.sessionId] = (bySession[entry.metadata.sessionId] || 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
totalEntries: this.cache.size,
|
|
206
|
+
byType,
|
|
207
|
+
bySession,
|
|
208
|
+
avgEmbeddingDim: 0, // Would be calculated from actual embeddings
|
|
209
|
+
indexSize: this.embeddingCache.size,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generate text for embedding from entry
|
|
215
|
+
*/
|
|
216
|
+
private generateEmbeddingText(entry: BrowserMemoryEntry): string {
|
|
217
|
+
const parts: string[] = [];
|
|
218
|
+
|
|
219
|
+
if (entry.metadata.goal) parts.push(entry.metadata.goal);
|
|
220
|
+
if (entry.metadata.url) parts.push(entry.metadata.url);
|
|
221
|
+
|
|
222
|
+
if (entry.type === 'trajectory') {
|
|
223
|
+
const trajectory = entry.value as unknown as BrowserTrajectory;
|
|
224
|
+
parts.push(trajectory.goal);
|
|
225
|
+
trajectory.steps?.forEach((step) => {
|
|
226
|
+
parts.push(`${step.action} ${JSON.stringify(step.input)}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (entry.type === 'error') {
|
|
231
|
+
parts.push(String(entry.value.message || ''));
|
|
232
|
+
parts.push(String(entry.value.stack || ''));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return parts.join(' ');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Simple hash for embedding placeholder (real implementation would use ONNX)
|
|
240
|
+
*/
|
|
241
|
+
private simpleHash(text: string): number[] {
|
|
242
|
+
const hash: number[] = new Array(128).fill(0);
|
|
243
|
+
for (let i = 0; i < text.length; i++) {
|
|
244
|
+
hash[i % 128] += text.charCodeAt(i);
|
|
245
|
+
}
|
|
246
|
+
const max = Math.max(...hash);
|
|
247
|
+
return hash.map((v) => v / max);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Browser Memory Manager
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* High-level memory manager for browser automation
|
|
257
|
+
*/
|
|
258
|
+
export class BrowserMemoryManager {
|
|
259
|
+
private adapter: IMemoryAdapter;
|
|
260
|
+
private sessionId: string;
|
|
261
|
+
|
|
262
|
+
constructor(sessionId: string, adapter?: IMemoryAdapter) {
|
|
263
|
+
this.sessionId = sessionId;
|
|
264
|
+
this.adapter = adapter || new ClaudeFlowMemoryAdapter();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Store a completed trajectory
|
|
269
|
+
*/
|
|
270
|
+
async storeTrajectory(trajectory: BrowserTrajectory): Promise<void> {
|
|
271
|
+
await this.adapter.store({
|
|
272
|
+
id: trajectory.id,
|
|
273
|
+
type: 'trajectory',
|
|
274
|
+
key: trajectory.id,
|
|
275
|
+
value: trajectory as unknown as Record<string, unknown>,
|
|
276
|
+
metadata: {
|
|
277
|
+
sessionId: this.sessionId,
|
|
278
|
+
url: trajectory.steps[0]?.input?.url as string,
|
|
279
|
+
goal: trajectory.goal,
|
|
280
|
+
success: trajectory.success,
|
|
281
|
+
duration: this.calculateDuration(trajectory),
|
|
282
|
+
timestamp: trajectory.completedAt || new Date().toISOString(),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Store a learned pattern
|
|
289
|
+
*/
|
|
290
|
+
async storePattern(
|
|
291
|
+
patternId: string,
|
|
292
|
+
goal: string,
|
|
293
|
+
steps: Array<{ action: string; selector?: string; value?: string }>,
|
|
294
|
+
success: boolean
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
await this.adapter.store({
|
|
297
|
+
id: patternId,
|
|
298
|
+
type: 'pattern',
|
|
299
|
+
key: patternId,
|
|
300
|
+
value: { goal, steps, success },
|
|
301
|
+
metadata: {
|
|
302
|
+
sessionId: this.sessionId,
|
|
303
|
+
goal,
|
|
304
|
+
success,
|
|
305
|
+
timestamp: new Date().toISOString(),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Store a snapshot for later retrieval
|
|
312
|
+
*/
|
|
313
|
+
async storeSnapshot(snapshotId: string, snapshot: Snapshot): Promise<void> {
|
|
314
|
+
await this.adapter.store({
|
|
315
|
+
id: snapshotId,
|
|
316
|
+
type: 'snapshot',
|
|
317
|
+
key: snapshotId,
|
|
318
|
+
value: snapshot as unknown as Record<string, unknown>,
|
|
319
|
+
metadata: {
|
|
320
|
+
sessionId: this.sessionId,
|
|
321
|
+
url: snapshot.url,
|
|
322
|
+
timestamp: snapshot.timestamp,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Store an error for learning
|
|
329
|
+
*/
|
|
330
|
+
async storeError(
|
|
331
|
+
errorId: string,
|
|
332
|
+
error: Error,
|
|
333
|
+
context: { action?: string; selector?: string; url?: string }
|
|
334
|
+
): Promise<void> {
|
|
335
|
+
await this.adapter.store({
|
|
336
|
+
id: errorId,
|
|
337
|
+
type: 'error',
|
|
338
|
+
key: errorId,
|
|
339
|
+
value: {
|
|
340
|
+
message: error.message,
|
|
341
|
+
stack: error.stack,
|
|
342
|
+
name: error.name,
|
|
343
|
+
...context,
|
|
344
|
+
},
|
|
345
|
+
metadata: {
|
|
346
|
+
sessionId: this.sessionId,
|
|
347
|
+
url: context.url,
|
|
348
|
+
success: false,
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Find similar trajectories for a given goal
|
|
356
|
+
*/
|
|
357
|
+
async findSimilarTrajectories(goal: string, topK = 5): Promise<BrowserTrajectory[]> {
|
|
358
|
+
const results = await this.adapter.search(goal, {
|
|
359
|
+
topK,
|
|
360
|
+
type: 'trajectory',
|
|
361
|
+
minScore: 0.3,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return results.map((r) => r.entry.value as unknown as BrowserTrajectory);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Find patterns for a given goal
|
|
369
|
+
*/
|
|
370
|
+
async findPatterns(goal: string, successfulOnly = true): Promise<MemorySearchResult[]> {
|
|
371
|
+
const results = await this.adapter.search(goal, {
|
|
372
|
+
topK: 10,
|
|
373
|
+
type: 'pattern',
|
|
374
|
+
minScore: 0.2,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (successfulOnly) {
|
|
378
|
+
return results.filter((r) => r.entry.metadata.success === true);
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get session memory stats
|
|
385
|
+
*/
|
|
386
|
+
async getSessionStats(): Promise<{
|
|
387
|
+
trajectories: number;
|
|
388
|
+
patterns: number;
|
|
389
|
+
snapshots: number;
|
|
390
|
+
errors: number;
|
|
391
|
+
successRate: number;
|
|
392
|
+
}> {
|
|
393
|
+
const entries = await this.adapter.list({ sessionId: this.sessionId });
|
|
394
|
+
|
|
395
|
+
let trajectories = 0;
|
|
396
|
+
let patterns = 0;
|
|
397
|
+
let snapshots = 0;
|
|
398
|
+
let errors = 0;
|
|
399
|
+
let successCount = 0;
|
|
400
|
+
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
switch (entry.type) {
|
|
403
|
+
case 'trajectory':
|
|
404
|
+
trajectories++;
|
|
405
|
+
if (entry.metadata.success) successCount++;
|
|
406
|
+
break;
|
|
407
|
+
case 'pattern':
|
|
408
|
+
patterns++;
|
|
409
|
+
break;
|
|
410
|
+
case 'snapshot':
|
|
411
|
+
snapshots++;
|
|
412
|
+
break;
|
|
413
|
+
case 'error':
|
|
414
|
+
errors++;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
trajectories,
|
|
421
|
+
patterns,
|
|
422
|
+
snapshots,
|
|
423
|
+
errors,
|
|
424
|
+
successRate: trajectories > 0 ? successCount / trajectories : 0,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private calculateDuration(trajectory: BrowserTrajectory): number {
|
|
429
|
+
if (!trajectory.startedAt || !trajectory.completedAt) return 0;
|
|
430
|
+
return new Date(trajectory.completedAt).getTime() - new Date(trajectory.startedAt).getTime();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ============================================================================
|
|
435
|
+
// Singleton Factory
|
|
436
|
+
// ============================================================================
|
|
437
|
+
|
|
438
|
+
let defaultAdapter: IMemoryAdapter | null = null;
|
|
439
|
+
|
|
440
|
+
export function getMemoryAdapter(): IMemoryAdapter {
|
|
441
|
+
if (!defaultAdapter) {
|
|
442
|
+
defaultAdapter = new ClaudeFlowMemoryAdapter();
|
|
443
|
+
}
|
|
444
|
+
return defaultAdapter;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function createMemoryManager(sessionId: string): BrowserMemoryManager {
|
|
448
|
+
return new BrowserMemoryManager(sessionId, getMemoryAdapter());
|
|
449
|
+
}
|