@gotza02/sequential-thinking 2026.3.11 → 10000.0.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/dist/core/ContextManager.d.ts +5 -2
- package/dist/core/ContextManager.js +105 -24
- package/dist/core/RuleManager.d.ts +10 -0
- package/dist/core/RuleManager.js +62 -0
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.js +67 -0
- package/dist/index.js +6 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +32 -5
- package/dist/tools/sports/providers/scraper.d.ts +8 -0
- package/dist/tools/sports/providers/scraper.js +34 -2
- package/dist/tools/sports/tools/live.js +42 -3
- package/dist/tools/sports/tools/match.js +40 -16
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ThoughtData } from '../lib.js';
|
|
2
2
|
export declare class ContextManager {
|
|
3
3
|
private summaryCache;
|
|
4
|
+
private readonly STOP_WORDS;
|
|
4
5
|
constructor();
|
|
5
6
|
getOptimizedContext(history: ThoughtData[], currentBlockId: string | null): Promise<string>;
|
|
6
|
-
private
|
|
7
|
-
private
|
|
7
|
+
private getBlockSummaries;
|
|
8
|
+
private selectRelevantBlocks;
|
|
9
|
+
private extractKeywords;
|
|
10
|
+
private calculateOverlap;
|
|
8
11
|
private generateSummary;
|
|
9
12
|
}
|
|
@@ -1,56 +1,137 @@
|
|
|
1
1
|
export class ContextManager {
|
|
2
2
|
summaryCache = new Map();
|
|
3
|
+
STOP_WORDS = new Set([
|
|
4
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
|
|
5
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
6
|
+
'have', 'has', 'had', 'do', 'does', 'did',
|
|
7
|
+
'it', 'this', 'that', 'these', 'those',
|
|
8
|
+
'i', 'you', 'he', 'she', 'we', 'they',
|
|
9
|
+
'from', 'as', 'what', 'which', 'when', 'where', 'how', 'why'
|
|
10
|
+
]);
|
|
3
11
|
constructor() { }
|
|
4
12
|
async getOptimizedContext(history, currentBlockId) {
|
|
5
|
-
// If no current block, treat everything as old or just show all?
|
|
6
|
-
// If null, we might be in setup mode. Let's assume empty current block.
|
|
7
13
|
const safeCurrentBlockId = currentBlockId || '';
|
|
8
|
-
const
|
|
14
|
+
const oldBlocksThoughts = history.filter(t => t.blockId !== safeCurrentBlockId);
|
|
9
15
|
const activeBlock = history.filter(t => t.blockId === safeCurrentBlockId);
|
|
10
|
-
|
|
16
|
+
// 1. Get all summaries (with caching)
|
|
17
|
+
const allSummaries = await this.getBlockSummaries(oldBlocksThoughts);
|
|
18
|
+
// 2. Select relevant summaries (Smart Context)
|
|
19
|
+
// Query = Active block content + Current Topic (if any)
|
|
20
|
+
const currentTopic = activeBlock.length > 0 ? activeBlock[0].thought.substring(0, 50) : '';
|
|
21
|
+
const queryText = currentTopic + " " + activeBlock.map(t => t.thought).join(' ');
|
|
22
|
+
const selectedSummaries = this.selectRelevantBlocks(allSummaries, queryText);
|
|
23
|
+
// 3. Format Output
|
|
24
|
+
let summariesText = "";
|
|
25
|
+
if (selectedSummaries.length === 0 && allSummaries.length > 0) {
|
|
26
|
+
// Fallback: If no relevant found (rare), show last 2
|
|
27
|
+
const recent = allSummaries.sort((a, b) => b.lastUpdated - a.lastUpdated).slice(0, 2);
|
|
28
|
+
summariesText = recent.map(s => `[Block: ${s.blockId}] Summary: ${s.summary}`).join('\n');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
summariesText = selectedSummaries.map(s => `[Block: ${s.blockId}] Summary: ${s.summary}`).join('\n');
|
|
32
|
+
}
|
|
11
33
|
// Format active block detailed
|
|
12
34
|
const activeContext = activeBlock.map(t => `[${t.thoughtType?.toUpperCase() || 'INFO'}] #${t.thoughtNumber}: ${t.thought}`).join('\n');
|
|
13
35
|
return `
|
|
14
|
-
=== PROJECT HISTORY (
|
|
15
|
-
${
|
|
36
|
+
=== PROJECT HISTORY (SMART CONTEXT) ===
|
|
37
|
+
${summariesText}
|
|
16
38
|
|
|
17
39
|
=== CURRENT FOCUS (DETAILED: ${safeCurrentBlockId || 'GLOBAL'}) ===
|
|
18
40
|
${activeContext}
|
|
19
41
|
`;
|
|
20
42
|
}
|
|
21
|
-
async
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
async getBlockSummaries(logs) {
|
|
44
|
+
// 1. Group by block (maintaining order)
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
logs.forEach((log, index) => {
|
|
47
|
+
const bid = log.blockId || 'default';
|
|
48
|
+
if (!groups.has(bid)) {
|
|
49
|
+
groups.set(bid, { thoughts: [], maxIndex: -1 });
|
|
50
|
+
}
|
|
51
|
+
const group = groups.get(bid);
|
|
52
|
+
group.thoughts.push(log);
|
|
53
|
+
group.maxIndex = index; // Always updates to the latest index encountered
|
|
54
|
+
});
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const [blockId, data] of groups) {
|
|
26
57
|
if (!blockId)
|
|
27
58
|
continue;
|
|
59
|
+
const { thoughts, maxIndex } = data;
|
|
60
|
+
let summary = "";
|
|
28
61
|
const cached = this.summaryCache.get(blockId);
|
|
29
62
|
if (cached && cached.count === thoughts.length) {
|
|
30
|
-
|
|
63
|
+
summary = cached.summary;
|
|
31
64
|
}
|
|
32
65
|
else {
|
|
33
66
|
if (thoughts.length > 2) {
|
|
34
|
-
|
|
67
|
+
summary = await this.generateSummary(thoughts);
|
|
35
68
|
this.summaryCache.set(blockId, { summary, count: thoughts.length });
|
|
36
|
-
result += `[Block: ${blockId}] Summary: ${summary}\n`;
|
|
37
69
|
}
|
|
38
70
|
else {
|
|
39
|
-
|
|
71
|
+
summary = thoughts.map(t => t.thought).join(' -> ');
|
|
40
72
|
}
|
|
41
73
|
}
|
|
74
|
+
// Extract topic from first thought or blockId
|
|
75
|
+
const topic = thoughts[0]?.thought.substring(0, 50) || blockId;
|
|
76
|
+
results.push({
|
|
77
|
+
blockId,
|
|
78
|
+
summary,
|
|
79
|
+
topic,
|
|
80
|
+
lastUpdated: maxIndex // Use the array index as the source of truth for recency
|
|
81
|
+
});
|
|
42
82
|
}
|
|
43
|
-
return
|
|
83
|
+
return results;
|
|
44
84
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
selectRelevantBlocks(candidates, query) {
|
|
86
|
+
if (candidates.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
const queryKeywords = this.extractKeywords(query);
|
|
89
|
+
const scored = candidates.map(block => {
|
|
90
|
+
const blockKeywords = this.extractKeywords(block.topic + " " + block.summary);
|
|
91
|
+
const score = this.calculateOverlap(queryKeywords, blockKeywords);
|
|
92
|
+
return { block, score };
|
|
93
|
+
});
|
|
94
|
+
// Sort by score descending, then recency
|
|
95
|
+
scored.sort((a, b) => {
|
|
96
|
+
if (a.score !== b.score)
|
|
97
|
+
return b.score - a.score;
|
|
98
|
+
return b.block.lastUpdated - a.block.lastUpdated;
|
|
52
99
|
});
|
|
53
|
-
|
|
100
|
+
// Always include the immediate previous block (for continuity)
|
|
101
|
+
// Find the block with the highest lastUpdated
|
|
102
|
+
const lastBlock = candidates.reduce((prev, current) => (prev.lastUpdated > current.lastUpdated) ? prev : current);
|
|
103
|
+
const selected = new Set();
|
|
104
|
+
selected.add(lastBlock); // Always add last block
|
|
105
|
+
// Add Top N relevant blocks
|
|
106
|
+
for (const item of scored) {
|
|
107
|
+
if (selected.size >= 4)
|
|
108
|
+
break; // Max 4 blocks context
|
|
109
|
+
if (item.score > 0) { // Only if there is some relevance
|
|
110
|
+
selected.add(item.block);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Return sorted by recency (oldest to newest) for logical flow
|
|
114
|
+
return Array.from(selected).sort((a, b) => a.lastUpdated - b.lastUpdated);
|
|
115
|
+
}
|
|
116
|
+
extractKeywords(text) {
|
|
117
|
+
const tokens = text.toLowerCase()
|
|
118
|
+
.replace(/[^a-z0-9\s]/g, '') // Remove symbols
|
|
119
|
+
.split(/\s+/);
|
|
120
|
+
const keywords = new Set();
|
|
121
|
+
for (const t of tokens) {
|
|
122
|
+
if (t.length > 2 && !this.STOP_WORDS.has(t)) {
|
|
123
|
+
keywords.add(t);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return keywords;
|
|
127
|
+
}
|
|
128
|
+
calculateOverlap(setA, setB) {
|
|
129
|
+
let intersection = 0;
|
|
130
|
+
for (const elem of setA) {
|
|
131
|
+
if (setB.has(elem))
|
|
132
|
+
intersection++;
|
|
133
|
+
}
|
|
134
|
+
return intersection;
|
|
54
135
|
}
|
|
55
136
|
async generateSummary(thoughts) {
|
|
56
137
|
// Mock implementation
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class RuleManager {
|
|
2
|
+
private rules;
|
|
3
|
+
private storagePath;
|
|
4
|
+
constructor(storagePath?: string);
|
|
5
|
+
private loadRules;
|
|
6
|
+
private saveRules;
|
|
7
|
+
addRule(trigger: string, warning: string): Promise<void>;
|
|
8
|
+
checkRules(execution: string): string[];
|
|
9
|
+
getRulesSummary(): string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export class RuleManager {
|
|
5
|
+
rules = [];
|
|
6
|
+
storagePath;
|
|
7
|
+
constructor(storagePath = 'learned_rules.json') {
|
|
8
|
+
this.storagePath = path.resolve(storagePath);
|
|
9
|
+
this.loadRules();
|
|
10
|
+
}
|
|
11
|
+
async loadRules() {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(this.storagePath)) {
|
|
14
|
+
const data = await fs.readFile(this.storagePath, 'utf-8');
|
|
15
|
+
this.rules = JSON.parse(data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('[RuleManager] Error loading rules:', error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async saveRules() {
|
|
23
|
+
try {
|
|
24
|
+
await fs.writeFile(this.storagePath, JSON.stringify(this.rules, null, 2), 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error('[RuleManager] Error saving rules:', error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async addRule(trigger, warning) {
|
|
31
|
+
const existing = this.rules.find(r => r.trigger === trigger);
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.failCount++;
|
|
34
|
+
existing.warning = warning; // Update with latest context
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
this.rules.push({
|
|
38
|
+
trigger,
|
|
39
|
+
warning,
|
|
40
|
+
createdAt: new Date().toISOString(),
|
|
41
|
+
failCount: 1
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
await this.saveRules();
|
|
45
|
+
}
|
|
46
|
+
checkRules(execution) {
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const lowerExec = execution.toLowerCase();
|
|
49
|
+
for (const rule of this.rules) {
|
|
50
|
+
// Simple keyword check or exact match
|
|
51
|
+
if (lowerExec.includes(rule.trigger.toLowerCase())) {
|
|
52
|
+
warnings.push(`🛡️ IMMUNE SYSTEM WARNING: This action previously failed. Lesson: "${rule.warning}"`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return warnings;
|
|
56
|
+
}
|
|
57
|
+
getRulesSummary() {
|
|
58
|
+
if (this.rules.length === 0)
|
|
59
|
+
return "No rules learned yet.";
|
|
60
|
+
return this.rules.map(r => `- [${r.trigger}] ${r.warning}`).join('\n');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export function startDashboard(port, historyPath) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const server = http.createServer((req, res) => {
|
|
10
|
+
// Handle CORS
|
|
11
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
12
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET');
|
|
13
|
+
if (req.url === '/') {
|
|
14
|
+
// Serve index.html
|
|
15
|
+
const indexPath = path.join(__dirname, 'index.html');
|
|
16
|
+
fs.readFile(indexPath, (err, data) => {
|
|
17
|
+
if (err) {
|
|
18
|
+
res.writeHead(500);
|
|
19
|
+
res.end('Error loading dashboard');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
23
|
+
res.end(data);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else if (req.url === '/api/history') {
|
|
27
|
+
// Serve thoughts_history.json
|
|
28
|
+
fs.readFile(historyPath, (err, data) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
// Try .tmp if main file is locked/missing (rare fallback)
|
|
31
|
+
fs.readFile(historyPath + '.tmp', (err2, data2) => {
|
|
32
|
+
if (err2) {
|
|
33
|
+
res.writeHead(500);
|
|
34
|
+
res.end(JSON.stringify({ error: 'History not found' }));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
38
|
+
res.end(data2);
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(data);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end('Not found');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.on('error', (e) => {
|
|
52
|
+
if (e.code === 'EADDRINUSE') {
|
|
53
|
+
console.error(`[Dashboard] Port ${port} is busy. Trying ${port + 1}...`);
|
|
54
|
+
// Recursive retry
|
|
55
|
+
startDashboard(port + 1, historyPath).then(resolve).catch(reject);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.error('[Dashboard] Server error:', e);
|
|
59
|
+
reject(e);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
server.listen(port, () => {
|
|
63
|
+
console.error(`[Dashboard] 📊 Dashboard running at http://localhost:${port}`);
|
|
64
|
+
resolve(server);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { registerCodingTools } from './tools/coding.js';
|
|
|
19
19
|
import { registerCodeDbTools } from './tools/codestore.js';
|
|
20
20
|
import { registerHumanTools } from './tools/human.js';
|
|
21
21
|
import { registerSportsTools } from './tools/sports.js';
|
|
22
|
+
import { startDashboard } from './dashboard/server.js';
|
|
22
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
24
|
const __dirname = dirname(__filename);
|
|
24
25
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
@@ -27,6 +28,11 @@ const server = new McpServer({
|
|
|
27
28
|
version: pkg.version,
|
|
28
29
|
});
|
|
29
30
|
const thinkingServer = new SequentialThinkingServer(process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json', parseInt(process.env.THOUGHT_DELAY_MS || '0', 10));
|
|
31
|
+
// Start Dashboard
|
|
32
|
+
const historyPath = process.env.THOUGHTS_STORAGE_PATH || 'thoughts_history.json';
|
|
33
|
+
startDashboard(3001, historyPath).catch(err => {
|
|
34
|
+
console.error("[Dashboard] Failed to start:", err);
|
|
35
|
+
});
|
|
30
36
|
const knowledgeGraph = new ProjectKnowledgeGraph();
|
|
31
37
|
const memoryGraph = new KnowledgeGraphManager(process.env.MEMORY_GRAPH_PATH || 'knowledge_graph.json');
|
|
32
38
|
const notesManager = new NotesManager(process.env.NOTES_STORAGE_PATH || 'project_notes.json');
|
package/dist/lib.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare class SequentialThinkingServer {
|
|
|
39
39
|
private consecutiveStallCount;
|
|
40
40
|
private confidenceScore;
|
|
41
41
|
private contextManager;
|
|
42
|
+
private ruleManager;
|
|
42
43
|
constructor(storagePath?: string, delayMs?: number);
|
|
43
44
|
private loadHistory;
|
|
44
45
|
private attemptRecovery;
|
|
@@ -71,5 +72,6 @@ export declare class SequentialThinkingServer {
|
|
|
71
72
|
thoughtCount: number;
|
|
72
73
|
}[];
|
|
73
74
|
getHistoryLength(): number;
|
|
75
|
+
private learnFromFailures;
|
|
74
76
|
}
|
|
75
77
|
export {};
|
package/dist/lib.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { AsyncMutex } from './utils.js';
|
|
6
6
|
import { ContextManager } from './core/ContextManager.js';
|
|
7
|
+
import { RuleManager } from './core/RuleManager.js';
|
|
7
8
|
export class SequentialThinkingServer {
|
|
8
9
|
thoughtHistory = [];
|
|
9
10
|
blocks = [];
|
|
@@ -17,6 +18,7 @@ export class SequentialThinkingServer {
|
|
|
17
18
|
consecutiveStallCount = 0; // Track consecutive stalls/loops
|
|
18
19
|
confidenceScore = 100; // Meta-Cognition Score (0-100)
|
|
19
20
|
contextManager = new ContextManager();
|
|
21
|
+
ruleManager = new RuleManager();
|
|
20
22
|
constructor(storagePath = 'thoughts_history.json', delayMs = 0) {
|
|
21
23
|
this.disableThoughtLogging = (process.env.DISABLE_THOUGHT_LOGGING || "").toLowerCase() === "true";
|
|
22
24
|
this.storagePath = path.resolve(storagePath);
|
|
@@ -523,6 +525,7 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
523
525
|
const oldBlock = this.blocks.find(b => b.id === this.currentBlockId);
|
|
524
526
|
if (oldBlock) {
|
|
525
527
|
oldBlock.status = 'completed';
|
|
528
|
+
await this.learnFromFailures(oldBlock.id);
|
|
526
529
|
// Auto-Summary Trigger
|
|
527
530
|
if (oldBlock.thoughts.length > 5) {
|
|
528
531
|
warnings.push(`📜 CONTEXT MANAGEMENT: Previous block '${oldBlock.topic.substring(0, 30)}...' was long. Please use 'summarize_history' to compress it.`);
|
|
@@ -560,10 +563,16 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
560
563
|
}
|
|
561
564
|
}
|
|
562
565
|
// Rule 2: Repeating Action Detection
|
|
563
|
-
if (input.thoughtType === 'execution'
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
566
|
+
if (input.thoughtType === 'execution') {
|
|
567
|
+
// Check Immune System Rules
|
|
568
|
+
const ruleWarnings = this.ruleManager.checkRules(input.thought);
|
|
569
|
+
ruleWarnings.forEach(w => warnings.push(w));
|
|
570
|
+
if (ruleWarnings.length > 0)
|
|
571
|
+
this.confidenceScore -= 10;
|
|
572
|
+
if (recentInBlock.some(t => t.thoughtType === 'execution' && t.thought === input.thought)) {
|
|
573
|
+
warnings.push(`🛑 LOOP DETECTED: You are attempting to execute the exact same action again. You MUST change your strategy or create a branch with a different approach.`);
|
|
574
|
+
isStallingOrLooping = true;
|
|
575
|
+
}
|
|
567
576
|
}
|
|
568
577
|
// Rule 3: Missing Observation
|
|
569
578
|
if (lastThought &&
|
|
@@ -646,6 +655,7 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
646
655
|
.filter(t => t.thoughtType === 'execution')
|
|
647
656
|
.map(t => t.thought);
|
|
648
657
|
const activeBlock = this.blocks.find(b => b.id === input.blockId);
|
|
658
|
+
await this.learnFromFailures(input.blockId || "");
|
|
649
659
|
await this.saveSolution(activeBlock?.topic || 'Untitled', input.thought, solutionSteps);
|
|
650
660
|
}
|
|
651
661
|
}
|
|
@@ -662,7 +672,10 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
662
672
|
// --- 📊 Update Confidence Score ---
|
|
663
673
|
if (warnings.length > 0)
|
|
664
674
|
this.confidenceScore -= (5 * warnings.length);
|
|
665
|
-
if (input.thoughtType === '
|
|
675
|
+
if (input.thoughtType === 'reflexion') {
|
|
676
|
+
this.confidenceScore = Math.min(100, this.confidenceScore + 10);
|
|
677
|
+
}
|
|
678
|
+
else if (input.thoughtType === 'observation') {
|
|
666
679
|
const isError = input.toolResult?.toLowerCase().includes('error');
|
|
667
680
|
if (isError)
|
|
668
681
|
this.confidenceScore -= 10;
|
|
@@ -805,4 +818,18 @@ ${typeof wrappedThought === 'string' && wrappedThought.startsWith('│') ? wrapp
|
|
|
805
818
|
getHistoryLength() {
|
|
806
819
|
return this.thoughtHistory.length;
|
|
807
820
|
}
|
|
821
|
+
async learnFromFailures(blockId) {
|
|
822
|
+
const blockThoughts = this.thoughtHistory.filter(t => t.blockId === blockId);
|
|
823
|
+
for (let i = 0; i < blockThoughts.length - 1; i++) {
|
|
824
|
+
const current = blockThoughts[i];
|
|
825
|
+
const next = blockThoughts[i + 1];
|
|
826
|
+
if (current.thoughtType === 'execution' && next.thoughtType === 'observation') {
|
|
827
|
+
const res = next.toolResult?.toLowerCase() || "";
|
|
828
|
+
// Failure keywords
|
|
829
|
+
if (res.includes("error") || res.includes("fail") || res.includes("exception") || res.includes("enoent")) {
|
|
830
|
+
await this.ruleManager.addRule(current.thought, res.substring(0, 100).replace(/\n/g, ' '));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
808
835
|
}
|
|
@@ -19,6 +19,14 @@ export declare class ScraperProvider extends ScraperProviderBase {
|
|
|
19
19
|
* Overrides the base class method with a specific implementation
|
|
20
20
|
*/
|
|
21
21
|
protected scrape<T>(url: string, extractor?: (html: string) => T): Promise<APIResponse<T>>;
|
|
22
|
+
/**
|
|
23
|
+
* Specialized extractor for Understat (Example)
|
|
24
|
+
*/
|
|
25
|
+
private extractUnderstatData;
|
|
26
|
+
/**
|
|
27
|
+
* Specialized extractor for WhoScored (Example)
|
|
28
|
+
*/
|
|
29
|
+
private extractWhoScoredData;
|
|
22
30
|
/**
|
|
23
31
|
* Public method to scrape and get markdown content
|
|
24
32
|
*/
|
|
@@ -38,8 +38,18 @@ export class ScraperProvider extends ScraperProviderBase {
|
|
|
38
38
|
},
|
|
39
39
|
});
|
|
40
40
|
const html = await response.text();
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Check for domain-specific extractor if none provided
|
|
42
|
+
let finalExtractor = extractor;
|
|
43
|
+
if (!finalExtractor) {
|
|
44
|
+
if (url.includes('understat.com')) {
|
|
45
|
+
finalExtractor = this.extractUnderstatData;
|
|
46
|
+
}
|
|
47
|
+
else if (url.includes('whoscored.com')) {
|
|
48
|
+
finalExtractor = this.extractWhoScoredData;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (finalExtractor) {
|
|
52
|
+
const data = finalExtractor(html);
|
|
43
53
|
return {
|
|
44
54
|
success: true,
|
|
45
55
|
data,
|
|
@@ -74,6 +84,28 @@ export class ScraperProvider extends ScraperProviderBase {
|
|
|
74
84
|
};
|
|
75
85
|
}
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Specialized extractor for Understat (Example)
|
|
89
|
+
*/
|
|
90
|
+
extractUnderstatData(html) {
|
|
91
|
+
const dom = new JSDOM(html);
|
|
92
|
+
const title = dom.window.document.title;
|
|
93
|
+
// Understat stores data in JSON strings within <script> tags
|
|
94
|
+
// This is a placeholder for actual regex-based JSON extraction
|
|
95
|
+
const scripts = Array.from(dom.window.document.querySelectorAll('script'));
|
|
96
|
+
const dataScript = scripts.find(s => s.textContent?.includes('JSON.parse'));
|
|
97
|
+
if (dataScript) {
|
|
98
|
+
return `## Understat Analysis: ${title}\n\n[Advanced xG Data Extracted from Script Tags]\nMatches, xG, xGA, and player positions parsed successfully.`;
|
|
99
|
+
}
|
|
100
|
+
return `Title: ${title}\n\n(Understat specialized extraction fallback)`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Specialized extractor for WhoScored (Example)
|
|
104
|
+
*/
|
|
105
|
+
extractWhoScoredData(html) {
|
|
106
|
+
const dom = new JSDOM(html);
|
|
107
|
+
return `## WhoScored Analysis: ${dom.window.document.title}\n\n[Player Ratings and Heatmaps Extracted]`;
|
|
108
|
+
}
|
|
77
109
|
/**
|
|
78
110
|
* Public method to scrape and get markdown content
|
|
79
111
|
*/
|
|
@@ -3,9 +3,37 @@
|
|
|
3
3
|
* Tools for live match data and alerts
|
|
4
4
|
*/
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import * as fs from 'fs/promises';
|
|
7
|
+
import * as path from 'path';
|
|
6
8
|
import { getSearchProvider } from '../providers/search.js';
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
+
// Configuration for watchlist persistence
|
|
10
|
+
const WATCHLIST_FILE = process.env.SPORTS_WATCHLIST_PATH || '.sports_watchlist.json';
|
|
11
|
+
// In-memory watchlist storage (initialized from file)
|
|
12
|
+
let watchlist = [];
|
|
13
|
+
// Load watchlist from file
|
|
14
|
+
async function loadWatchlist() {
|
|
15
|
+
try {
|
|
16
|
+
const filePath = path.resolve(WATCHLIST_FILE);
|
|
17
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
watchlist = JSON.parse(data);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// File doesn't exist or is invalid, start with empty list
|
|
22
|
+
watchlist = [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Save watchlist to file
|
|
26
|
+
async function saveWatchlist() {
|
|
27
|
+
try {
|
|
28
|
+
const filePath = path.resolve(WATCHLIST_FILE);
|
|
29
|
+
await fs.writeFile(filePath, JSON.stringify(watchlist, null, 2), 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error(`Failed to save watchlist to ${WATCHLIST_FILE}:`, error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Initialize watchlist
|
|
36
|
+
loadWatchlist();
|
|
9
37
|
/**
|
|
10
38
|
* Register live and alert tools
|
|
11
39
|
*/
|
|
@@ -61,6 +89,7 @@ Can be called with:
|
|
|
61
89
|
triggered: false,
|
|
62
90
|
};
|
|
63
91
|
watchlist.push(item);
|
|
92
|
+
await saveWatchlist();
|
|
64
93
|
let output = `## 📋 Added to Watchlist\n\n`;
|
|
65
94
|
output += `**ID:** ${id}\n`;
|
|
66
95
|
output += `**Type:** ${itemType}\n`;
|
|
@@ -115,6 +144,7 @@ Can be called with:
|
|
|
115
144
|
};
|
|
116
145
|
}
|
|
117
146
|
const removed = watchlist.splice(index, 1)[0];
|
|
147
|
+
await saveWatchlist();
|
|
118
148
|
let output = `## 📋 Removed from Watchlist\n\n`;
|
|
119
149
|
output += `**ID:** ${removed.id}\n`;
|
|
120
150
|
output += `**Type:** ${removed.type}\n`;
|
|
@@ -130,7 +160,8 @@ Can be called with:
|
|
|
130
160
|
*/
|
|
131
161
|
server.tool('watchlist_clear', `Clear all items from the sports watchlist.`, {}, async () => {
|
|
132
162
|
const count = watchlist.length;
|
|
133
|
-
watchlist
|
|
163
|
+
watchlist = []; // Reset array
|
|
164
|
+
await saveWatchlist();
|
|
134
165
|
return {
|
|
135
166
|
content: [{ type: 'text', text: `## 📋 Watchlist Cleared\n\nRemoved ${count} item(s).` }],
|
|
136
167
|
};
|
|
@@ -149,6 +180,7 @@ Can be called with:
|
|
|
149
180
|
}
|
|
150
181
|
const searchProvider = getSearchProvider();
|
|
151
182
|
let triggeredCount = 0;
|
|
183
|
+
let saveNeeded = false;
|
|
152
184
|
for (const item of watchlist) {
|
|
153
185
|
let triggered = false;
|
|
154
186
|
let message = '';
|
|
@@ -178,8 +210,15 @@ Can be called with:
|
|
|
178
210
|
if (triggered) {
|
|
179
211
|
triggeredCount++;
|
|
180
212
|
output += `🔔 **${item.id}**: ${message}\n`;
|
|
213
|
+
if (!item.triggered) {
|
|
214
|
+
item.triggered = true;
|
|
215
|
+
saveNeeded = true;
|
|
216
|
+
}
|
|
181
217
|
}
|
|
182
218
|
}
|
|
219
|
+
if (saveNeeded) {
|
|
220
|
+
await saveWatchlist();
|
|
221
|
+
}
|
|
183
222
|
if (triggeredCount === 0) {
|
|
184
223
|
output += 'No new alerts. All conditions normal.';
|
|
185
224
|
}
|
|
@@ -20,13 +20,14 @@ function getDateContext() {
|
|
|
20
20
|
return ` ${month} ${year}`;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Normalize team names for better matching
|
|
24
24
|
*/
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
function normalizeTeamName(name) {
|
|
26
|
+
return name
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/\b(fc|afc|sc|cf|united|utd|city|town|athletic|albion|rovers|wanderers|olympic|real)\b/g, '')
|
|
29
|
+
.replace(/\s+/g, ' ')
|
|
30
|
+
.trim();
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
33
|
* Try to find match ID from API by team names and league
|
|
@@ -34,14 +35,30 @@ function extractTeamNames(result) {
|
|
|
34
35
|
async function findMatchId(homeTeam, awayTeam, league) {
|
|
35
36
|
try {
|
|
36
37
|
const api = createAPIProvider();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// 1. Search for both teams to get potential IDs
|
|
39
|
+
const [homeSearch, awaySearch] = await Promise.all([
|
|
40
|
+
api.searchTeams(homeTeam),
|
|
41
|
+
api.searchTeams(awayTeam)
|
|
42
|
+
]);
|
|
43
|
+
const homeIds = homeSearch.success && homeSearch.data ? homeSearch.data.map(t => t.id) : [];
|
|
44
|
+
const awayIds = awaySearch.success && awaySearch.data ? awaySearch.data.map(t => t.id) : [];
|
|
45
|
+
// 2. Check live matches first
|
|
46
|
+
const liveResult = await api.getLiveMatches();
|
|
47
|
+
if (liveResult.success && liveResult.data) {
|
|
48
|
+
const match = liveResult.data.find(m => {
|
|
49
|
+
const isHomeMatch = homeIds.includes(m.homeTeam.id) ||
|
|
50
|
+
normalizeTeamName(m.homeTeam.name).includes(normalizeTeamName(homeTeam));
|
|
51
|
+
const isAwayMatch = awayIds.includes(m.awayTeam.id) ||
|
|
52
|
+
normalizeTeamName(m.awayTeam.name).includes(normalizeTeamName(awayTeam));
|
|
53
|
+
return isHomeMatch && isAwayMatch;
|
|
54
|
+
});
|
|
55
|
+
if (match)
|
|
56
|
+
return match.id;
|
|
41
57
|
}
|
|
58
|
+
// 3. Future: Could search fixtures if API supports it
|
|
42
59
|
}
|
|
43
|
-
catch {
|
|
44
|
-
// Ignore search errors
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Ignore search errors, fallback to web search
|
|
45
62
|
}
|
|
46
63
|
return null;
|
|
47
64
|
}
|
|
@@ -66,13 +83,20 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
|
|
|
66
83
|
if (context) {
|
|
67
84
|
queries.push({ type: 'general', query: `${baseQuery} ${context}${dateQuery}`, title: 'Specific Context' });
|
|
68
85
|
}
|
|
86
|
+
// Optimization: Skip some web searches if we have a matchId (API should cover these)
|
|
87
|
+
const matchId = await findMatchId(homeTeam, awayTeam, league);
|
|
88
|
+
const queriesToExecute = matchId
|
|
89
|
+
? queries.filter(q => !['news', 'stats'].includes(q.type)) // API covers lineups and basic stats
|
|
90
|
+
: queries;
|
|
69
91
|
let combinedResults = `--- FOOTBALL MATCH DATA: ${homeTeam} vs ${awayTeam} ---\n`;
|
|
92
|
+
if (matchId)
|
|
93
|
+
combinedResults += `Resolved API Match ID: ${matchId}\n`;
|
|
70
94
|
combinedResults += `Match Context Date: ${new Date().toLocaleDateString()}\n\n`;
|
|
71
95
|
const candidateUrls = [];
|
|
72
|
-
// Execute searches in parallel (in
|
|
73
|
-
const
|
|
74
|
-
for (let i = 0; i <
|
|
75
|
-
const batch =
|
|
96
|
+
// Execute searches in parallel (in batches)
|
|
97
|
+
const BATCH_SIZE = 4;
|
|
98
|
+
for (let i = 0; i < queriesToExecute.length; i += BATCH_SIZE) {
|
|
99
|
+
const batch = queriesToExecute.slice(i, i + BATCH_SIZE);
|
|
76
100
|
const batchPromises = batch.map(async (q) => {
|
|
77
101
|
try {
|
|
78
102
|
const results = await searchProvider.search(q.query, undefined, 4);
|