char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
a35b3b6
main
1import type { Context } from "@earendil-works/chord" ; 2import { type FileError , getOrThrow } from "@earendil-works/pi-durable/env" ; 3import ignore , { type Ignore } from "ignore" ; 4import type { Environment } from "@berry/api/environment.ts" ; 5 6function unavailable ( error :FileError ) :boolean { 7return [ "not_found" , "not_directory" , "permission_denied" , "invalid" ]. includes ( error . code ); 8} 9 10function score ( path :string , query :string ) :number { 11const text = path . toLowerCase (); 12if ( query . length > text . length ) return - Infinity ; 13let previous = Array < number >( text . length ). fill ( - Infinity ); 14for ( let q = 0 ; q < query . length ; q ++ ) { 15const current = Array < number >( text . length ). fill ( - Infinity ); 16let best = - Infinity ; 17for ( let i = 0 ; i < text . length ; i ++ ) { 18if ( text [ i ] === query [ q ]) { 19const boundary = i === 0 || / [/._-] / u . test ( text [ i - 1 ]) || 20( path [ i ] !== text [ i ] && path [ i - 1 ] === text [ i - 1 ]); 21current [ i ] = ( q === 0 ?- i * 0.1 :Math . max ( best , ( previous [ i - 1 ] ?? - Infinity ) + 8 )) + 221 + ( boundary ?6 :0 ); 23} 24best = Math . max ( best , previous [ i ]) - 1 ; 25} 26previous = current ; 27} 28const quality = Math . max ( ...previous ); 29if ( ! Number . isFinite ( quality )) return - Infinity ; 30const name = text . replace ( / \/$ / u , "" ). split ( "/" ). at ( - 1 ) ! ; 31const rank = text === query || name === query 32 ?3 33 :name . startsWith ( query ) 34 ?2 35 :name . includes ( query ) 36 ?1 37 :0 ; 38return rank * 1000 + quality - path . length * 0.01 ; 39} 40 41const WALK_WIDTH = 16 ; 42 43/** Whether `path` names a directory, through any links; `false` where nothing can be seen. */ 44async function isDirectory ( env :Environment , path :string , context :Context ) :Promise < boolean > { 45const canonical = await env . canonicalPath ( path , context ); 46const info = canonical . ok ?await env . fileInfo ( canonical . value , context ) :canonical ; 47if ( info . ok ) return info . value . kind === "directory" ; 48if ( unavailable ( info . error )) return false ; 49throw info . error ; 50} 51 52/** Paths extending a reference from the environment's working directory, with directories left open. */ 53export async function references ( 54env :Environment , 55prefix :string , 56context :Context , 57) :Promise < string []> { 58if ( / \s / u . test ( prefix ) || prefix . includes ( "\u0000" )) return []; 59if ( prefix === "~" ) return [ "~/" ]; 60let base = prefix . match ( / ^(?:~\/|\/|(?:\.\.?\/)+) / u )?.[ 0 ] ?? "" ; 61let query = prefix . slice ( base . length ); 62let directory = getOrThrow ( await env . absolutePath ( base || "." , context )); 63while ( query . includes ( "/" )) { 64const part = query . slice ( 0 , query . indexOf ( "/" ) + 1 ); 65const next = getOrThrow ( await env . joinPath ([ directory , part ], context )); 66if ( ! ( await isDirectory ( env , next , context ))) break ; 67base += part ; 68query = query . slice ( part . length ); 69directory = next ; 70} 71query = query . toLowerCase (); 72const matches :{ path :string ; score :number }[] = []; 73type Rule = { root :string ; matcher :Ignore }; 74type Visit = { path :string ; current :string ; rules :Rule [] }; 75async function visit ({ path, current, rules :inherited } :Visit ) :Promise < Visit []> { 76const entries = await env . listDir ( current , context ); 77if ( ! entries . ok ) { 78if ( unavailable ( entries . error )) return []; 79throw entries . error ; 80} 81let rules = inherited ; 82const gitignore = entries . value . find (( entry ) => entry . name === ".gitignore" ); 83if ( gitignore ) { 84const source = await env . readTextFile ( gitignore . path , context ); 85if ( source . ok ) rules = [ ...rules , { root :path , matcher :ignore (). add ( source . value ) }]; 86else if ( ! unavailable ( source . error )) throw source . error ; 87} 88const next :Visit [] = []; 89for ( const entry of entries . value ) { 90if ( / \s / u . test ( entry . name ) || [ ".git" , ".jj" , "node_modules" ]. includes ( entry . name )) continue ; 91const relative = path + entry . name ; 92const directory = entry . kind === "directory" || 93( entry . kind === "symlink" && await isDirectory ( env , entry . path , context )); 94const candidate = relative + ( directory ?"/" :"" ); 95let ignored = false ; 96for ( const rule of rules ) { 97const result = rule . matcher . test ( candidate . slice ( rule . root . length )); 98if ( result . ignored ) ignored = true ; 99else if ( result . unignored ) ignored = false ; 100} 101if ( ignored ) continue ; 102const quality = query === "" ?0 :score ( candidate , query ); 103if ( Number . isFinite ( quality )) matches . push ({ path :base + candidate , score :quality }); 104// explicit symlink directories can be browsed, but recursive search must not follow cycles 105if ( query !== "" && entry . kind === "directory" ) { 106next . push ({ path :candidate , current :entry . path , rules}); 107} 108} 109return next ; 110} 111// several directories at once, as each can be a round trip to another machine 112const pending :Visit [] = [{ path :"" , current :directory , rules :[] }]; 113const running = new Set < Promise < void >>(); 114while ( pending . length > 0 || running . size > 0 ) { 115while ( pending . length > 0 && running . size < WALK_WIDTH ) { 116const visiting :Promise < void > = visit ( pending . pop () ! ). then (( next ) => { 117running . delete ( visiting ); 118pending . push ( ...next ); 119}); 120running . add ( visiting ); 121} 122await Promise . race ( running ); 123} 124return matches . sort (( a , b ) => b . score - a . score || a . path . localeCompare ( b . path )) 125. slice ( 0 , 50 ). map (( match ) => match . path ); 126}