- sabia-core: descoberta de interfaces, controle de modo monitor/canal via iw (ponte até ter nl80211 nativo), captura crua 802.11 via AF_PACKET (sem libpcap), parsing de radiotap e quadros de gerenciamento (beacon/probe/EAPOL), agregação em tabela de APs/ clientes, escrita de .pcap, trilha de auditoria encadeada por hash, auto-atualização via GitHub releases (curl, sem dependências de rede extras) - sabia-cli: `sabia iface`, `sabia scan`, `sabia update`, `sabia audit-verify`, `sabia ui` (servidor local + navegador, protegido por token, mesma filosofia do Guará) - OpenWolf inicializado para memória de projeto entre agentes
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import * as fs from "node:fs"
|
|
import * as path from "node:path"
|
|
import { getWolfDir, writeJSON, readJSON, normalizePath, sessionFilePath } from "./fs.js"
|
|
import { lookupEntry } from "./anatomy.js"
|
|
import type { PartialSessionState } from "./types.js"
|
|
|
|
export function handlePreRead(directory: string, sessionId: string, filePath: string, isRangedRead = false): void {
|
|
filePath=path.resolve(directory,filePath)
|
|
const relative=path.relative(directory,filePath)
|
|
if (!relative || relative === ".." || relative.startsWith(".."+path.sep) || path.isAbsolute(relative)) return
|
|
const wolfDir = getWolfDir(directory)
|
|
if (!fs.existsSync(wolfDir)) return
|
|
|
|
const hooksDir = path.join(wolfDir, "hooks")
|
|
const sessionFile = sessionFilePath(hooksDir, sessionId)
|
|
const normalizedFile = normalizePath(filePath)
|
|
|
|
const projectDir = normalizePath(directory)
|
|
const relToProject = normalizedFile.startsWith(projectDir)
|
|
? normalizedFile.slice(projectDir.length).replace(/^\//, "")
|
|
: ""
|
|
if (relToProject.startsWith(".wolf/") || relToProject.startsWith(".wolf\\")) return
|
|
|
|
// Ranged reads (offset/limit) are exactly what the symbol hints steer the
|
|
// model toward — never warn about them or record them as full reads (a
|
|
// ranged first contact must not make a later legitimate full read look like
|
|
// a duplicate).
|
|
if (isRangedRead) return
|
|
|
|
const session = readJSON<PartialSessionState>(sessionFile, {
|
|
session_id: "", files_read: {}, anatomy_hits: 0, anatomy_misses: 0,
|
|
repeated_reads_warned: 0,
|
|
})
|
|
|
|
if (!session.files_read) session.files_read = {}
|
|
|
|
// Repeat detection is mtime-gated (issue #41): only warn while the file is
|
|
// unchanged since the recorded read. A modified file is a legitimate
|
|
// re-read — drop the stale record and track it fresh below.
|
|
if (session.files_read[normalizedFile]) {
|
|
const prev = session.files_read[normalizedFile]
|
|
let modifiedSinceRead = true
|
|
try {
|
|
const mtime = fs.statSync(filePath).mtimeMs
|
|
modifiedSinceRead = prev.read_mtime === undefined || mtime !== prev.read_mtime
|
|
} catch {}
|
|
if (!modifiedSinceRead) {
|
|
prev.count++
|
|
session.repeated_reads_warned = (session.repeated_reads_warned || 0) + 1
|
|
console.warn(`OpenWolf: ${path.basename(normalizedFile)} was already read this session (~${prev.tokens} tok), unchanged since. If you only need the gist, your earlier read may suffice; for exact text (edit anchors, line numbers), the re-read is fine.`)
|
|
writeJSON(sessionFile, session)
|
|
return
|
|
}
|
|
delete session.files_read[normalizedFile]
|
|
}
|
|
|
|
// Anatomy lookup: O(1) against the durable store, legacy md scan fallback.
|
|
const entry = lookupEntry(wolfDir, projectDir, normalizedFile)
|
|
const found = entry !== null
|
|
if (entry) {
|
|
if (entry.description) {
|
|
console.warn(`OpenWolf anatomy: ${entry.file}: ${entry.description} (~${entry.tokens} tok)`)
|
|
}
|
|
|
|
// Symbol hint: point at slices of big files. Suppressed if the on-disk
|
|
// file no longer matches what was indexed — a stale line range that
|
|
// misdirects an offset read is worse than no hint at all.
|
|
if (entry.symbols && entry.symbols.length > 0) {
|
|
let fresh = false
|
|
try {
|
|
const st = fs.statSync(filePath)
|
|
fresh = (entry.size === undefined || st.size === entry.size) &&
|
|
(entry.mtimeMs === undefined || Math.abs(st.mtimeMs - entry.mtimeMs) < 1)
|
|
} catch {}
|
|
if (fresh) {
|
|
const top = [...entry.symbols].sort((a, b) => b.tokens - a.tokens).slice(0, 5)
|
|
const list = top.map((s) => `${s.kind} ${s.name} L${s.startLine}-${s.endLine} ~${s.tokens} tok`).join("; ")
|
|
console.warn(`Largest sections: ${list}. Read with offset/limit to fetch just the part you need.`)
|
|
}
|
|
}
|
|
}
|
|
|
|
session.anatomy_hits = (session.anatomy_hits || 0) + (found ? 1 : 0)
|
|
session.anatomy_misses = (session.anatomy_misses || 0) + (found ? 0 : 1)
|
|
|
|
// Record initial read entry (tokens will be updated in post-read)
|
|
let readMtime: number | undefined
|
|
try {
|
|
readMtime = fs.statSync(filePath).mtimeMs
|
|
} catch {}
|
|
session.files_read[normalizedFile] = {
|
|
count: 1,
|
|
tokens: 0,
|
|
first_read: new Date().toISOString(),
|
|
read_mtime: readMtime,
|
|
anatomy_hit: found,
|
|
}
|
|
|
|
writeJSON(sessionFile, session)
|
|
}
|