()
| 230 | // ─── Main ──────────────────────────────────────────────── |
| 231 | |
| 232 | async function main() { |
| 233 | const openDb = new Database(OPENCODE_DB, { readonly: true }); |
| 234 | const ctxDb = new Database(CONTEXT_DB, { readonly: true }); |
| 235 | |
| 236 | // Resolve session ID |
| 237 | if (!sessionId) { |
| 238 | const latest = openDb.prepare(` |
| 239 | SELECT id FROM session ORDER BY time_created DESC LIMIT 1 |
| 240 | `).get() as { id: string } | null; |
| 241 | if (!latest) { console.error("No sessions found"); process.exit(1); } |
| 242 | sessionId = latest.id; |
| 243 | } |
| 244 | |
| 245 | // Resolve project identity |
| 246 | const proc = Bun.spawnSync(["git", "rev-list", "--max-parents=0", "HEAD"], { stdout: "pipe" }); |
| 247 | const rootHash = new TextDecoder().decode(proc.stdout).trim().split("\n")[0] ?? ""; |
| 248 | const projectPath = rootHash ? `git:${rootHash}` : ""; |
| 249 | |
| 250 | console.log(`Session: ${sessionId}`); |
| 251 | console.log(`Project: ${projectPath}`); |
| 252 | console.log(`Threshold: ${scoreThreshold}`); |
| 253 | console.log(`Limit: ${messageLimit} user messages (newest first)\n`); |
| 254 | |
| 255 | const messages = readUserMessages(openDb, sessionId, messageLimit); |
| 256 | console.log(`Found ${messages.length} user messages\n`); |
| 257 | console.log("═".repeat(80)); |
| 258 | |
| 259 | let wouldNudge = 0; |
| 260 | let totalMessages = 0; |
| 261 | let skippedShort = 0; |
| 262 | |
| 263 | for (const msg of messages) { |
| 264 | totalMessages++; |
| 265 | const preview = msg.text.length > 120 ? `${msg.text.slice(0, 120)}…` : msg.text; |
| 266 | |
| 267 | // Extract terms |
| 268 | const terms = extractSearchTerms(msg.text); |
| 269 | |
| 270 | if (terms.length < 2) { |
| 271 | skippedShort++; |
| 272 | console.log(`\n[${totalMessages}] ${preview}`); |
| 273 | console.log(` Terms: ${terms.length === 0 ? "(none)" : terms.join(", ")}`); |
| 274 | console.log(" → SKIP (too few terms)"); |
| 275 | continue; |
| 276 | } |
| 277 | |
| 278 | // Build search query from top terms (max 6 to avoid noise) |
| 279 | const searchQuery = terms.slice(0, 6).join(" "); |
| 280 | |
| 281 | // Search |
| 282 | const memHits = searchMemoriesFTS(ctxDb, projectPath, searchQuery, 5); |
| 283 | const factHits = searchFactsFTS(ctxDb, sessionId, searchQuery); |
| 284 | |
| 285 | const allHits = [...memHits, ...factHits].sort((a, b) => b.score - a.score); |
| 286 | const topHit = allHits[0]; |
| 287 | const triggered = topHit && topHit.score >= scoreThreshold; |
| 288 | |
| 289 | console.log(`\n[${totalMessages}] ${preview}`); |
no test coverage detected