
Brewdoc:Auto Sync
- 19 installs
- 29 repo stars
- Updated August 2, 2026
- kochetkov-ma/claude-brewcode
Helps with ai & agent building tasks.
About
brewdoc:auto-sync is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- brewdoc:auto-sync
- AI & Agent Building
- AI-coding skill
Brewdoc:Auto Sync by the numbers
- 19 all-time installs (skills.sh)
- Ranked #10,587 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kochetkov-ma/claude-brewcode --skill brewdocauto-syncAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 29 |
| Last updated | August 2, 2026 |
| Repository | kochetkov-ma/claude-brewcode ↗ |
What it does
Helps with ai & agent building tasks.
Files
Auto-Sync
<instructions>
Mode Detection
EXECUTE using Bash tool (args: $ARGUMENTS):
bash "${CLAUDE_SKILL_DIR}/scripts/detect-mode.sh" $ARGUMENTSParse output: MODE|ARG|FLAGS. If exit code non-zero → report error, EXIT.
| Mode | Trigger | Scope |
|---|---|---|
| STATUS | status | Report INDEX state → EXIT |
| INIT | init <path> | Tag file + add to INDEX → EXIT |
| GLOBAL | global | ~/.claude/** (excludes managed dirs) |
| PROJECT | empty | .claude/** (excludes managed dirs) |
| FILE | file path | Single file |
| FOLDER | folder path | All .md in folder |
Managed directories (excluded from auto-scan, explicit path required):
rules/— sync via/brewdoc:auto-sync .claude/rulesagents/— sync via/brewdoc:auto-sync .claude/agentsskills/— sync via/brewdoc:auto-sync .claude/skills
INDEX Format
{"p":"skills/auth/SKILL.md","t":"skill","u":"2026-02-05","pr":"default"}| Field | Description |
|---|---|
p | Relative path |
t | Type: skill/agent/rule/config/doc |
u | Last sync date (YYYY-MM-DD) |
pr | Protocol: default/override |
Paths: Project .claude/auto-sync/INDEX.jsonl (primary) | Global ${BD_PLUGIN_DATA}/auto-sync/INDEX.jsonl (plugin data dir — used for GLOBAL mode because ~/.claude/* is blocked by protected-path policy)
Frontmatter Fields
Required (3):
auto-sync: enabled
auto-sync-date: 2026-02-05
auto-sync-type: skillOptional override (multiline YAML):
auto-sync-override: |
sources: src/**/*.ts, .claude/agents/*.md
focus: API endpoints, error handling
preserve: ## User Notes, ## Custom ConfigOverride Field
When auto-sync-override: present in frontmatter → INDEX gets pr: "override".
Stored in frontmatter only — never in document body.
</instructions>
<phase name="status">
1. Read INDEX.jsonl, verify indexed files exist 2. Find all .md files in scope 3. Compare indexed vs found → identify non-indexed 4. Detect type for non-indexed (discover.sh typed) — output: TYPE|PATH per line 5. Output report: Indexed (path, type, protocol, last sync, stale), Non-Indexed (path, detected type, reason), Summary (counts) 6. EXIT
</phase>
<phase name="init">
Input: init <path>
1. Read <path> — if NOT found → error, EXIT 2. If has auto-sync: enabled → "Already tagged", EXIT 3. Detect type via discover.sh 4. Add frontmatter: auto-sync: enabled, auto-sync-date: {today}, auto-sync-type: {type} 5. Check frontmatter auto-sync-override: → set pr: override|default 6. Add to INDEX.jsonl 7. Output: path, type, protocol; EXIT
</phase>
<phase name="sync">
Sync Mode (PROJECT/GLOBAL/FILE/FOLDER)
Phase 1: Setup INDEX
EXECUTE using Bash tool:
SCOPE="project" # or "global"
# Primary: project-relative .claude/auto-sync/ (required — ~/.claude/* is
# blocked by Claude Code's protected-path policy in headless sessions).
INDEX_DIR=".claude/auto-sync"
if [ "$SCOPE" = "global" ]; then
# GLOBAL mode scans ~/.claude/** (read-only) but writes its INDEX to
# the plugin data dir, NOT to ~/.claude/auto-sync (blocked path).
INDEX_DIR="${BD_PLUGIN_DATA:-$HOME/.claude/brewdoc}/auto-sync"
fi
mkdir -p "$INDEX_DIR" && INDEX_FILE="$INDEX_DIR/INDEX.jsonl" && touch "$INDEX_FILE"
echo "INDEX=$INDEX_FILE"Phase 2: Discover + Queue (load config: INTERVAL_DAYS, PARALLEL_AGENTS from .claude/tasks/cfg/brewdoc.config.json)
1. Find tagged files — EXECUTE using Bash tool:
bash "${CLAUDE_SKILL_DIR}/scripts/discover.sh" "$SCOPE_PATH" typedOutput: TYPE|PATH per line (types: skill, agent, rule, config, doc). Capped at MAX_FILES (default 50).
2. For each file not in INDEX → auto-add:
- Read file, use type from discover output
- If no frontmatter → add
auto-sync: enabled,auto-sync-date,auto-sync-type - Check
<auto-sync-override>→ setpr - Add to INDEX (
index-ops.sh add)
3. Find stale entries — EXECUTE using Bash tool:
bash "${CLAUDE_SKILL_DIR}/scripts/index-ops.sh" stale "$INDEX_FILE" "$INTERVAL_DAYS"4. Queue: new + stale files
Phase 3: Process + Report
1. Launch bd-auto-sync-processor agents (max PARALLEL_AGENTS batches, model="sonnet"):
Task(subagent_type="brewdoc:bd-auto-sync-processor",
prompt="PATH: {path} | TYPE: {type} | FLAGS: {flags}")Context: BD_PLUGIN_ROOT is injected into agent prompt by pre-task.mjs hook.
2. For each result:
- If status =
updatedorunchanged→ update INDEXuto today (index-ops.sh update) - If status =
error→ log to Errors table, do NOT update INDEX (file remains stale for retry)
3. Output report:
## Auto-Sync Complete
| Metric | Count |
|--------|-------|
| Discovered | {N} |
| Queued (stale/new) | {N} |
| Updated | {N} |
| Unchanged | {N} |
| Errors | {N} |
### Updated
| Path | Type | Changes |
|------|------|---------|
### Errors
| Path | Error |
|------|-------|</phase>
Error Handling
| Error | Action |
|---|---|
| INDEX corrupt | Rebuild from discovery |
| File not found | Skip, add to errors |
| Agent timeout | Retry once |
| No tagged files | Report "0 found" |
/brewdoc:doc called | "Use /brewdoc:auto-sync" |
LLM Text Rules
| Rule | Details |
|---|---|
| Tables over prose, bullets over numbered | Multi-column ~66% savings, bullets when order irrelevant |
code over text, inline over blocks | Identifiers, paths, short values; blocks only if >3 lines |
| Comma-separated inline lists | a, b, c not bullet per item when saving space |
| One-liner rules, arrows for flow | old -> new, conditions with -> (~40% savings) |
| No filler, no water | Cut "please note", "it's important", "only", "exactly", "basically" |
| Positive framing, no aggressive lang | "Do Y" not "Don't X"; "Use when..." not "CRITICAL: MUST..." |
| Imperative form | "Do X" not "You should do X"; 3rd person for descriptions |
| Bold for key terms, no extra formatting | **term** for emphasis; no decorative lines, headers, dividers |
| No emojis except status markers | Only 3 allowed: ✅, ❌, ⚠️ |
| Merge duplicates, abbreviate in tables | Single source of truth; REQ, impl, cfg, args, ret, err |
Sync Instructions: Agent
Verification Checklist
| Check | How |
|---|---|
| Tools list valid | Each tool in tools: frontmatter exists in Claude Code |
| Model appropriate | model: matches complexity (opus for complex, sonnet for routine) |
| Workflow matches patterns | Compare workflow steps to actual hook/skill implementations |
| Input/Output format current | Verify JSON schemas match actual usage in callers |
| Responsibilities complete | Cross-reference with hooks that invoke this agent |
| Related agents referenced | Check See also links point to existing agents |
Research Directions
| Signal | Tool | Focus |
|---|---|---|
Hook references (hooks/*.mjs) | Grep | Find hooks that call this agent |
| Agent cross-refs | Glob | Verify referenced agents exist |
| Claude Code features | Grep | Verify tool/model references |
LLM Text Rules
See instructions/llm-text-rules.md for the full rules table.Update Rules
- Preserve identity (name, purpose)
- Update workflow if patterns evolved
- Sync tool list with needed capabilities
- Keep output format stable (consumers depend on it)
- Verify frontmatter
permissionMode - Do NOT change responsibilities scope
- Respect
preserve:— if frontmatter hasauto-sync-override:withpreserve:field, never modify those sections
Sync Instructions: Config
Verification Checklist
| Check | How |
|---|---|
| Project structure accurate | Verify directory tree matches actual layout |
| Paths and commands valid | Test referenced paths exist, commands have correct flags |
| Config examples current | Compare JSON/YAML examples with actual config files |
| Integration points exist | Verify referenced hooks, skills, agents are present |
| Environment variables valid | Check referenced env vars are documented/used |
| Version info current | Compare versions with plugin.json, package.json |
Research Directions
| Signal | Tool | Focus |
|---|---|---|
| Path references | Glob | Verify all paths exist on disk |
| Command references | Grep | Verify commands match implementations |
| Plugin/tool references | Grep | Verify against current Claude Code features |
LLM Text Rules
See instructions/llm-text-rules.md for the full rules table.Update Rules
- Preserve CLAUDE.md structure (## sections in order)
- Update facts: paths, commands, versions, config fields
- Preserve custom user sections
- Keep table formats consistent
- Verify command syntax
- Do NOT restructure sections
- Respect
preserve:override — if document has<auto-sync-override>withpreserve:field, never modify those sections
Sync Instructions: Doc
Verification Checklist
| Check | How |
|---|---|
| File references valid | Glob/Read all paths mentioned in document |
| URLs accessible | WebFetch URLs mentioned, check for 200 response |
| Structure descriptions match | Verify directory trees, file lists match actual state |
| Command examples work | Verify CLI commands and flags are valid |
| Version numbers current | Check versions against package.json, plugin.json, etc. |
| Cross-references valid | Verify links to other docs point to existing files |
Research Directions
| Signal | Tool | Focus |
|---|---|---|
| URLs in document | WebFetch | Check URLs, extract updates |
| File paths | Glob + Grep | Verify paths exist, content matches |
| Version numbers | Read | Compare with source-of-truth files |
LLM Text Rules
See instructions/llm-text-rules.md for the full rules table.Update Rules
- Preserve structure and formatting
- Update facts (paths, versions, URLs, names)
- Preserve user sections (## User Notes, ## Custom)
- Do NOT remove content — mark stale inline if needed
- Update directory trees to match structure
- Keep tone consistent
- Respect
preserve:— if frontmatter hasauto-sync-override:withpreserve:field, never modify those sections
Sync Instructions: Rule
Verification Checklist
| Check | How |
|---|---|
| Patterns still valid | Grep codebase for each pattern/anti-pattern mentioned |
| File refs exist | Verify all referenced file paths exist on disk |
| Rules not contradictory | Cross-check with other rules in same directory |
| KNOWLEDGE alignment | Compare with KNOWLEDGE.jsonl entries |
| Examples current | Verify code examples match actual codebase |
Research Directions
| Signal | Tool | Focus |
|---|---|---|
| Code patterns mentioned | Grep | Verify patterns exist in codebase |
| KNOWLEDGE references | Read | Cross-check with KNOWLEDGE.jsonl |
| Best practice claims | WebFetch | Verify against current practices |
LLM Text Rules
See instructions/llm-text-rules.md for the full rules table.Update Rules
- Preserve table structure (#, Rule/Avoid, Instead/Context, Why/Source)
- Update facts only (paths, patterns, values)
- Do NOT add rules (KNOWLEDGE manager's job)
- Do NOT remove rules (mark outdated instead)
- Keep numbering sequential
- Respect
preserve:— if frontmatter hasauto-sync-override:withpreserve:field, never modify those sections
Sync Instructions: Skill
Verification Checklist
| Check | How |
|---|---|
| Name matches path | Frontmatter name: matches skills/{name}/SKILL.md pattern |
| Description accurate | Compare description to actual instructions content |
| Tools list valid | Each tool in allowed-tools: is a real Claude Code tool |
| Referenced files exist | Glob/Grep all file paths mentioned in instructions |
| Code examples current | Compare inline code snippets to actual source files |
| Bash commands work | Verify script paths and variable names |
Research Directions
| Signal | Tool | Focus |
|---|---|---|
Script paths (*.sh, *.mjs) | Glob + Read | Verify scripts exist, check argument formats |
| Tool references | Grep | Verify tool names and parameters |
File patterns (**/*.ts) | Glob | Verify patterns match actual files |
LLM Text Rules
See instructions/llm-text-rules.md for the full rules table.Update Rules
- Preserve
<instructions>,<phase>structure exactly - Update facts: paths, names, field values
- Preserve
EXECUTEblocks — verify but don't rephrase - Keep frontmatter order
- Update examples to match files
- Do NOT change tone or structure
- Respect
preserve:— if frontmatter hasauto-sync-override:withpreserve:field, never modify those sections
MIT License
Copyright (c) 2025-2026 Maxim Kochetkov (kochetkov-ma)
https://github.com/kochetkov-ma/claude-brewcode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Auto-Sync
Keeps Claude Code documentation (skills, agents, rules, configs, markdown) in sync with your codebase. Discovers files tagged with auto-sync: enabled frontmatter, detects stale content, and updates it automatically.
Quick Start
/brewdoc:auto-sync # sync all project docs (.claude/**)
/brewdoc:auto-sync status # see what is tracked vs stale
/brewdoc:auto-sync init path/to.md # tag a file and add it to the index
/brewdoc:auto-sync global # sync global docs (~/.claude/**)Modes
| Mode | Trigger | What it does |
|---|---|---|
| STATUS | status | Reads INDEX, compares against discovered files, reports tracked/stale/non-indexed. No changes. |
| INIT | init <path> | Adds auto-sync frontmatter to the file and registers it in INDEX. |
| PROJECT | no args | Discovers all tagged .md files under .claude/**, syncs stale ones. |
| GLOBAL | global | Same as PROJECT but scoped to ~/.claude/**. |
| FILE | path/to/file.md | Syncs a single file regardless of its stale status. |
| FOLDER | path/to/folder | Syncs all .md files inside the given folder. |
Managed directories (rules/, agents/, skills/) are excluded from automatic PROJECT/GLOBAL scans. Target them explicitly when needed (see examples below).
Examples
Good Usage
Check what needs attention:
/brewdoc:auto-sync statusTag a new file for tracking:
/brewdoc:auto-sync init .claude/agents/my-agent.mdSync the entire project (default 7-day staleness window):
/brewdoc:auto-syncSync a managed directory explicitly:
/brewdoc:auto-sync .claude/rulesSync global docs with text optimization enabled:
/brewdoc:auto-sync global -oCommon Mistakes
Expecting managed dirs to be included in a bare sync -- rules/, agents/, skills/ are excluded from auto-scan. Pass the path explicitly:
# wrong -- will skip .claude/rules/
/brewdoc:auto-sync
# correct
/brewdoc:auto-sync .claude/rulesRunning init without a path:
# wrong -- exits with error
/brewdoc:auto-sync init
# correct
/brewdoc:auto-sync init docs/guide.mdForgetting frontmatter on manually created files -- files without the auto-sync: enabled tag will not be discovered. Use init to add it, or add the three required fields by hand.
How File Discovery Works
A file is discovered when it contains this YAML frontmatter block:
auto-sync: enabled
auto-sync-date: 2026-04-01
auto-sync-type: skillThe type is inferred from the file path:
| Path contains | Detected type |
|---|---|
skills/ | skill |
agents/ | agent |
rules/ | rule |
filename is CLAUDE.md | config |
| anything else | doc |
Discovery is capped at 50 files per scan (override with MAX_FILES env var).
Output
INDEX location:
| Scope | Path |
|---|---|
| Project | .claude/auto-sync/INDEX.jsonl |
| Global | ${BD_PLUGIN_DATA}/auto-sync/INDEX.jsonl |
Each line in INDEX is a JSON object:
{"p":"skills/auth/SKILL.md","t":"skill","u":"2026-04-01","pr":"default"}| Field | Meaning |
|---|---|
p | Relative path to the file |
t | Type: skill, agent, rule, config, doc |
u | Last sync date (YYYY-MM-DD) |
pr | Protocol: default or override |
After a sync run, the skill prints a summary table with counts for discovered, queued, updated, unchanged, and errored files.
Tips
- Run
statusfirst to understand what is tracked before launching a full sync. - Use the
-o/--optimizeflag to enable text optimization during sync -- useful for reducing token usage in large skill files. - Add an
auto-sync-overrideblock in frontmatter to control which source files and sections the sync agent examines:
auto-sync-override: |
sources: src/**/*.ts, .claude/agents/*.md
focus: API endpoints, error handling
preserve: ## User Notes, ## Custom Config- Files that error during sync are not marked as updated in INDEX, so they remain stale and will be retried on the next run.
Documentation
Full docs: auto-sync
#!/bin/sh
# Detects auto-sync mode from arguments
# Usage: detect-mode.sh "$@"
# Output: MODE|ARG|FLAGS (e.g., "STATUS||" or "PROJECT||optimize")
#
# Modes:
# STATUS||[flags] - "status"
# INIT|<path>|[flags] - "init <path> [prompt]"
# GLOBAL||[flags] - "global"
# PROJECT||[flags] - empty args
# FILE|<path>|[flags] - single file path (*.md)
# FOLDER|<path>|[flags] - folder path
#
# Flags:
# -o, --optimize - Enable optimization (adds "optimize" to FLAGS)
set -e
# --- Phase 1: Extract flags from arguments ---
FLAGS=""
CLEAN_ARGS=""
for arg in "$@"; do
case "$arg" in
-o|--optimize)
FLAGS="optimize"
;;
*)
if [ -z "$CLEAN_ARGS" ]; then
CLEAN_ARGS="$arg"
else
CLEAN_ARGS="$CLEAN_ARGS $arg"
fi
;;
esac
done
# --- Phase 2: Detect mode from cleaned arguments ---
# Normalize: lowercase, trim whitespace
ARGS_LOWER=$(echo "$CLEAN_ARGS" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
# Empty args = PROJECT mode
if [ -z "$ARGS_LOWER" ]; then
echo "PROJECT||$FLAGS"
exit 0
fi
# Check for "status"
if [ "$ARGS_LOWER" = "status" ]; then
echo "STATUS||$FLAGS"
exit 0
fi
# Check for bare "init" without arguments
if [ "$ARGS_LOWER" = "init" ]; then
echo "ERROR|init requires a path argument|$FLAGS" >&2
exit 1
fi
# Check for "init <path>"
if echo "$ARGS_LOWER" | grep -qE '^init[[:space:]]+'; then
INIT_ARGS=$(echo "$CLEAN_ARGS" | sed 's/^[^[:space:]][^[:space:]]*[[:space:]][[:space:]]*//')
echo "INIT|$INIT_ARGS|$FLAGS"
exit 0
fi
# Check for "global"
if [ "$ARGS_LOWER" = "global" ]; then
echo "GLOBAL||$FLAGS"
exit 0
fi
# Check if it's a file path (ends with .md)
if echo "$CLEAN_ARGS" | grep -qE '\.md$'; then
echo "FILE|$CLEAN_ARGS|$FLAGS"
exit 0
fi
# Check if it's a folder path
if [ -d "$CLEAN_ARGS" ]; then
echo "FOLDER|$CLEAN_ARGS|$FLAGS"
exit 0
fi
# Check if path looks like a folder (contains / but no .md)
if echo "$CLEAN_ARGS" | grep -qE '^[./~]' && ! echo "$CLEAN_ARGS" | grep -qE '\.md$'; then
echo "FOLDER|$CLEAN_ARGS|$FLAGS"
exit 0
fi
# Default: treat as PROJECT mode with the args as context
echo "WARN: unrecognized argument '$CLEAN_ARGS', defaulting to PROJECT mode" >&2
echo "PROJECT|$CLEAN_ARGS|$FLAGS"
#!/bin/sh
# Discovers files with auto-sync enabled
# Usage: discover.sh [search_path] [output_format]
# search_path - Directory to search (default: .)
# output_format - paths (default) | json | typed
set -e
SEARCH_PATH="${1:-.}"
OUTPUT_FORMAT="${2:-paths}"
MAX_FILES="${MAX_FILES:-50}"
# Explicit path detection: if path contains rules/, agents/, skills/ - user explicitly requested it
is_explicit_managed_path() {
case "$SEARCH_PATH" in
*/rules|*/rules/*|*/agents|*/agents/*|*/skills|*/skills/*) return 0 ;;
*) return 1 ;;
esac
}
# Build exclusion args for managed directories (only when auto-scanning)
build_exclusions() {
if is_explicit_managed_path; then
echo "" # No exclusions for explicit paths
else
# Exclude rules/, agents/, skills/ from auto-scan (only by explicit request)
echo "-not -path */rules/* -not -path */agents/* -not -path */skills/*"
fi
}
# Find files with auto-sync: enabled (YAML frontmatter only)
find_autosync_files() {
_exclusions=$(build_exclusions)
# shellcheck disable=SC2086
_all=$(find "$SEARCH_PATH" -name "*.md" \
-not -path '*/.git/*' \
-not -path '*/node_modules/*' \
-not -path '*/.claude/tasks/*' \
$_exclusions \
-exec grep -lE '^auto-sync:[[:space:]]*enabled' {} + \
2>/dev/null | sort -u || true)
_count=$(echo "$_all" | grep -c . || true)
if [ "$_count" -gt "$MAX_FILES" ]; then
echo "WARN: found $_count files, capped to $MAX_FILES (set MAX_FILES to override)" >&2
echo "$_all" | head -n "$MAX_FILES"
else
echo "$_all"
fi
}
# Detect type from file path
detect_type() {
_path="$1"
_base=$(basename "$_path")
if [ "$_base" = "CLAUDE.md" ]; then
echo "config"
elif echo "$_path" | grep -qE '(^|/)skills/'; then
echo "skill"
elif echo "$_path" | grep -qE '(^|/)agents/'; then
echo "agent"
elif echo "$_path" | grep -qE '(^|/)rules/'; then
echo "rule"
else
echo "doc"
fi
}
# Output as paths (one per line)
output_paths() {
find_autosync_files
}
# Output as JSON array
output_json() {
FILES=$(find_autosync_files)
if [ -z "$FILES" ]; then
echo "[]"
return
fi
echo "["
echo "$FILES" | sed 's/.*/ "&"/' | sed '$!s/$/,/'
echo "]"
}
# Output as typed (TYPE|PATH per line)
output_typed() {
FILES=$(find_autosync_files)
if [ -z "$FILES" ]; then
return
fi
echo "$FILES" | while IFS= read -r file; do
_type=$(detect_type "$file")
echo "${_type}|${file}"
done
}
# Main
case "$OUTPUT_FORMAT" in
paths)
output_paths
;;
json)
output_json
;;
typed)
output_typed
;;
*)
echo "Usage: discover.sh [search_path] [output_format]" >&2
echo " search_path - Directory to search (default: .)" >&2
echo " output_format - paths (default) | json | typed" >&2
exit 1
;;
esac
#!/bin/sh
# INDEX operations for auto-sync
# Usage: index-ops.sh <command> [args...]
#
# Format: {"p":"path","t":"type","u":"YYYY-MM-DD","pr":"default"}
# p = relative path (identity key)
# t = type (skill/agent/rule/config/doc)
# u = last sync date (YYYY-MM-DD)
# pr = protocol ("default" or "override")
#
# Commands:
# read <index_path> - Read and validate INDEX
# add <index_path> <json_entry> - Add entry from JSON
# add <index_path> <path> <type> [protocol] - Add entry from positional args (date=today)
# update <index_path> <path> <field> <val> - Update entry field by path
# remove <index_path> <path> - Remove entry by path
# stale <index_path> [days] - Find stale entries (default: 7)
set -e
CMD="${1:-help}"
shift 2>/dev/null || true
require_jq() {
if ! command -v jq >/dev/null 2>&1; then
echo "X jq is required but not installed" >&2
echo " Install: brew install jq" >&2
exit 1
fi
}
threshold_date() {
days="$1"
if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then
date -v-"${days}"d +%Y-%m-%d
else
date -d "-${days} days" +%Y-%m-%d
fi
}
cmd_read() {
index_path="${1:-}"
if [ -z "$index_path" ]; then
echo "X Missing index_path" >&2
echo "Usage: index-ops.sh read <index_path>" >&2
exit 1
fi
if [ ! -f "$index_path" ]; then
echo "X INDEX not found: $index_path" >&2
exit 1
fi
require_jq
grep -v "^#" "$index_path" | while IFS= read -r line; do
if [ -n "$line" ]; then
echo "$line" | jq -c '.' 2>/dev/null || echo "X Invalid JSON: $line" >&2
fi
done
}
cmd_add() {
index_path="${1:-}"
arg2="${2:-}"
arg3="${3:-}"
arg4="${4:-default}"
if [ -z "$index_path" ] || [ -z "$arg2" ]; then
echo "X Missing arguments" >&2
echo "Usage: index-ops.sh add <index_path> <json_entry>" >&2
echo " index-ops.sh add <index_path> <path> <type> [protocol]" >&2
exit 1
fi
require_jq
# Detect format: JSON (starts with {) or positional args
case "$arg2" in
\{*)
# JSON format
entry="$arg2"
if ! echo "$entry" | jq -e '.' >/dev/null 2>&1; then
echo "X Invalid JSON entry" >&2
exit 1
fi
;;
*)
# Positional format: <path> <type> [protocol]
if [ -z "$arg3" ]; then
echo "X Missing type argument" >&2
echo "Usage: index-ops.sh add <index_path> <path> <type> [protocol]" >&2
exit 1
fi
today=$(date +%Y-%m-%d)
entry=$(jq -nc --arg p "$arg2" --arg t "$arg3" --arg u "$today" --arg pr "$arg4" \
'{p:$p, t:$t, u:$u, pr:$pr}')
;;
esac
path=$(echo "$entry" | jq -r '.p')
if [ -f "$index_path" ] && grep -v "^#" "$index_path" 2>/dev/null | jq -r '.p' 2>/dev/null | grep -qxF "$path"; then
echo "X Entry already exists: $path" >&2
echo " Use 'update' to modify existing entries" >&2
exit 1
fi
echo "$entry" >> "$index_path"
echo "V Added: $path"
}
cmd_update() {
# NOTE: read-modify-write is NOT atomic. Callers must invoke sequentially (SKILL.md guarantees this).
index_path="${1:-}"
path="${2:-}"
field="${3:-}"
value="${4:-}"
if [ -z "$index_path" ] || [ -z "$path" ] || [ -z "$field" ] || [ -z "$value" ]; then
echo "X Missing arguments" >&2
echo "Usage: index-ops.sh update <index_path> <path> <field> <value>" >&2
exit 1
fi
case "$field" in
p|t|u|pr) ;;
*) echo "X Invalid field: $field (allowed: p, t, u, pr)" >&2; exit 1 ;;
esac
require_jq
tmp_file=$(mktemp)
trap 'rm -f "$tmp_file"' EXIT
found=false
while IFS= read -r line; do
case "$line" in
\#*) echo "$line" ;;
"") ;; # Empty lines dropped intentionally — JSONL has no empty-line semantics
*)
entry_path=$(echo "$line" | jq -r '.p' 2>/dev/null)
if [ "$entry_path" = "$path" ]; then
echo "$line" | jq -c --arg v "$value" ".${field} = \$v"
found=true
else
echo "$line"
fi
;;
esac
done < "$index_path" > "$tmp_file"
if [ "$found" = true ]; then
mv "$tmp_file" "$index_path"
echo "V Updated: $path.$field = $value"
else
rm "$tmp_file"
echo "X Entry not found: $path" >&2
exit 1
fi
}
cmd_remove() {
index_path="${1:-}"
path="${2:-}"
if [ -z "$index_path" ] || [ -z "$path" ]; then
echo "X Missing arguments" >&2
echo "Usage: index-ops.sh remove <index_path> <path>" >&2
exit 1
fi
require_jq
tmp_file=$(mktemp)
trap 'rm -f "$tmp_file"' EXIT
found=false
while IFS= read -r line; do
case "$line" in
\#*) echo "$line" ;;
"") ;; # Empty lines dropped intentionally — JSONL has no empty-line semantics
*)
entry_path=$(echo "$line" | jq -r '.p' 2>/dev/null)
if [ "$entry_path" = "$path" ]; then
found=true
else
echo "$line"
fi
;;
esac
done < "$index_path" > "$tmp_file"
if [ "$found" = true ]; then
mv "$tmp_file" "$index_path"
echo "V Removed: $path"
else
rm "$tmp_file"
echo "X Entry not found: $path" >&2
exit 1
fi
}
cmd_stale() {
index_path="${1:-}"
days="${2:-7}"
if [ -z "$index_path" ]; then
echo "X Missing index_path" >&2
echo "Usage: index-ops.sh stale <index_path> [days]" >&2
exit 1
fi
case "$days" in
''|*[!0-9]*) echo "X days must be a positive integer, got: $days" >&2; exit 1 ;;
0) echo "X days must be > 0, got: 0" >&2; exit 1 ;;
esac
if [ ! -f "$index_path" ]; then
echo "X INDEX not found: $index_path" >&2
exit 1
fi
require_jq
cutoff=$(threshold_date "$days")
grep -v "^#" "$index_path" 2>/dev/null | while IFS= read -r line; do
if [ -n "$line" ]; then
entry_date=$(echo "$line" | jq -r '.u // empty' 2>/dev/null)
if [ -z "$entry_date" ]; then
echo "$line"
elif expr "$entry_date" \< "$cutoff" > /dev/null 2>&1; then
echo "$line"
fi
fi
done
}
cmd_help() {
echo "INDEX operations for auto-sync"
echo ""
echo "Usage: index-ops.sh <command> [args...]"
echo ""
echo "Format: {\"p\":\"path\",\"t\":\"type\",\"u\":\"YYYY-MM-DD\",\"pr\":\"default\"}"
echo ""
echo "Commands:"
echo " read <index_path> - Read and validate INDEX"
echo " add <index_path> <json_entry> - Add entry from JSON"
echo " add <index_path> <path> <type> [protocol] - Add entry (date=today, protocol=default)"
echo " update <index_path> <path> <field> <val> - Update entry field by path"
echo " remove <index_path> <path> - Remove entry by path"
echo " stale <index_path> [days] - Find stale entries (default: 7)"
echo ""
echo "Examples:"
echo " index-ops.sh add idx.jsonl '{\"p\":\"file.md\",\"t\":\"doc\",\"u\":\"2026-02-11\",\"pr\":\"default\"}'"
echo " index-ops.sh add idx.jsonl file.md doc"
echo " index-ops.sh add idx.jsonl file.md skill override"
}
case "$CMD" in
read) cmd_read "$@" ;;
add) cmd_add "$@" ;;
update) cmd_update "$@" ;;
remove) cmd_remove "$@" ;;
stale) cmd_stale "$@" ;;
help|--help|-h)
cmd_help
;;
*)
echo "X Unknown command: $CMD" >&2
cmd_help >&2
exit 1
;;
esac