Files
sabia/.opencode/plugin/openwolf/pre-read.ts
T
Lucas e989413018 Esqueleto inicial: sabia-core + sabia-cli + UI local, OpenWolf
- 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
2026-10-23 08:22:04 -03:00

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)
}