char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
d76f269
main
1// A CredentialStore over pi's auth.json, locked the way pi locks it, so berry and pi share logins and 2// never double-refresh a rotating oauth token. 3 4import { mkdir , readFile , stat , writeFile } from "node:fs/promises" ; 5import { homedir } from "node:os" ; 6import { dirname , join } from "node:path" ; 7import lockfile from "proper-lockfile" ; 8import type { Credential , CredentialStore } from "@earendil-works/pi-ai" ; 9 10type AuthData = Record < string , Credential >; 11 12export const PI_AUTH_PATH = join ( homedir (), ".pi" , "agent" , "auth.json" ); 13 14/** pi's config values: commands are rejected; `$VAR`/`${VAR}` interpolate, `$$` and `$!` escape. */ 15export function resolveConfigValue ( 16config :string , 17env ?:Record < string , string >, 18) :string | undefined { 19if ( config . startsWith ( "!" )) { 20throw new Error ( 21"Command-based API keys are disabled; use a literal key or an environment variable." , 22); 23} 24let resolved = "" ; 25for ( 26const [ text , braced , bare ] of config . matchAll ( 27/ \$[$!]|\$\{([A-Za-z_]\w*)\}|\$([A-Za-z_]\w*)|[^$]+|\$ / g , 28) 29) { 30const name = braced ?? bare ; 31if ( name === undefined ) { 32resolved += text . length === 2 && text [ 0 ] === "$" ?text [ 1 ] :text ; 33continue ; 34} 35const value = env ?.[ name ] || Deno . env . get ( name ); 36if ( ! value ) return undefined ; 37resolved += value ; 38} 39return resolved ; 40} 41 42export function piAuthStore ( path = PI_AUTH_PATH ) :CredentialStore { 43async function locked < T > ( 44use :( 45data :AuthData , 46) => { result :T ; next ?:AuthData } | Promise <{ result :T ; next ?:AuthData }>, 47signal ?:AbortSignal , 48) :Promise < T > { 49signal ?. throwIfAborted (); 50await mkdir ( dirname ( path ), { recursive :true , mode :0o700 }); 51await writeFile ( path , "{}" , { mode :0o600 , flag :"wx" }). catch (( error ) => { 52if ( error . code !== "EEXIST" ) throw error ; 53}); 54const release = await lockfile . lock ( path , { 55realpath :false , 56stale :30_000 , 57retries :{ retries :30 , factor :1.5 , minTimeout :10 , maxTimeout :1_000 }, 58}); 59try { 60signal ?. throwIfAborted (); 61const text = ( await readFile ( path , "utf8" )). replace ( / ^\uFEFF / , "" ); 62const { result, next} = await use ( text . trim () ?JSON . parse ( text ) :{}); 63signal ?. throwIfAborted (); 64if ( next ) await writeFile ( path , JSON . stringify ( next , null , 2 ), { mode :0o600 }); 65return result ; 66} finally { 67await release (). catch (() => {}); 68} 69} 70 71// reads take the lock only when the file changed, as pi's do: listing models reads every provider at once 72let cached :{ revision :string ; data :Promise < AuthData > } | undefined ; 73async function current ( signal ?:AbortSignal ) :Promise < AuthData > { 74const info = await stat ( path ). catch (() => undefined ); 75const revision = info ?` ${ info . mtimeMs } : ${ info . size } ` :"absent" ; 76if ( cached ?. revision !== revision ) { 77const data = locked (( data ) => ({ result :data }), signal ); 78cached = { revision, data}; 79data . catch (() => cached ?. data === data && ( cached = undefined )); 80} 81return await cached . data ; 82} 83 84return { 85read :async ( provider , options ) => { 86const credential = ( await current ( options ?. signal ))[ provider ]; 87if ( credential ?. type !== "api_key" || credential . key === undefined ) return credential ; 88return { ...credential , key :resolveConfigValue ( credential . key , credential . env ) }; 89}, 90 91list :async ( options ) => 92Object . entries ( await current ( options ?. signal )). map (([ providerId , { type}]) => ({ 93 providerId, 94 type, 95})), 96 97modify :( provider , change , options ) => 98locked ( async ( data ) => { 99const next = await change ( data [ provider ]); 100return next === undefined 101 ?{ result :data [ provider ] } 102 :{ result :next , next :{ ...data , [ provider ] :next } }; 103}, options ?. signal ), 104 105delete :( provider , options ) => 106locked (( data ) => { 107const { [ provider ] :_ , ...rest } = data ; 108return { result :undefined , next :rest }; 109}, options ?. signal ), 110}; 111}