ref: 974a684109c1333eccd8c89d519ea869ceec1836
author: Jean-André Santoni <jean.andre.santoni@gmail.com>
date: Thu Mar 19 09:15:35 EDT 2026
initial commit
--- /dev/null
+++ b/ai
@@ -1,0 +1,650 @@
+#!/bin/rc
+
+rfork e
+
+wctl=/dev/null
+if(test -w /dev/wctl)
+ wctl=/dev/wctl
+
+if(~ $AI_MODEL '')
+ model=gpt-4o-mini
+if not
+ model=$AI_MODEL
+
+apiurl=https://api.openai.com/v1/responses
+state=/tmp/ai.$pid
+sessionlog=/tmp/ai.$pid.log
+
+fn usage {+ echo 'usage: ai [-m model] [question ...]' >[1=2]
+ exit usage
+}
+
+fn sigexit {+ rm -rf $state
+}
+
+fn logmsg {+ {+ echo $*
+ } >>$sessionlog
+}
+
+fn logsection {+ ifs='' {label=$1}+ file=$2
+ {+ echo '== '^$label^' =='
+ if(test -f $file){+ if(test -s $file)
+ cat $file
+ if not
+ echo '(empty)'
+ }
+ if not
+ echo '(missing)'
+ echo
+ } >>$sessionlog
+}
+
+fn needkey {+ ifs='' {apikey=`{auth/userpasswd -n 'service=openai host=api.openai.com user=api' >[2]/dev/null | sed -n '2p'}}+ if(! ~ $apikey '')
+ status=()
+ if not {+ echo 'ai: add an openai token to factotum' >[1=2]
+ echo 'ai: echo ''key proto=pass service=openai host=api.openai.com user=api !password=...'' >/mnt/factotum/ctl' >[1=2]
+ exit apikey
+ }
+}
+
+fn sysmsg {+ cat <<'!'
+You are ai, a small coding assistant running from rc on 9front.
+
+You are helping inside the user's current source tree.
+The user may ask about the current tree or about the local system.
+You do not have direct file access. You must use the available tools.
+
+Available tools:
+
+tool files
+ Return a compact file listing for the current directory, with line counts.
+ Output format: one line per file, as "<path> <lines>".
+
+tool man
+page <manual page name>
+ Show a manual page from the local 9front system.
+
+tool shell
+cmd <plan9 rc command>
+ Run one command under Plan 9 rc in the current directory.
+
+tool search
+pattern <grep pattern>
+ Search files in the current directory.
+
+tool read
+path <relative file path>
+start <1-based line number>
+count <line count>
+ Read a slice of one file.
+
+Rules:
+- Ask for at most one tool per response.
+- Use only relative paths.
+- Gather direct evidence before answering substantive questions.
+- Prefer the tool that directly answers the user's question over indirect file inspection.
+- Use repository files only when they are relevant to the user's question.
+- tool shell runs Plan 9 rc, not sh or bash.
+- In Plan 9 rc, $path is the command search path; `ls $path` lists available commands.
+- Use Plan 9 commands and rc syntax when using tool shell.
+- Use tool read to inspect implementation before making claims about behavior.
+- Do not answer from file names, target names, or build metadata alone when implementation files are available.
+- Do not modify files through tool shell unless the user explicitly asks for changes.
+- Do not invent file contents.
+- Do not request write tools; edits are not implemented yet.
+- When you are ready to answer the user, stop using tools.
+
+Output format:
+- For a tool request, output exactly:
+ tool files
+or
+ tool man
+ page ...
+or
+ tool shell
+ cmd ...
+or
+ tool search
+ pattern ...
+or
+ tool read
+ path ...
+ start ...
+ count ...
+- For a final user-facing response, output exactly:
+ reply
+ <your response text>
+
+Do not wrap the output in markdown fences.
+!
+}
+
+fn planmsg {+ cat <<'!'
+You are ai, a small coding assistant running from rc on 9front.
+
+Available tools: files, man, shell, search, read.
+
+Write a short execution plan for the latest user request.
+Focus on the user's request, not on these instructions.
+Treat the latest user request as the only task to plan for.
+Choose the smallest set of relevant tools.
+The latest request may be about the repository or about the local system.
+Do not use tools yet.
+Do not answer the user yet.
+
+Output format:
+- Output exactly:
+ plan
+ <1-3 short lines>
+
+Do not wrap the output in markdown fences.
+!
+}
+
+fn listfiles {+ ls
+}
+
+fn summary {+ echo 'pwd'
+ pwd
+ echo
+
+ echo 'top level'
+ ls | sed 120q
+ echo
+}
+
+fn turnview {+ if(test -f $state/turn)
+ cat $state/turn
+ if not
+ echo '(empty)'
+}
+
+fn mkprompt {+ {+ sysmsg
+ echo
+ echo 'Latest user request:'
+ cat $state/user
+ echo
+ echo 'Repository summary:'
+ summary
+ echo 'Prior conversation:'
+ if(test -f $state/history)
+ cat $state/history
+ if not
+ echo '(empty)'
+ echo
+ echo 'Current turn work:'
+ turnview
+ echo
+ } >$state/prompt
+}
+
+fn mkplanprompt {+ {+ planmsg
+ echo
+ echo 'Latest user request:'
+ cat $state/user
+ } >$state/prompt
+}
+
+fn mkrequest {+ ifs='' {+ modelq=`{jquote $model}+ inputq=`{jquote < $state/prompt}+ }
+ echo -n '{"model":'^$modelq^',"input":'^$inputq^',"store":false}' >$state/request+}
+
+fn httpmeta {+ phase=$1
+ {+ echo 'phase '^$phase
+ echo 'method POST'
+ echo 'url '^$apiurl
+ echo 'header Content-Type: application/json'
+ echo 'header Authorization: Bearer [redacted]'
+ echo 'body '^$state/request
+ } >$state/http
+}
+
+fn apicall {+ phase=$1
+ rm -f $state/errorbody
+ httpmeta $phase
+ logsection $phase^-http $state/http
+ hget \
+ -e $state/errorbody \
+ -r 'Content-Type: application/json' \
+ -r 'Authorization: Bearer '^$apikey \
+ -P $apiurl \
+ <$state/request >$state/response
+ httpstatus=$status
+ {+ if(~ $httpstatus '')
+ echo 'status ok'
+ if not
+ echo 'status '^$"httpstatus
+ } >$state/httpstatus
+ logsection $phase^-http-status $state/httpstatus
+ logsection $phase^-response $state/response
+ if(test -f $state/errorbody)
+ logsection $phase^-errorbody $state/errorbody
+ status=$httpstatus
+}
+
+fn callmodel {+ mkprompt
+ mkrequest
+ logsection prompt $state/prompt
+ logsection model-request $state/request
+ echo 'thinking...' >[1=2]
+ apicall model
+}
+
+fn callplan {+ mkplanprompt
+ mkrequest
+ logsection plan-prompt $state/prompt
+ logsection plan-request $state/request
+ echo 'planning...' >[1=2]
+ apicall plan
+}
+
+fn extracttext {+ errtype=`{jget type error < $state/response >[2]/dev/null}+ if(~ $errtype object){+ if(jget key error < $state/response | jget key message | jget str >$state/error >[2]/dev/null)
+ cat $state/error >[1=2]
+ if not
+ cat $state/response >[1=2]
+ status=error
+ }
+ if not
+ if(! jget key output < $state/response \
+ | jget idx 0 \
+ | jget key content \
+ | jget idx 0 \
+ | jget str text >$state/model >[2]/dev/null){+ echo 'ai: could not decode model response' >[1=2]
+ cat $state/response >[1=2]
+ status=decode
+ }
+ if not
+ status=()
+}
+
+fn cleanmodel {+ sed 's/\r$//' $state/model >$state/model.clean
+ mv $state/model.clean $state/model
+}
+
+fn cleantool {+ sed '/^$/d' $state/model >$state/model.clean
+ mv $state/model.clean $state/model
+}
+
+fn field {+ ifs='' {value=`{grep '^'^$1^' ' $2 | sed 1q | sed 's/^[^ ]* //' | sed 's/[ ]*$//' | sed 's/^"\(.*\)"$/\1/' | sed 's/\r$//'}}+ echo -n $value
+}
+
+fn goodpath {+ switch($1){+ case '' /* ../* */../* */.. ..
+ status=badpath
+ case *
+ status=()
+ }
+}
+
+fn toolfiles {+ for(f in `{listfiles}){+ if(test -f $f){+ n=`{wc -l < $f}+ echo $f^' '^$n
+ }
+ }
+}
+
+fn toolman {+ page=$1
+
+ if(~ $page ''){+ echo 'missing page'
+ status=page
+ }
+ if not
+ man $page | sed 200q
+}
+
+fn toolshell {+ ifs=' ' {cmd=$*}+
+ if(~ $cmd ''){+ echo 'missing command'
+ status=command
+ }
+ if not {+ rc -c $cmd
+ cmdstatus=$status
+ if(~ $cmdstatus '')
+ echo 'status 0'
+ if not
+ echo 'status '^$"cmdstatus
+ status=()
+ }
+}
+
+fn toolsearch {+ ifs=' ' {pat=$*}+ files=()
+
+ if(~ $pat ''){+ echo 'missing pattern'
+ status=pattern
+ }
+ if not
+ files=`{listfiles}+
+ if(~ $#files 0){+ echo 'no files'
+ status=nofiles
+ }
+ if not
+ @{+ for(f in $files)
+ if(test -f $f)
+ grep -n $pat $f >[2]/dev/null
+ } | sed 200q
+}
+
+fn toolread {+ file=$1
+ start=$2
+ count=$3
+
+ if(~ $start '')
+ start=1
+ if(~ $count '')
+ count=120
+
+ if(! goodpath $file){+ echo 'bad path'
+ status=badpath
+ }
+ if not {+ if(! test -f $file){+ echo 'missing file'
+ status=missing
+ }
+ if not {+ end=`{echo $start $count + 1 - p | dc}+ sed -n $start^','^$end^'{=;p;}' $file | sed 'N;s/\n/: /'+ }
+ }
+}
+
+fn adduser {+ {+ echo '== user =='
+ cat $state/user
+ echo
+ } >>$state/history
+}
+
+fn addtool {+ {+ echo '== tool '^$1^' =='
+ cat $2
+ echo
+ } >>$state/turn
+}
+
+fn addreply {+ {+ echo '== assistant =='
+ cat $1
+ echo
+ } >>$state/history
+}
+
+fn addplan {+ {+ echo '== plan =='
+ cat $1
+ echo
+ } >>$state/turn
+}
+
+fn setuser {+ echo $* >$state/user
+ rm -f $state/lasttool
+ rm -f $state/plan
+ rm -f $state/turn
+ logsection user $state/user
+}
+
+fn planturn {+ if(! callplan)
+ status=plan
+ if not {+ if(! extracttext)
+ status=plan
+ if not {+ cleanmodel
+ logsection plan-model $state/model
+ kind=`{sed 1q $state/model | sed 's/[ ].*//'}+ switch($kind){+ case plan
+ sed 1d $state/model >$state/plan
+ case *
+ cp $state/model $state/plan
+ }
+ if(! test -s $state/plan)
+ echo '(empty plan)' >$state/plan
+ echo 'plan:' >[1=2]
+ cat $state/plan >[1=2]
+ addplan $state/plan
+ status=()
+ }
+ }
+}
+
+fn runturn {+ done=no
+ for(step in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32){+ if(~ $done no){+ logmsg '== round '^$step^' =='
+ if(! callmodel)
+ done=error
+ if not {+ if(! extracttext)
+ done=error
+ if not {+ cleanmodel
+ logsection model $state/model
+ type=`{sed 1q $state/model | sed 's/[ ].*//'}+ logmsg 'type '^$type
+ switch($type){+ case reply
+ sed 1d $state/model >$state/reply
+ if(! test -s $state/reply)
+ echo '(empty reply)' >$state/reply
+ cat $state/reply
+ addreply $state/reply
+ done=yes
+
+ case tool
+ cleantool
+ logsection tool-request $state/model
+ name=`{sed 1q $state/model | sed 's/^[^ ]*[ ]*//'}+ logmsg 'tool '^$name
+ repeat=no
+ if(test -f $state/lasttool)
+ if(cmp -s $state/model $state/lasttool >[2]/dev/null){+ repeat=yes
+ }
+ if(~ $repeat yes){+ echo 'repeat tool request ignored; choose a different tool or reply.' >$state/toolout
+ addtool repeat $state/toolout
+ logsection toolout $state/toolout
+ }
+ if(~ $repeat no){+ cp $state/model $state/lasttool
+ switch($name){+ case files
+ echo 'tool: files' >[1=2]
+ toolfiles | sed 200q >$state/toolout >[2=1]
+ addtool files $state/toolout
+ logsection toolout $state/toolout
+
+ case man
+ ifs='' {page=`{field page $state/model}}+ echo 'tool: man '^$page >[1=2]
+ toolman $page >$state/toolout >[2=1]
+ addtool man $state/toolout
+ logsection toolout $state/toolout
+
+ case shell
+ ifs='' {cmd=`{field cmd $state/model}}+ echo 'tool: shell '^$cmd >[1=2]
+ toolshell $cmd >$state/toolout >[2=1]
+ addtool shell $state/toolout
+ logsection toolout $state/toolout
+
+ case search
+ ifs='' {pat=`{field pattern $state/model}}+ echo 'tool: search '^$pat >[1=2]
+ toolsearch $pat >$state/toolout >[2=1]
+ addtool search $state/toolout
+ logsection toolout $state/toolout
+
+ case read
+ file=`{field path $state/model}+ start=`{field start $state/model}+ count=`{field count $state/model}+ echo -n 'tool: read ' >[1=2]
+ echo -n $file >[1=2]
+ echo -n ' ' >[1=2]
+ echo -n $start >[1=2]
+ echo -n ' ' >[1=2]
+ echo $count >[1=2]
+ toolread $file $start $count >$state/toolout >[2=1]
+ addtool read $state/toolout
+ logsection toolout $state/toolout
+
+ case *
+ cat $state/model
+ addreply $state/model
+ done=yes
+ }
+ }
+
+ case *
+ cat $state/model
+ addreply $state/model
+ done=yes
+ }
+ }
+ }
+ }
+ }
+
+ switch($done){+ case yes
+ status=()
+ case error
+ status=runturn
+ case *
+ echo 'ai: too many tool rounds' >[1=2]
+ status=toomanytools
+ }
+}
+
+fn help {+ echo '/help show this help'
+ echo '/reset clear the conversation'
+ echo '/context show the current repo summary'
+ echo '/quit exit'
+}
+
+while(! ~ $#* 0 && ~ $1 -*){+ switch($1){+ case -m
+ shift
+ if(~ $#* 0)
+ usage
+ model=$1
+ case *
+ usage
+ }
+ shift
+}
+
+mkdir $state || exit state
+{+ echo 'ai log'
+ echo 'pid '^$pid
+ echo 'model '^$model
+ echo 'pwd'
+ pwd
+ echo
+} >$sessionlog
+needkey
+
+if(! ~ $#* 0){+ setuser $*
+ adduser
+ if(! planturn)
+ echo 'ai: could not make plan; continuing' >[1=2]
+ runturn
+ exit $status
+}
+
+echo 'ai: terminal mode; /help for commands' >[1=2]
+while() {+ echo -n current >$wctl
+ echo -n top >$wctl
+ echo -n 'ai> ' >[1=2]
+ ifs='' {line=`{read}}+ if(~ $#line 0)
+ exit
+
+ switch($line){+ case /quit /exit
+ exit
+ case /help
+ help
+ case /reset
+ rm -f $state/history
+ echo 'conversation cleared'
+ case /context
+ summary
+ case ''
+ # ignore
+ case *
+ setuser $"line
+ adduser
+ if(! planturn)
+ echo 'ai: could not make plan; continuing' >[1=2]
+ runturn
+ }
+}
--
⑨