Posted on
Artificial Intelligence

Artificial Intelligence Mod Management

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Mod Management on Linux (with Bash)

Tired of spending nights juggling mods, guessing load orders, and chasing down conflicts? What if you could keep the deterministic, scriptable power of Bash while offloading the fuzzy “what should go first?” work to an AI that reads your metadata and makes a reasoned suggestion?

This post shows you how to build a simple, reproducible mod management pipeline on Linux using Bash plus a local AI model. You’ll:

  • Index mods and extract metadata/readmes

  • Detect file conflicts deterministically

  • Ask a local LLM for a recommended load order

  • Apply the chosen order via symlinks for quick rollbacks

No more hand-waving; you’ll own the pipeline, version it, and re-run it any time you add or update mods.

Why this matters

  • Mods are messy: readmes, vague dependencies, inconsistent manifests, overlapping files.

  • Bash excels at reproducible pipelines: hashing, indexing, conflict detection, and repeatability.

  • AI excels at text reasoning: dependency notes, patch chains, soft compatibility, and prioritization logic.

  • Combined: reproducible data + AI reasoning = faster, safer modding with documented decisions.

What we’ll build

  • A workspace with:

    • mods/ archives (zip/7z)
    • index/ machine-readable metadata
    • analysis/ conflict reports and AI outputs
    • build/ a layered “virtual install” via symlinks based on chosen load order
  • Scripts:

    • mods-index.sh to parse archives, extract metadata, and build JSON
    • mods-conflicts.sh to detect file overlaps
    • mods-ai-order.sh to ask a local LLM for an ordered list
    • mods-apply.sh to materialize the load order into a single directory

You can adapt this to any game or engine (Skyrim, Stardew, Minecraft, etc.) by adjusting the “data root” expectations and patterns.


Prerequisites and installation

We’ll use standard CLI tools. Install them using your distro’s package manager.

Required: jq, ripgrep (rg), fd, 7z, unzip, Python 3 (optional), git, curl.

  • Ubuntu/Debian (apt):
sudo apt update
sudo apt install -y jq ripgrep fd-find p7zip-full unzip python3 python3-pip git curl
# fd is named fdfind on Debian/Ubuntu; add a convenience alias:
echo 'alias fd=fdfind' >> ~/.bashrc && source ~/.bashrc
  • Fedora (dnf):
sudo dnf install -y jq ripgrep fd-find p7zip p7zip-plugins unzip python3 python3-pip git curl
  • openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y jq ripgrep fd p7zip unzip python3 python3-pip git curl

Optional: a local LLM runner (Ollama) to keep your workflow offline:

curl -fsSL https://ollama.com/install.sh | sh
# Then pull a suitable model (pick one you like):
ollama pull llama3:instruct
# or
ollama pull qwen2.5:7b-instruct

Note: If you prefer a different local model/runtime, adapt the mods-ai-order.sh script accordingly.


Step 1 — Create your workspace

mkdir -p ~/ai-mods/{mods,index,analysis,build,workspace}
cd ~/ai-mods

Drop your .zip or .7z mod archives into mods/.

We’ll assume “data-root” is the top of each archive (e.g., Data/, assets/, etc.). You can tweak the scripts to your game’s conventions.


Step 2 — Index mods and gather metadata

The script below:

  • Lists contents of each archive (7z)

  • Ingests any likely metadata or readmes

  • Normalizes a per-mod JSON record

  • Emits one combined index/mods.json

Save as mods-index.sh and make it executable.

#!/usr/bin/env bash
set -euo pipefail

ROOT="${1:-$PWD}"
MODDIR="$ROOT/mods"
INDEX="$ROOT/index"
TMP="$ROOT/workspace"

mkdir -p "$INDEX" "$TMP"

# Helper: prefer fd or fdfind transparently
if command -v fd >/dev/null 2>&1; then
  FD=fd
elif command -v fdfind >/dev/null 2>&1; then
  FD=fdfind
else
  echo "fd/fdfind not found. Install fd (see instructions above)." >&2
  exit 1
fi

# List mod archives
mapfile -t ARCHIVES < <($FD -e zip -e 7z . "$MODDIR" -x 'index' -x 'workspace' -x 'analysis')

mods_json="[]"

for arc in "${ARCHIVES[@]}"; do
  name="$(basename "$arc")"
  sha="$(sha256sum "$arc" | awk '{print $1}')"

  # List files in archive
  mapfile -t files < <(7z l -ba -slt "$arc" 2>/dev/null | awk -F'= ' '/^Path = /{print $2}')

  # Try to extract a readme-ish file to text
  readme_text=""
  for rpat in README README.txt readme.txt readme.md CHANGELOG.txt; do
    if printf '%s\n' "${files[@]}" | grep -qiE "^$rpat$|/$rpat$"; then
      # Attempt to extract first match to stdout
      readme_text="$(7z x -so "$arc" "$(printf '%s\n' "${files[@]}" | grep -iE "$rpat" | head -n1)" 2>/dev/null | sed 's/\r$//')"
      break
    fi
  done

  # Heuristic depends_on (scan readme for "Requires:")
  depends=$(printf '%s\n' "$readme_text" | grep -iE '^\s*(requires|dependency|depends on)[: ]' || true)
  # Normalize to a simple array of names (very naive; adjust per game)
  if [ -n "$depends" ]; then
    dep_array=$(printf '%s\n' "$depends" | sed -E 's/.*:(.*)$/\1/i' | tr ',' '\n' | sed -E 's/^\s+|\s+$//g' | sed -E '/^$/d' | jq -R . | jq -s .)
  else
    dep_array="[]"
  fi

  # Provide a normalized list of "game files" (filter out obvious docs)
  game_files=$(printf '%s\n' "${files[@]}" \
    | grep -viE '\.(txt|md|pdf|rtf)$' \
    | grep -viE '(readme|license|changelog)' \
    | jq -R . | jq -s .)

  # Build per-mod JSON
  mod_json=$(jq -n \
    --arg name "$name" \
    --arg path "$arc" \
    --arg sha256 "$sha" \
    --arg readme "$readme_text" \
    --argjson files "$game_files" \
    --argjson depends "$dep_array" \
    '{
      name: $name,
      archive: $path,
      sha256: $sha256,
      depends_on: $depends,
      provides: $files,
      notes: (if ($readme|length)>0 then "readme-present" else "no-readme" end)
    }')

  mods_json=$(jq -n --argjson arr "$mods_json" --argjson item "$mod_json" '$arr + [$item]')
done

printf '%s\n' "$mods_json" | jq '.' > "$INDEX/mods.json"
echo "Wrote $INDEX/mods.json"

Run it:

chmod +x mods-index.sh
./mods-index.sh

You should now have index/mods.json with an array of mod descriptors.


Step 3 — Detect conflicts deterministically

We’ll compute which file paths are provided by more than one mod.

Save as mods-conflicts.sh:

#!/usr/bin/env bash
set -euo pipefail

ROOT="${1:-$PWD}"
IDX="$ROOT/index/mods.json"
OUT="$ROOT/analysis/conflicts.json"
mkdir -p "$(dirname "$OUT")"

if [ ! -f "$IDX" ]; then
  echo "Run mods-index.sh first to generate $IDX" >&2
  exit 1
fi

# Build a map: file -> [mods...]
# Normalize file paths to lowercase for case-insensitive collisions
jq -r '.[] | .name as $n | .provides[] | [$n, (ascii_downcase(.))] | @tsv' "$IDX" \
| awk -F'\t' '
{ file=$2; mod=$1;
  key=file;
  files[key]=files[key] ? files[key] "," mod : mod
}
END{
  print "{"
  first=1
  for (k in files){
    split(files[k], arr, ",")
    if (length(arr)>1){
      if (!first) { print "," }
      printf "\"%s\": [", k
      for (i=1; i<=length(arr); i++){
        printf "%s\"%s\"", (i>1?",":""), arr[i]
      }
      printf "]"
      first=0
    }
  }
  print "\n}"
}' | jq '.' > "$OUT"

echo "Wrote $OUT"

Run it:

chmod +x mods-conflicts.sh
./mods-conflicts.sh

Open analysis/conflicts.json. Any key with multiple mods is a collision that needs order or a patch.


Step 4 — Ask a local LLM for a recommended load order

We’ll prompt the model with:

  • A compact summary of conflicts

  • Each mod’s readme-derived hints and dependencies

  • Instructions to output a JSON load order and notes

First, ensure Ollama is installed and you’ve pulled a model:

# Install (works on most distros)
curl -fsSL https://ollama.com/install.sh | sh
# Pull a small instruct model
ollama pull llama3:instruct

Now save this as mods-ai-order.sh:

#!/usr/bin/env bash
set -euo pipefail

ROOT="${1:-$PWD}"
IDX="$ROOT/index/mods.json"
CONFLICTS="$ROOT/analysis/conflicts.json"
AI_OUT="$ROOT/analysis/ai_order_raw.txt"
ORDER_JSON="$ROOT/analysis/ai_order.json"
MODEL="${MODEL:-llama3:instruct}"

if ! command -v ollama >/dev/null 2>&1; then
  echo "ollama not found. Install it: curl -fsSL https://ollama.com/install.sh | sh" >&2
  exit 1
fi

if [ ! -f "$IDX" ] || [ ! -f "$CONFLICTS" ]; then
  echo "Missing index or conflicts. Run: ./mods-index.sh && ./mods-conflicts.sh" >&2
  exit 1
fi

# Compact context to avoid overly long prompts
mods_compact=$(jq '[.[] | {name, depends_on, notes, provides: (.provides | map(ascii_downcase) | .[0:100])}]' "$IDX")
conflicts_compact=$(jq 'to_entries | map({file: .key, mods: .value}) | .[0:500]' "$CONFLICTS")

read -r -d '' PROMPT <<'EOF'
You are helping organize a mod load order. Output strict JSON only with this schema:
{
  "load_order": ["mod1.zip", "mod2.7z", "..."],  // descending priority: later overrides earlier
  "rationale": "short explanation of key decisions",
  "warnings": ["any potential issues users should check"]
}

Guidelines:

- Respect declared dependencies: a dependency must load BEFORE the depender.

- If two mods conflict on the same file, prefer patches or newer/HD versions later.

- If a mod looks like a patch/fix for another, place it AFTER the base mod.

- If unsure, group similar mods and order by specificity (patches last).

- Do not invent mod names. Only use names provided.

Return JSON only. No code fences, no commentary.
EOF

full_prompt=$(jq -n \
  --arg sys "$PROMPT" \
  --argjson mods "$mods_compact" \
  --argjson conflicts "$conflicts_compact" \
  '{
    instruction: $sys,
    mods: $mods,
    conflicts: $conflicts
  }' | jq -c .)

# Ask the model
ollama run "$MODEL" -p "$full_prompt" > "$AI_OUT" || true

# Extract JSON from model output (strip accidental code fences/markdown)
clean=$(cat "$AI_OUT" \
  | sed -e 's/^```json$//' -e 's/^```$//' -e 's/```//g' \
  | awk 'BEGIN{p=0}{if ($0 ~ /^{/) p=1; if(p) print}' )

# Validate JSON and only keep known mods in the order
if echo "$clean" | jq . >/dev/null 2>&1; then
  known=$(jq -r '.[].name' "$IDX")
  # Filter to known mods
  echo "$clean" \
  | jq --argjson names "$(jq -r '.[].name' "$IDX" | jq -R . | jq -s .)" '
    .load_order = (.load_order | map(select(IN(. ; $names[]))))
  ' > "$ORDER_JSON"
  echo "Wrote $ORDER_JSON"
else
  echo "Model did not return valid JSON. See $AI_OUT for details." >&2
  exit 1
fi

Run it:

chmod +x mods-ai-order.sh
./mods-ai-order.sh

Check analysis/ai_order.json for:

  • load_order: array of mod archive names (earliest to latest; latest wins)

  • rationale and warnings to keep as documentation in your repo

Tip: Commit both the prompt input and the model’s output so the decision trail is transparent and reproducible.


Step 5 — Apply the load order via symlinks

We’ll construct a final “merged” directory where later mods override earlier ones by re-linking files in order. This is fast and undoable.

Save as mods-apply.sh:

#!/usr/bin/env bash
set -euo pipefail

ROOT="${1:-$PWD}"
ORDER_JSON="$ROOT/analysis/ai_order.json"
BUILD="$ROOT/build"
TMP="$ROOT/workspace/extract"

if [ ! -f "$ORDER_JSON" ]; then
  echo "Missing $ORDER_JSON. Run ./mods-ai-order.sh first." >&2
  exit 1
fi

mkdir -p "$BUILD" "$TMP"
rm -rf "$BUILD"/*
rm -rf "$TMP"/*

# Extract each mod in order to a temp directory and then lay down symlinks
mapfile -t ORDER < <(jq -r '.load_order[]' "$ORDER_JSON")

for modname in "${ORDER[@]}"; do
  arc=$(jq -r --arg n "$modname" '.[] | select(.name==$n) | .archive' "$ROOT/index/mods.json")
  [ -n "$arc" ] || { echo "Archive not found for $modname"; exit 1; }

  dest="$TMP/$modname.extract"
  mkdir -p "$dest"

  # Extract preserving paths
  7z x -y -o"$dest" "$arc" >/dev/null

  # Link files into build, overriding earlier links
  # Adjust filters to your game’s "data root" if needed
  while IFS= read -r -d '' f; do
    rel="${f#$dest/}"
    mkdir -p "$BUILD/$(dirname "$rel")"
    # Overwrite any existing link/file
    ln -snf "$f" "$BUILD/$rel"
  done < <(find "$dest" -type f -print0)
done

echo "Build ready at $BUILD"

Run it:

chmod +x mods-apply.sh
./mods-apply.sh

Point your game/mod loader to build/ as the content directory, or copy files from there if symlinks aren’t supported.


Real-world example pattern

  • “HD Textures.zip” vs “HD Textures Patch.zip” vs “Quest Fixes.7z”
    • Conflicts show several .dds files present in both “HD Textures” and “HD Textures Patch”
    • The AI suggests:
    • Load order: HD Textures.zip, HD Textures Patch.zip, Quest Fixes.7z
    • Rationale: The patch should override the base textures; quest fixes don’t overlap textures
    • Warnings: Verify patch version matches base textures version X.Y

You retain the reproducible artifacts:

  • index/mods.jsoncaptures what was scanned and how

  • analysis/conflicts.json captures objective overlaps

  • analysis/ai_order.json captures the AI recommendation and reasoning

  • build/ reflects the chosen order at a glance


Tips, guardrails, and extensions

  • Treat AI as an advisor. Always review its output. Keep “rationale” and “warnings” checked into version control.

  • Pin your model and keep the exact prompt; this helps reproducibility.

  • Add rules: a local “policy.json” with known mandatory orderings or blacklists; merge those with AI output.

  • Enrich indexing: parse game-specific manifests (e.g., modinfo.json, manifest.json) and prioritize them over readme heuristics.

  • Continuous integration: run mods-index.sh, mods-conflicts.sh, and a non-interactive AI job on PRs that add/update archives.


Conclusion and next steps

You now have a practical, Linux-first workflow that blends Bash’s determinism with AI’s reasoning to tame mod chaos:

  • Index mods → Detect conflicts → Ask AI → Apply order via symlinks

Next steps:

  • Drop your mod archives into mods/ and run the four scripts in sequence.

  • Customize conflict rules and data-root filters for your specific game.

  • Version-control index/ and analysis/ so your modlist is auditable and shareable.

If this helped, adapt the scripts to your favorite game and share your tweaks—let’s make Linux modding both smart and reproducible.