@poolzin/pool-bot 2026.1.29 → 2026.1.31
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/CHANGELOG.md +29 -1
- package/README.md +11 -0
- package/dist/agents/system-prompt.js +16 -16
- package/dist/agents/tools/memory-tool.js +2 -1
- package/dist/build-info.json +3 -3
- package/dist/cli/program/command-registry.js +5 -0
- package/dist/cli/program/register.completion.js +355 -0
- package/dist/gateway/hooks/index.js +49 -0
- package/dist/gateway/hooks/lifecycle-hooks-integration.js +256 -0
- package/dist/gateway/hooks/lifecycle-hooks.js +236 -0
- package/dist/gateway/hooks/progressive-disclosure-details.js +237 -0
- package/dist/gateway/hooks/progressive-disclosure-index.js +354 -0
- package/dist/gateway/hooks/progressive-disclosure-timeline.js +231 -0
- package/dist/gateway/hooks/progressive-disclosure-types.js +65 -0
- package/dist/gateway/hooks/progressive-disclosure.js +242 -0
- package/dist/gateway/hooks/tool-usage-capture.js +253 -0
- package/dist/gateway/hooks/tool-usage-storage.js +144 -0
- package/dist/gateway/server-methods/nodes.js +2 -0
- package/dist/gateway/server.impl.js +4 -0
- package/dist/imessage/monitor/monitor-provider.js +14 -1
- package/dist/media/store.js +37 -1
- package/dist/memory/index.js +5 -0
- package/dist/memory/manager.js +25 -2
- package/docs/WHATSAPP-HEARTBEAT-TROUBLESHOOTING.md +319 -0
- package/package.json +1 -1
- package/skills/webgpu-threejs-tsl/REFERENCE.md +283 -0
- package/skills/webgpu-threejs-tsl/SKILL.md +91 -0
- package/skills/webgpu-threejs-tsl/docs/compute-shaders.md +404 -0
- package/skills/webgpu-threejs-tsl/docs/core-concepts.md +453 -0
- package/skills/webgpu-threejs-tsl/docs/materials.md +353 -0
- package/skills/webgpu-threejs-tsl/docs/post-processing.md +434 -0
- package/skills/webgpu-threejs-tsl/docs/wgsl-integration.md +324 -0
- package/skills/webgpu-threejs-tsl/examples/basic-setup.js +87 -0
- package/skills/webgpu-threejs-tsl/examples/custom-material.js +170 -0
- package/skills/webgpu-threejs-tsl/examples/earth-shader.js +292 -0
- package/skills/webgpu-threejs-tsl/examples/particle-system.js +259 -0
- package/skills/webgpu-threejs-tsl/examples/post-processing.js +199 -0
- package/skills/webgpu-threejs-tsl/templates/compute-shader.js +305 -0
- package/skills/webgpu-threejs-tsl/templates/webgpu-project.js +276 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive Disclosure - Details Layer (Phase 4)
|
|
3
|
+
*
|
|
4
|
+
* Load full memory content on-demand
|
|
5
|
+
* Safe: Read-only, rate-limited, lazy loading
|
|
6
|
+
*
|
|
7
|
+
* Purpose:
|
|
8
|
+
* - Only load full content when user explicitly requests
|
|
9
|
+
* - Provides: Complete file content, line ranges, metadata
|
|
10
|
+
* - Called only for specific results (not all results)
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const MEMORY_DIR = "/root/pool";
|
|
18
|
+
const MAX_CONTENT_SIZE = 100_000; // 100KB max per detail
|
|
19
|
+
const RATE_LIMIT_MS = 100; // Min 100ms between loads
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Rate Limiting (prevent abuse)
|
|
22
|
+
// ============================================================================
|
|
23
|
+
let lastLoadTime = 0;
|
|
24
|
+
/**
|
|
25
|
+
* Rate limiter for detail loading
|
|
26
|
+
* Safe: Prevents excessive I/O operations
|
|
27
|
+
*/
|
|
28
|
+
async function rateLimit() {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const elapsed = now - lastLoadTime;
|
|
31
|
+
if (elapsed < RATE_LIMIT_MS) {
|
|
32
|
+
const delay = RATE_LIMIT_MS - elapsed;
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
34
|
+
}
|
|
35
|
+
lastLoadTime = Date.now();
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Details Layer Implementation
|
|
39
|
+
// ============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Load full memory details for given result IDs
|
|
42
|
+
* Safe: Rate-limited, size-limited, handles errors gracefully
|
|
43
|
+
*
|
|
44
|
+
* @param ids - Result IDs (format: "memory/YYYY-MM-DD.md#L123")
|
|
45
|
+
* @returns Map of IDs to MemoryDetail
|
|
46
|
+
*/
|
|
47
|
+
export async function loadFullDetails(ids) {
|
|
48
|
+
// Validate inputs
|
|
49
|
+
if (!ids || ids.length === 0) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const details = {};
|
|
54
|
+
// Load each detail with rate limiting
|
|
55
|
+
for (const id of ids) {
|
|
56
|
+
await rateLimit();
|
|
57
|
+
const detail = await loadDetailForId(id);
|
|
58
|
+
if (detail) {
|
|
59
|
+
details[id] = detail;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return details;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error("[pd-details] Failed to load details:", err);
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Load detail for a single ID
|
|
71
|
+
* Safe: Returns null if invalid or error
|
|
72
|
+
*/
|
|
73
|
+
async function loadDetailForId(id) {
|
|
74
|
+
try {
|
|
75
|
+
// Parse ID to extract file path and line range
|
|
76
|
+
const { filePath, lineRange } = parseId(id);
|
|
77
|
+
// Read file content
|
|
78
|
+
const content = await readFile(filePath, "utf-8");
|
|
79
|
+
// Validate content size
|
|
80
|
+
if (content.length > MAX_CONTENT_SIZE) {
|
|
81
|
+
console.warn(`[pd-details] Content too large for ${id}, truncating`);
|
|
82
|
+
return createMemoryDetail(id, content.slice(0, MAX_CONTENT_SIZE), filePath, lineRange, true);
|
|
83
|
+
}
|
|
84
|
+
return createMemoryDetail(id, content, filePath, lineRange, false);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (err.code === "ENOENT") {
|
|
88
|
+
// File doesn't exist
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Other error
|
|
92
|
+
console.error(`[pd-details] Failed to load ${id}:`, err);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse result ID to extract file path and line range
|
|
98
|
+
* Safe: Returns defaults if invalid format
|
|
99
|
+
*/
|
|
100
|
+
function parseId(id) {
|
|
101
|
+
try {
|
|
102
|
+
// Format: "memory/YYYY-MM-DD.md#L123" or "MEMORY.md#L10-20"
|
|
103
|
+
const match = id.match(/^(.+\.md)(?:#L(\d+)(?:-(\d+))?)?$/);
|
|
104
|
+
if (!match) {
|
|
105
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
const [, fileName, startLine, endLine] = match;
|
|
108
|
+
// Determine file path
|
|
109
|
+
let filePath;
|
|
110
|
+
if (fileName === "MEMORY.md") {
|
|
111
|
+
filePath = join(MEMORY_DIR, "MEMORY.md");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
filePath = join(MEMORY_DIR, fileName);
|
|
115
|
+
}
|
|
116
|
+
// Parse line range
|
|
117
|
+
const lineRange = startLine
|
|
118
|
+
? {
|
|
119
|
+
start: parseInt(startLine, 10),
|
|
120
|
+
end: endLine ? parseInt(endLine, 10) : parseInt(startLine, 10),
|
|
121
|
+
}
|
|
122
|
+
: undefined;
|
|
123
|
+
return { filePath, lineRange };
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// Invalid format, return defaults
|
|
127
|
+
console.warn(`[pd-details] Invalid ID format: ${id}`);
|
|
128
|
+
return { filePath: join(MEMORY_DIR, "MEMORY.md") };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Create MemoryDetail object
|
|
133
|
+
*/
|
|
134
|
+
function createMemoryDetail(id, content, filePath, lineRange, truncated) {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
content,
|
|
138
|
+
compressed: truncated,
|
|
139
|
+
lines: lineRange,
|
|
140
|
+
metadata: {
|
|
141
|
+
fileType: filePath.includes("MEMORY.md") && !filePath.includes("memory/") ? "MEMORY" : "DAILY",
|
|
142
|
+
size: content.length,
|
|
143
|
+
lastModified: Date.now(), // Approximate (would need fs.stat for real)
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Batch Loading (with concurrency limit)
|
|
149
|
+
// ============================================================================
|
|
150
|
+
/**
|
|
151
|
+
* Load details in batches (limited concurrency)
|
|
152
|
+
* Safe: Prevents overwhelming the file system
|
|
153
|
+
*
|
|
154
|
+
* @param ids - Result IDs to load
|
|
155
|
+
* @param batchSize - Max concurrent loads (default: 5)
|
|
156
|
+
* @returns Map of IDs to MemoryDetail
|
|
157
|
+
*/
|
|
158
|
+
export async function loadDetailsBatch(ids, batchSize = 5) {
|
|
159
|
+
const details = {};
|
|
160
|
+
// Process in batches
|
|
161
|
+
for (let i = 0; i < ids.length; i += batchSize) {
|
|
162
|
+
const batch = ids.slice(i, i + batchSize);
|
|
163
|
+
// Load batch in parallel
|
|
164
|
+
const promises = batch.map(async (id) => {
|
|
165
|
+
await rateLimit();
|
|
166
|
+
return loadDetailForId(id);
|
|
167
|
+
});
|
|
168
|
+
const results = await Promise.allSettled(promises);
|
|
169
|
+
// Merge results
|
|
170
|
+
for (let j = 0; j < results.length; j++) {
|
|
171
|
+
const result = results[j];
|
|
172
|
+
const id = batch[j];
|
|
173
|
+
if (result.status === "fulfilled" && result.value) {
|
|
174
|
+
details[id] = result.value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return details;
|
|
179
|
+
}
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Content Extraction (for line ranges)
|
|
182
|
+
// ============================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Extract specific lines from content
|
|
185
|
+
* Safe: Handles out-of-range gracefully
|
|
186
|
+
*
|
|
187
|
+
* @param content - Full file content
|
|
188
|
+
* @param startLine - Start line number (1-indexed)
|
|
189
|
+
* @param endLine - End line number (1-indexed)
|
|
190
|
+
* @returns Extracted lines
|
|
191
|
+
*/
|
|
192
|
+
export function extractLines(content, startLine, endLine) {
|
|
193
|
+
const lines = content.split("\n");
|
|
194
|
+
// Adjust to 0-indexed
|
|
195
|
+
const start = Math.max(0, startLine - 1);
|
|
196
|
+
const end = Math.min(lines.length, endLine);
|
|
197
|
+
return lines.slice(start, end).join("\n");
|
|
198
|
+
}
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Utilities (for testing)
|
|
201
|
+
// ============================================================================
|
|
202
|
+
/**
|
|
203
|
+
* Get file stats (size, last modified)
|
|
204
|
+
* Safe: Returns defaults if error
|
|
205
|
+
*/
|
|
206
|
+
async function getFileStats(filePath) {
|
|
207
|
+
try {
|
|
208
|
+
const { stat } = await import("node:fs/promises");
|
|
209
|
+
const stats = await stat(filePath);
|
|
210
|
+
return {
|
|
211
|
+
size: stats.size,
|
|
212
|
+
lastModified: stats.mtimeMs,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Fallback to approximate values
|
|
217
|
+
return {
|
|
218
|
+
size: 0,
|
|
219
|
+
lastModified: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Reset rate limiter (for testing)
|
|
225
|
+
*/
|
|
226
|
+
export function resetRateLimiter() {
|
|
227
|
+
lastLoadTime = 0;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get rate limiter stats (for testing)
|
|
231
|
+
*/
|
|
232
|
+
export function getRateLimiterStats() {
|
|
233
|
+
return {
|
|
234
|
+
lastLoadTime,
|
|
235
|
+
rateLimit: RATE_LIMIT_MS,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive Disclosure - Index Layer (Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Lightweight search: returns only index + snippets
|
|
5
|
+
* Token savings: ~90% vs full content search
|
|
6
|
+
*
|
|
7
|
+
* Safe:
|
|
8
|
+
* - Read-only operations (no modifications)
|
|
9
|
+
* - Pure functions (no side effects)
|
|
10
|
+
* - Cacheable results
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { PROGRESSIVE_DISCLOSURE_FLAGS } from "./progressive-disclosure-types.js";
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Constants
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const MEMORY_DIR = "/root/pool";
|
|
19
|
+
const MAX_SNIPPET_LENGTH = 100;
|
|
20
|
+
const DEFAULT_MAX_RESULTS = 10;
|
|
21
|
+
const MIN_SCORE_THRESHOLD = 0.1;
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Cache Implementation (in-memory, thread-safe for single-threaded Node.js)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const cache = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Generate cache key from query
|
|
28
|
+
*/
|
|
29
|
+
function getCacheKey(query, options) {
|
|
30
|
+
const filter = options.tags?.join(",") || "";
|
|
31
|
+
const range = options.dateRange ? `${options.dateRange.start}-${options.dateRange.end}` : "";
|
|
32
|
+
return `${query}:${filter}:${range}:${options.maxResults || DEFAULT_MAX_RESULTS}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get from cache if valid
|
|
36
|
+
*/
|
|
37
|
+
function getFromCache(key) {
|
|
38
|
+
if (!PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_ENABLED) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const cached = cache.get(key);
|
|
42
|
+
if (!cached) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Check TTL
|
|
46
|
+
const age = Date.now() - cached.timestamp;
|
|
47
|
+
if (age > PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_TTL_MS) {
|
|
48
|
+
cache.delete(key);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (PROGRESSIVE_DISCLOSURE_FLAGS.DEBUG_MODE) {
|
|
52
|
+
console.log(`[pd-index] Cache hit: ${key}`);
|
|
53
|
+
}
|
|
54
|
+
return cached.results;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Save to cache (respecting max size)
|
|
58
|
+
*/
|
|
59
|
+
function saveToCache(key, results) {
|
|
60
|
+
if (!PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_ENABLED) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Enforce max cache size
|
|
64
|
+
if (cache.size >= PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_MAX_SIZE) {
|
|
65
|
+
// Remove oldest entry (first key in map)
|
|
66
|
+
const firstKey = cache.keys().next().value;
|
|
67
|
+
if (firstKey) {
|
|
68
|
+
cache.delete(firstKey);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
cache.set(key, {
|
|
72
|
+
key,
|
|
73
|
+
results,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
ttl: PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_TTL_MS,
|
|
76
|
+
});
|
|
77
|
+
if (PROGRESSIVE_DISCLOSURE_FLAGS.DEBUG_MODE) {
|
|
78
|
+
console.log(`[pd-index] Cached: ${key} (${results.length} results)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear all cache (for testing)
|
|
83
|
+
*/
|
|
84
|
+
export function clearIndexCache() {
|
|
85
|
+
cache.clear();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get cache statistics
|
|
89
|
+
*/
|
|
90
|
+
export function getIndexCacheStats() {
|
|
91
|
+
return {
|
|
92
|
+
size: cache.size,
|
|
93
|
+
maxSize: PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_MAX_SIZE,
|
|
94
|
+
ttl: PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_TTL_MS,
|
|
95
|
+
enabled: PROGRESSIVE_DISCLOSURE_FLAGS.CACHE_ENABLED,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Index Layer Implementation
|
|
100
|
+
// ============================================================================
|
|
101
|
+
/**
|
|
102
|
+
* Search memory files and return lightweight index
|
|
103
|
+
* Safe: Read-only, cacheable, pure function
|
|
104
|
+
*
|
|
105
|
+
* @param query - Search query string
|
|
106
|
+
* @param options - Search options
|
|
107
|
+
* @returns Array of search results (lightweight)
|
|
108
|
+
*/
|
|
109
|
+
export async function searchIndexOnly(query, options = {}) {
|
|
110
|
+
// Validate inputs
|
|
111
|
+
const safeQuery = query?.trim() || "";
|
|
112
|
+
if (!safeQuery) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const maxResults = options.maxResults || DEFAULT_MAX_RESULTS;
|
|
116
|
+
const threshold = options.threshold ?? MIN_SCORE_THRESHOLD;
|
|
117
|
+
// Check cache
|
|
118
|
+
const cacheKey = getCacheKey(safeQuery, options);
|
|
119
|
+
const cached = getFromCache(cacheKey);
|
|
120
|
+
if (cached) {
|
|
121
|
+
return cached.slice(0, maxResults);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
// Search in memory files
|
|
125
|
+
const results = await searchMemoryFiles(safeQuery, options);
|
|
126
|
+
// Filter by threshold
|
|
127
|
+
const filtered = results.filter((r) => r.score >= threshold);
|
|
128
|
+
// Sort by score (descending)
|
|
129
|
+
const sorted = filtered.sort((a, b) => b.score - a.score);
|
|
130
|
+
// Limit results
|
|
131
|
+
const limited = sorted.slice(0, maxResults);
|
|
132
|
+
// Cache results
|
|
133
|
+
saveToCache(cacheKey, limited);
|
|
134
|
+
return limited;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error("[pd-index] Search failed:", err);
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Search memory files (main implementation)
|
|
143
|
+
* Safe: Read-only, handles errors gracefully
|
|
144
|
+
*/
|
|
145
|
+
async function searchMemoryFiles(query, options) {
|
|
146
|
+
const results = [];
|
|
147
|
+
const queryLower = query.toLowerCase();
|
|
148
|
+
// Files to search (in order of priority)
|
|
149
|
+
const filesToSearch = getFilesToSearch(options);
|
|
150
|
+
for (const filePath of filesToSearch) {
|
|
151
|
+
try {
|
|
152
|
+
const content = await readFile(filePath, "utf-8");
|
|
153
|
+
const lines = content.split("\n");
|
|
154
|
+
// Extract date from filename
|
|
155
|
+
const date = extractDate(filePath);
|
|
156
|
+
// Search for matches
|
|
157
|
+
const matches = findMatchesInContent(content, lines, queryLower, filePath, date);
|
|
158
|
+
results.push(...matches);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
// Skip files that can't be read
|
|
162
|
+
if (PROGRESSIVE_DISCLOSURE_FLAGS.DEBUG_MODE) {
|
|
163
|
+
console.warn(`[pd-index] Failed to read ${filePath}:`, err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get list of files to search based on options
|
|
171
|
+
*/
|
|
172
|
+
function getFilesToSearch(options) {
|
|
173
|
+
const files = [];
|
|
174
|
+
// Always search MEMORY.md first
|
|
175
|
+
files.push(join(MEMORY_DIR, "MEMORY.md"));
|
|
176
|
+
// Search daily memory files
|
|
177
|
+
if (!options.dateRange) {
|
|
178
|
+
// No date filter: search all daily files
|
|
179
|
+
// For now, just search recent files (last 30 days)
|
|
180
|
+
const recentFiles = getRecentDailyFiles(30);
|
|
181
|
+
files.push(...recentFiles);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Date range filter
|
|
185
|
+
const rangeFiles = getDailyFilesInRange(options.dateRange.start, options.dateRange.end);
|
|
186
|
+
files.push(...rangeFiles);
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get recent daily memory files
|
|
192
|
+
* Safe: Returns empty array if directory doesn't exist
|
|
193
|
+
*/
|
|
194
|
+
function getRecentDailyFiles(days) {
|
|
195
|
+
const files = [];
|
|
196
|
+
const now = new Date();
|
|
197
|
+
for (let i = 0; i < days; i++) {
|
|
198
|
+
const date = new Date(now);
|
|
199
|
+
date.setDate(date.getDate() - i);
|
|
200
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
201
|
+
const filePath = join(MEMORY_DIR, "memory", `${dateStr}.md`);
|
|
202
|
+
files.push(filePath);
|
|
203
|
+
}
|
|
204
|
+
return files;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get daily files in date range
|
|
208
|
+
*/
|
|
209
|
+
function getDailyFilesInRange(start, end) {
|
|
210
|
+
const files = [];
|
|
211
|
+
const startDate = new Date(start);
|
|
212
|
+
const endDate = new Date(end);
|
|
213
|
+
const currentDate = new Date(startDate);
|
|
214
|
+
while (currentDate <= endDate) {
|
|
215
|
+
const dateStr = currentDate.toISOString().split("T")[0];
|
|
216
|
+
const filePath = join(MEMORY_DIR, "memory", `${dateStr}.md`);
|
|
217
|
+
files.push(filePath);
|
|
218
|
+
// Next day
|
|
219
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
220
|
+
}
|
|
221
|
+
return files;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Extract date from file path
|
|
225
|
+
*/
|
|
226
|
+
function extractDate(filePath) {
|
|
227
|
+
// Format: /root/pool/memory/2026-02-04.md -> 2026-02-04
|
|
228
|
+
const match = filePath.match(/(\d{4}-\d{2}-\d{2})\.md$/);
|
|
229
|
+
if (match) {
|
|
230
|
+
return match[1];
|
|
231
|
+
}
|
|
232
|
+
// MEMORY.md -> today's date
|
|
233
|
+
return new Date().toISOString().split("T")[0];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Find matches in content (simple string matching)
|
|
237
|
+
* Safe: No regex, handles edge cases
|
|
238
|
+
*/
|
|
239
|
+
function findMatchesInContent(content, lines, queryLower, filePath, date) {
|
|
240
|
+
const results = [];
|
|
241
|
+
const queryWords = queryLower.split(/\s+/);
|
|
242
|
+
// Scan content for matches
|
|
243
|
+
let matchCount = 0;
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const line = lines[i];
|
|
246
|
+
const lineLower = line.toLowerCase();
|
|
247
|
+
// Simple word matching (all query words must be present)
|
|
248
|
+
const allWordsPresent = queryWords.every((word) => lineLower.includes(word));
|
|
249
|
+
if (!allWordsPresent) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Calculate score (simple: word count / line length)
|
|
253
|
+
const score = calculateScore(line, queryWords);
|
|
254
|
+
// Create result
|
|
255
|
+
const result = {
|
|
256
|
+
id: `${filePath}#L${i + 1}`,
|
|
257
|
+
date,
|
|
258
|
+
snippet: extractSnippet(line, content, i),
|
|
259
|
+
score,
|
|
260
|
+
tags: extractTags(line, content),
|
|
261
|
+
title: extractTitle(content),
|
|
262
|
+
};
|
|
263
|
+
results.push(result);
|
|
264
|
+
matchCount++;
|
|
265
|
+
// Limit matches per file (avoid too many from same file)
|
|
266
|
+
if (matchCount >= 5) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Calculate relevance score
|
|
274
|
+
* Simple heuristic: more query words = higher score
|
|
275
|
+
*/
|
|
276
|
+
function calculateScore(line, queryWords) {
|
|
277
|
+
const lineLower = line.toLowerCase();
|
|
278
|
+
let matchCount = 0;
|
|
279
|
+
for (const word of queryWords) {
|
|
280
|
+
if (lineLower.includes(word)) {
|
|
281
|
+
matchCount++;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Score = (matches / total words) normalized 0-1
|
|
285
|
+
const baseScore = matchCount / queryWords.length;
|
|
286
|
+
// Boost score for shorter lines (more focused)
|
|
287
|
+
const lengthBonus = Math.max(0, 1 - line.length / 500);
|
|
288
|
+
return Math.min(1, baseScore * 0.8 + lengthBonus * 0.2);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract snippet (preview)
|
|
292
|
+
*/
|
|
293
|
+
function extractSnippet(line, content, lineIndex) {
|
|
294
|
+
// Return first MAX_SNIPPET_LENGTH chars of line
|
|
295
|
+
if (line.length <= MAX_SNIPPET_LENGTH) {
|
|
296
|
+
return line;
|
|
297
|
+
}
|
|
298
|
+
return line.slice(0, MAX_SNIPPET_LENGTH) + "...";
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Extract tags from content (simple heuristic)
|
|
302
|
+
*/
|
|
303
|
+
function extractTags(line, content) {
|
|
304
|
+
const tags = [];
|
|
305
|
+
const lineLower = line.toLowerCase();
|
|
306
|
+
// Simple tag detection based on keywords
|
|
307
|
+
const tagKeywords = [
|
|
308
|
+
"decision",
|
|
309
|
+
"project",
|
|
310
|
+
"todo",
|
|
311
|
+
"important",
|
|
312
|
+
"bug",
|
|
313
|
+
"feature",
|
|
314
|
+
"fix",
|
|
315
|
+
"deploy",
|
|
316
|
+
"meeting",
|
|
317
|
+
"review",
|
|
318
|
+
];
|
|
319
|
+
for (const keyword of tagKeywords) {
|
|
320
|
+
if (lineLower.includes(keyword)) {
|
|
321
|
+
tags.push(keyword);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return tags;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Extract title from content (first heading)
|
|
328
|
+
*/
|
|
329
|
+
function extractTitle(content) {
|
|
330
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
331
|
+
return match ? match[1].trim() : undefined;
|
|
332
|
+
}
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Public API (for testing)
|
|
335
|
+
// ============================================================================
|
|
336
|
+
/**
|
|
337
|
+
* Force cache invalidation (for testing)
|
|
338
|
+
*/
|
|
339
|
+
export function invalidateIndexCache(query) {
|
|
340
|
+
if (query) {
|
|
341
|
+
// Invalidate all cache keys containing the query
|
|
342
|
+
const keysToDelete = [];
|
|
343
|
+
cache.forEach((_, key) => {
|
|
344
|
+
if (key.startsWith(query)) {
|
|
345
|
+
keysToDelete.push(key);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
keysToDelete.forEach((key) => cache.delete(key));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Clear all cache
|
|
352
|
+
cache.clear();
|
|
353
|
+
}
|
|
354
|
+
}
|