okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""What the daemon's folder browser and project readers need from a remote host. 2 3albedo_gather.py 4 5reads one JSON request line on stdin and prints one JSON snapshot. The daemon 6interprets the snapshot with the same code it uses on its own disk 7(albedo/daemon/folders.gleam, albedo/harness/vcs.gleam), so this side only 8collects: directory entries, which repository markers exist, the vcs 9commands the daemon planned, file sizes, and project files. Everything comes 10back in one ssh round trip per question. 11 12Request: `{route, dir, plans, tracked, deadline, counted}`, `dir` absolute 13and normalised. `route` is list, repo, preview or project, or exists, which 14answers `directory` alone. `tracked` names, per kind, the planned command 15that lists a preview's tracked files and the separator its output uses. 16 17Snapshot: `{directory, entries: {dir: [[name, dir, modified, symlink]]}, exists: 18[path], outputs: {key: stdout}, sizes: {path: bytes}, files: {relative: 19base64}}`. An output's key is its program and arguments joined by NUL; only 20commands that exit 0 within the deadline answer. 21""" 22 23from __future__import annotations 24 25import base64 26import json 27import os 28import stat 29import subprocess 30import sys 31from concurrent .futures import ThreadPoolExecutor 32 33MARKERS = (".jj" ,".git" ) 34SUBDIRECTORIES = 256 # child directories whose entries a preview lists 35PROJECT_FILES = 512 36PROJECT_BYTES = 8 * 1024 * 1024 37FILE_BYTES = 1024 * 1024 + 1 # one past the instruction limit, so it still warns 38 39 40def child (directory :str ,name :str )-> str : 41return "/" + name if directory == "/" else f" { directory } / { name } " 42 43 44def shown (name :str )-> bool : 45"""Names the daemon can show and send back: valid UTF-8.""" 46try : 47name .encode () 48return True 49except UnicodeEncodeError : 50return False 51 52 53def entries (directory :str )-> list [list [object ]]: 54try : 55names = os .listdir (directory ) 56except OSError : 57return [] 58listed :list [list [object ]]= [] 59for name in filter (shown ,names ): 60path = child (directory ,name ) 61symlink = os .path .islink (path ) 62try : 63info = os .stat (path ) 64listed .append ( 65 [name ,stat .S_ISDIR (info .st_mode ),int (info .st_mtime ),symlink ] 66 ) 67except OSError : 68listed .append ([name ,False ,0 ,symlink ]) 69return listed 70 71 72def marks (directory :str ,exists :list [str ])-> str | None : 73"""Record the markers at `directory`; the kind it roots, .jj first.""" 74found = None 75for marker in MARKERS : 76path = child (directory ,marker ) 77if os .path .lexists (path )and (os .path .isfile (path )or os .path .isdir (path )): 78exists .append (path ) 79found = found or marker 80return found 81 82 83def parent (directory :str )-> str | None : 84return None if directory == "/" else os .path .dirname (directory )or "/" 85 86 87def checkout (directory :str ,exists :list [str ])-> tuple [str ,str ]| None : 88"""The nearest enclosing repository: (kind, root).""" 89current :str | None = directory 90while current is not None : 91found = marks (current ,exists ) 92if found is not None : 93return found [1 :],current 94current = parent (current ) 95return None 96 97 98def key (program :str ,args :list [str ])-> str : 99return "\0" .join ([program ,* args ]) 100 101 102def run ( 103command :list ,root :str ,directory :str ,deadline :float 104)-> tuple [str ,str | None ]: 105program ,args ,at = command 106try : 107done = subprocess .run ( 108 [program ,* args ], 109cwd = root if at == "root" else directory , 110stdin = subprocess .DEVNULL , 111capture_output = True , 112timeout = deadline , 113 ) 114except (OSError ,subprocess .TimeoutExpired ): 115return key (program ,args ),None 116output = done .stdout .decode (errors = "replace" )if done .returncode == 0 else None 117return key (program ,args ),output 118 119 120def project (directory :str )-> dict [str ,str ]: 121"""Project instruction files and skills, relative to `directory`.""" 122picked :list [str ]= [] 123for relative in ("" ,".agents" ,".albedo" ): 124base = child (directory ,relative )if relative else directory 125try : 126names = sorted (filter (shown ,os .listdir (base ))) 127except OSError : 128continue 129for name in names : 130if name .lower ().endswith (".md" )and os .path .isfile (child (base ,name )): 131picked .append (f" { relative } / { name } " if relative else name ) 132for relative in (".agents/skills" ,".albedo/skills" ): 133for root ,directories ,names in os .walk (child (directory ,relative )): 134directories .sort () 135for name in sorted (filter (shown ,names )): 136path = os .path .join (root ,name ) 137if os .path .isfile (path )and len (picked )< PROJECT_FILES : 138picked .append (os .path .relpath (path ,directory )) 139files :dict [str ,str ]= {} 140budget = PROJECT_BYTES 141for relative in picked : 142try : 143with open (child (directory ,relative ),"rb" )as handle : 144data = handle .read (min (FILE_BYTES ,budget )) 145except OSError : 146continue 147budget -= len (data ) 148files [relative ]= base64 .b64encode (data ).decode () 149if budget <= 0 : 150break 151return files 152 153 154def gather (request :dict )-> dict [str ,object ]: 155route ,directory = request ["route" ],request ["dir" ] 156snapshot :dict [str ,object ]= {"directory" :os .path .isdir (directory )} 157if not snapshot ["directory" ]or route == "exists" : 158return snapshot 159if route == "project" : 160snapshot ["files" ]= project (directory ) 161return snapshot 162exists :list [str ]= [] 163listed = {directory :entries (directory )} 164snapshot .update (entries = listed ,exists = exists ) 165if route == "list" : 166for name ,is_directory ,_ ,_symlink in listed [directory ]: 167if is_directory : 168marks (child (directory ,str (name )),exists ) 169return snapshot 170found = checkout (directory ,exists ) 171if route == "preview" : 172children = [ 173child (directory ,str (name )) 174for name ,is_directory ,_ ,_symlink in listed [directory ] 175if is_directory and not str (name ).startswith ("." ) 176 ] 177for path in children [:SUBDIRECTORIES ]: 178listed [path ]= entries (path ) 179if found is None : 180return snapshot 181kind ,root = found 182deadline = float (request .get ("deadline" ,2.0 )) 183plan = request .get ("plans" , {}).get (kind , []) 184with ThreadPoolExecutor (max_workers = max (1 ,len (plan )))as pool : 185answers = dict (pool .map (lambda c :run (c ,root ,directory ,deadline ),plan )) 186outputs = {k :v for k ,v in answers .items ()if v is not None } 187snapshot ["outputs" ]= outputs 188if route == "preview" : 189relatives :list [str ]= [] 190tracked = request .get ("tracked" , {}).get (kind ) 191if tracked is not None : 192program ,args ,_ = tracked ["command" ] 193listing = outputs .get (key (program ,args ))or "" 194relatives = listing .split (tracked ["separator" ]) 195sizes :dict [str ,int ]= {} 196for relative in relatives [:int (request .get ("counted" ,0 ))]: 197if not relative : 198continue 199path = child (directory ,relative ) 200try : 201info = os .stat (path ) 202except OSError : 203continue 204if stat .S_ISREG (info .st_mode ): 205sizes [path ]= info .st_size 206snapshot ["sizes" ]= sizes 207return snapshot 208 209 210def main ()-> int : 211request = json .loads (sys .stdin .readline ()) 212json .dumps (gather (request ))) 213return 0 214 215 216if __name__ == "__main__" : 217sys .exit (main ())