@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.
@@ -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
+ }