#!/usr/bin/env bash # Copyright 2026 Hee-Suk Kim # SPDX-License-Identifier: GPL-3.0-only set -o pipefail : "${SIB_DIR:=$HOME/.sib}" export SIB_DIR readonly TRACE_MAGIC=_SIBTRACE readonly TRACE_VERSION=v1 readonly DEFAULT_PLM_MODEL=gpt-5.6-luna readonly DEFAULT_PLM_ENDPOINT=https://api.openai.com/v1/responses readonly COMMIT_DATE='999999999 +0000' die() { [ $# -eq 0 ] || echo fatal: "$@" >&2 exit 1 } lgit() { env GIT_AUTHOR_NAME='Sibyl' \ GIT_COMMITTER_NAME='Sibyl' \ GIT_AUTHOR_EMAIL='sib@local' \ GIT_COMMITTER_EMAIL='sib@local' \ git --git-dir="$SIB_DIR" "$@" } cmd_init() { [[ $# -eq 0 ]] || die 'no argument is allowed' local reinit= [[ -f "$SIB_DIR/HEAD" ]] && reinit=true mkdir -p "$SIB_DIR" lgit init --bare --quiet if [[ -z $reinit ]]; then echo 'ref: refs/conv/scratch' > "$SIB_DIR/HEAD" sib config set sib.model $DEFAULT_PLM_MODEL sib config set sib.endpoint $DEFAULT_PLM_ENDPOINT echo "Initialized empty Sib store in $SIB_DIR" >&2 else echo "Reinitialized existing Sib store in $SIB_DIR" >&2 fi } # hash and format blob (to mktree format) # $1: blob name hf_blob() { [ -n "$1" ] || return 1; printf '100644 blob %s\t%s\n' $(lgit hash-object -w --stdin) "$1" } key_is_valid() { [[ $1 =~ ^[A-Za-z_][A-Za-z0-9._-]*$ ]] } tf_key() { local line key hash extra n=0 while IFS= read -r line || [[ -n $line ]]; do n=$((n + 1)) IFS=$'\t' read -r hash key <<<"$line" [[ -n $key && -n $hash ]] || { echo "invalid trace entry at line $n" >&2 return 1 } key_is_valid "$key" || { echo "invalid trace key at line $n: $key" >&2 return 1 } echo "$key" done } # chain-trace [-p ] cmd_chain_trace() { local parent_args=() trace= if [[ $# -eq 1 ]]; then trace="$1" elif [[ $# -eq 3 && "$1" == "-p" ]]; then cmd_ls_trace $2 >/dev/null # check if parent is valid parent_args=(-p "$2") trace="$3" else die "Usage: chain-trace [-p ] " fi # check if trace is valid cmd_ls_trace $trace >/dev/null || die trace $trace is invalid GIT_AUTHOR_DATE="$COMMIT_DATE" \ GIT_COMMITTER_DATE="$COMMIT_DATE" \ lgit commit-tree "${parent_args[@]}" $trace ' local tmp keys magic version tmp="$(lgit ls-tree "$1" 2>/dev/null)" || die "invalid object name $1" tmp=$(awk 'NF==4 { OFS="\t"; print $3, $4 }' <<<"$tmp") # Now $tmp is ls-trace format before screening keys=$(tf_key <<<"$tmp") || die 'invalid trace format' printf '%s\n' "$keys" | sort | uniq -d | grep -q . && die "duplicate key found in trace $1" magic=$(awk -F"\t" -v k=$TRACE_MAGIC '$2 == k { print $1 }' <<<"$tmp") [[ -z $magic ]] && die 'No magic found on trace' # Here it is ensured $magic is valid hash from ls-tree and single # line by upper dedup check version=$(lgit cat-file blob $magic) [[ $version != $TRACE_VERSION ]] && die "invalid trace version: $version" # Here, every key starting with $TRACE_MAGIC will be considered as # non-key and internal indicator. NF >= 2 is to not print \t for # empty input awk -F"\t" -v k=$TRACE_MAGIC -v OFS='\t' \ 'NF >= 2 && index($2, k) != 1 { print $1, $2 }' <<<"$tmp" || true # return 0 for empty trace } cmd_git() { lgit "$@" } # hash and format blob (to trace format) # $1: blob name tf_blob() { local key=$1 hash [[ -n $key ]] || return 1 hash=$(lgit hash-object -w --stdin) || return printf '%s\t%s\n' "$hash" "$key" } # Input: {"key": "value", "key": "value", ...} cmd_mktrace_json() { [[ $# -eq 0 ]] || die 'no argument is allowed' local json="$(cat)" # Reject null/array/object/non-object jq -e 'type == "object" and all(.[]; type == "string")' >/dev/null <<<"$json" \ || die 'input must be a JSON object containing only string values' tr=$(jq -r 'keys_unsorted[]' <<<"$json" | while IFS= read -r key; do [[ -z $key ]] && die empty key detacted on JSON jq -j --arg k "$key" '.[$k]' <<<"$json" | tf_blob "$key" done) || die cmd_mktrace <<<"$tr" } # print single trace object to one-line json (jsonl) cmd_ls_trace_json() { [[ $# -eq 1 ]] || die 'usage: ls-trace-json ' local entries entries=$(cmd_ls_trace "$1") || exit 1 [ -n "$entries" ] || { echo '{}'; return 0; } echo "$entries" | while IFS=$'\t' read -r hash key; do lgit cat-file blob "$hash" \ | jq -Rsc --arg k "$key" '{($k): .}' done | jq -cs 'add // {}' } cmd_chain_trace_json() { local tmp tmp="$(cmd_mktrace_json)" || die cmd_chain_trace "$@" "$tmp" } cmd_chain_jsonl() { [[ $# -eq 0 ]] || die 'no argument is allowed' local line trace chain parent= n=0 while IFS= read -r line || [ -n "$line" ]; do [ -n "$line" ] || continue # skip blank lines trace=$(printf '%s' "$line" | cmd_mktrace_json) \ || die "mktrace failed at line $((n+1))" if [ -z "$parent" ]; then chain=$(cmd_chain_trace "$trace") else chain=$(cmd_chain_trace -p "$parent" "$trace") fi parent="$chain" n=$((n+1)) done [ "$n" -gt 0 ] || die 'empty input' echo "$chain" } cmd_ls_chain() { [[ $# -eq 1 ]] || die 'usage: ls-chain ' lgit cat-file commit "$1" | awk ' /^$/ { exit } # stop at blank line (msg starts) $1 == "tree" { print "trace " $2 } $1 == "parent" { print "parent " $2 } ' } tf_value() { cut -d' ' -f2 } trace_of_chain() { cmd_ls_chain "$1" | grep '^trace ' | tf_value } cmd_rev_list() { lgit rev-list --reverse "$@" } cmd_rev_jsonl() { [[ $# -eq 1 ]] || die 'usage: rev-jsonl ' local commits commits=$(cmd_rev_list "$1") || die "rev-list failed for $1" while read -r commit; do local trace trace=$(trace_of_chain "$commit") || die "no trace for $commit" cmd_ls_trace_json "$trace" done <<< "$commits" } cmd_rev_parse() { [ $# -eq 1 ] || die 'usage: rev-parse ' # Refuse to read refs/conv/HEAD over HEAD. # To read it, use rev-parse conv/HEAD if [[ $1 != "HEAD" ]] && lgit rev-parse --quiet --verify --end-of-options \ "refs/conv/$1^{commit}" 2>/dev/null; then return 0 fi lgit rev-parse --verify --end-of-options "$1^{commit}" } # # Refs # cmd_update_ref() { lgit update-ref --create-reflog "$@" } cmd_show_ref() { [ $# -eq 1 ] || die 'usage: show-ref ' lgit show-ref --verify --hash "$1" } cmd_ls_refs() { local prefix=${1:-refs/} lgit for-each-ref --format='%(refname) %(objectname)' "$prefix" } cmd_symbolic_ref() { lgit symbolic-ref "$@" } # # dispatcher # usage() { cat <&2 usage: sib [-h ...] sib edit sib status sib init EOF } cmd_config() { lgit config "$@" } [[ $# -lt 1 ]] && { usage; die; } cmd="$1"; shift if [[ $cmd != "init" ]]; then lgit rev-parse --resolve-git-dir "$SIB_DIR" &>/dev/null \ || die "$SIB_DIR: not a sib storage: run \`sib init'" fi # We symlink /usr/bin/sib -> ../libexec/sib-core # script_dir need to be realpath ../libexec/sib-core script_dir="$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")" cmd_version() { echo sib version $(cat $script_dir/VERSION) } export PATH="$script_dir:$PATH" # Priority: Env > config > Default : "${PLM_MODEL:=$(cmd_config get sib.model 2>/dev/null)}" : "${PLM_MODEL:=$DEFAULT_PLM_MODEL}" : "${PLM_ENDPOINT:=$(cmd_config get sib.endpoint 2>/dev/null)}" : "${PLM_ENDPOINT:=$DEFAULT_PLM_ENDPOINT}" export PLM_MODEL PLM_ENDPOINT fn="cmd_${cmd//-/_}" if declare -F "$fn" > /dev/null; then "$fn" "$@" elif command -v "sib-$cmd" >/dev/null; then exec "sib-$cmd" "$@" else echo sib: unknown command $cmd >&2 echo >&2 usage die fi