diff --git a/.claude/commands/wiki-compile.md b/.claude/commands/wiki-compile.md new file mode 100644 index 0000000..d29e1ed --- /dev/null +++ b/.claude/commands/wiki-compile.md @@ -0,0 +1,367 @@ +# /wiki-compile — Compile session logs and Syncro data into wiki articles + +Seed new wiki articles or refresh existing ones from session logs, client documents, and live Syncro PSA data. + +--- + +## Usage + +``` +/wiki-compile client: Seed or refresh a client wiki article +/wiki-compile client: --full Force full Ollama recompile of existing article +/wiki-compile project: Compile a project wiki article (no Syncro) +/wiki-compile system: Compile a system wiki article (no Syncro) +/wiki-compile all Process all missing + stale articles +``` + +**Mode auto-detection:** +- If `wiki/clients/.md` **does not exist** → **Seed mode** (full Ollama synthesis) +- If `wiki/clients/.md` **exists** and no `--full` flag → **Refresh mode** (surgical update of dynamic fields only) +- `--full` flag → **Full recompile** (Ollama synthesis, preserves existing Patterns/History) + +--- + +## Phase 0 — Setup + +```bash +CLAUDETOOLS_ROOT="D:/claudetools" +VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" + +# Syncro auth (read-only operations only — GET requests ONLY in this skill) +BASE="https://computerguru.syncromsp.com/api/v1" +USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json") +case "$USER_ID" in + mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;; + howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;; + *) echo "[WARNING] Unknown user — Syncro enrichment skipped" ; API_KEY="" ;; +esac + +# Ollama endpoint +MACHINE=$(jq -r '.machine // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json") +case "$MACHINE" in + DESKTOP-0O8A1RL|GURU-BEAST-ROG) OLLAMA="http://localhost:11434" ;; + *) OLLAMA="http://100.101.122.4:11434" ;; +esac +if ! curl -s -m 3 "$OLLAMA/api/tags" >/dev/null 2>&1; then + echo "[INFO] Ollama unreachable — synthesis will be Claude-direct" + OLLAMA="" +fi +``` + +--- + +## Phase 1 — Argument Parsing + +Parse the target and flags: + +```bash +# Extract type and slug from argument like "client:cascades-tucson" or "client:cascades-tucson --full" +TARGET_RAW="$1" # e.g. "client:cascades-tucson" +FULL_FLAG="${2:-}" # "--full" or empty + +TARGET_TYPE="${TARGET_RAW%%:*}" # client | project | system | all +SLUG="${TARGET_RAW#*:}" # cascades-tucson + +# Determine article path +case "$TARGET_TYPE" in + client) ARTICLE_PATH="wiki/clients/${SLUG}.md" ;; + project) ARTICLE_PATH="wiki/projects/${SLUG}.md" ;; + system) ARTICLE_PATH="wiki/systems/${SLUG}.md" ;; + all) # handled separately — see "all" mode below +esac + +# Mode detection +if [ ! -f "$CLAUDETOOLS_ROOT/$ARTICLE_PATH" ]; then + MODE="seed" +elif [ "$FULL_FLAG" = "--full" ]; then + MODE="full" +else + MODE="refresh" +fi + +echo "[INFO] Mode: $MODE | Target: $TARGET_TYPE:$SLUG" +``` + +--- + +## Phase 2 — Syncro Enrichment (clients only, skip for project/system) + +**Skip this phase entirely if `TARGET_TYPE != client` or `API_KEY` is empty.** + +### 2a — Customer Search + +Convert slug to a Syncro search query: + +```bash +# Replace hyphens with spaces for the search query +SEARCH_QUERY=$(echo "$SLUG" | sed 's/-/ /g') + +CUST_RESULTS=$(curl -s "$BASE/customers?name=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$SEARCH_QUERY")&per_page=5&api_key=$API_KEY") +CUST_COUNT=$(echo "$CUST_RESULTS" | jq '.customers | length') +``` + +**If 0 results:** +``` +[SYNCRO] No customer found matching '${SEARCH_QUERY}' — skipping Syncro enrichment. + Proceeding with session logs only. +``` +Set `SYNCRO_DATA=""` and continue to Phase 3. + +**If 2+ results:** Show the list and PAUSE execution: +``` +[SYNCRO] Multiple customers match '${SEARCH_QUERY}': + 1. () + 2. () + ... + +Which customer should be used for this wiki article? (Enter number, or 'skip' to skip Syncro) +``` +Wait for user input before continuing. If user says `skip`, treat as 0 results. + +**If exactly 1 result:** Proceed immediately. + +### 2b — Customer Profile Pull + +```bash +CUST_ID=$(echo "$CUST_RESULTS" | jq -r '.customers[0].id') + +# Full customer record +CUST=$(curl -s "$BASE/customers/${CUST_ID}?api_key=$API_KEY" | jq '.customer') + +DISPLAY_NAME=$(echo "$CUST" | jq -r '.business_name // .firstname + " " + .lastname') +PREPAY_HOURS=$(echo "$CUST" | jq -r '.prepay_hours // "0"') +NOTES=$(echo "$CUST" | jq -r '.notes // ""') + +# Contacts: name, title, email, phone +CONTACTS=$(echo "$CUST" | jq -r ' + .contacts[]? | + "\(.firstname) \(.lastname)" + + (if .title != "" and .title != null then " (\(.title))" else "" end) + + (if .email != "" and .email != null then " — \(.email)" else "" end) + + (if .mobile != "" and .mobile != null then ", \(.mobile)" elif .phone != "" and .phone != null then ", \(.phone)" else "" end) +') +``` + +### 2c — Open Tickets + +```bash +OPEN_TICKETS=$(curl -s "$BASE/tickets?customer_id=${CUST_ID}&status=New,In+Progress,Scheduled,Waiting+on+Customer&per_page=10&api_key=$API_KEY" | jq '.tickets[]? | {id, number, subject, status, created_at, user_id}') +TICKET_COUNT=$(echo "$OPEN_TICKETS" | jq -s 'length') +``` + +### 2d — Recent Invoices (last 12) + +```bash +RECENT_INVOICES=$(curl -s "$BASE/invoices?customer_id=${CUST_ID}&per_page=12&api_key=$API_KEY" | jq '[.invoices[]? | {id, number, date, total, status}]') +``` + +Used only to infer billing pattern (break-fix vs prepaid, rate hints). Do not expose raw invoice data in the wiki. + +### 2e — Asset Count + +```bash +ASSET_COUNT=$(curl -s "$BASE/customer_assets?customer_id=${CUST_ID}&per_page=200&api_key=$API_KEY" | jq '[.assets[]?] | length') +``` + +Only the count is used — individual asset details go in session logs and client docs, not the wiki. + +--- + +## Phase 3 — Session Log Discovery + +Find all session logs that mention this client: + +```bash +cd "$CLAUDETOOLS_ROOT" + +# 1. Client-specific session logs (all) +CLIENT_LOGS=$(find "clients/${SLUG}/session-logs/" -name "*.md" 2>/dev/null | sort) + +# 2. Client-specific docs (README, CONTEXT.md, overview.md, etc.) +CLIENT_DOCS=$(find "clients/${SLUG}/" -name "*.md" -not -path "*/session-logs/*" 2>/dev/null | sort) + +# 3. Root session logs mentioning this client (case-insensitive grep on slug and display name) +ROOT_LOGS=$(grep -ril "$SLUG\|$(echo "$DISPLAY_NAME" | sed 's/ /\\|/g')" session-logs/*.md 2>/dev/null | sort) + +# 4. Memory files referencing this client +MEMORY_FILES=$(grep -ril "$SLUG" .claude/memory/*.md 2>/dev/null | sort) + +# Deduplicate and collect all source paths +ALL_SOURCES=$(echo "$CLIENT_LOGS $CLIENT_DOCS $ROOT_LOGS $MEMORY_FILES" | tr ' ' '\n' | sort -u | grep -v '^$') +SOURCE_COUNT=$(echo "$ALL_SOURCES" | grep -c '^' || echo 0) +echo "[INFO] Found $SOURCE_COUNT source files" +``` + +If `SOURCE_COUNT == 0` and no Syncro data: warn and stop. +``` +[ERROR] No session logs and no Syncro data found for '${SLUG}'. Cannot compile. + Create at least one session log in clients/${SLUG}/session-logs/ first. +``` + +--- + +## Phase 4 — Article Generation + +### Refresh Mode (existing article, no --full) + +Perform surgical updates only. No Ollama call. Three edits: + +**Edit 1 — Update hours remaining in Profile section:** +Find the `Hours remaining` line and replace with live Syncro value and today's date. Only run if `PREPAY_HOURS` is non-null and non-zero OR if the article currently shows a non-zero balance. + +``` +- **Hours remaining (if prepaid):** ${PREPAY_HOURS} hrs as of $(date +%Y-%m-%d) +``` + +**Edit 2 — Update Active Work ticket list:** +Replace the content of `## Active Work` with the Syncro open tickets formatted as: + +```markdown +## Active Work + +*As of $(date +%Y-%m-%d) — Syncro shows ${TICKET_COUNT} open ticket(s):* + +| Ticket | Subject | Status | Opened | +|---|---|---|---| +| # (ID: ) | | | | +``` + +If `TICKET_COUNT == 0`: +```markdown +## Active Work + +*No open tickets in Syncro as of $(date +%Y-%m-%d). See session logs for recent work.* +``` + +**Edit 3 — Update frontmatter:** +- `last_compiled`: today's date +- `compiled_by`: `/claude-main` +- Append new source files to `sources:` list (deduplicate) + +After edits, emit: +``` +[OK] Refresh complete for wiki/clients/.md + - Hours: updated to ${PREPAY_HOURS} hrs + - Active tickets: ${TICKET_COUNT} open + - Sources: ${SOURCE_COUNT} files tracked +``` + +### Seed Mode / Full Recompile — Ollama Synthesis + +Prepare the synthesis context by reading the most relevant source files. For sessions logs, read the full content of client-specific logs and the first 200 lines of root session logs (to avoid overwhelming the prompt). For full recompile, also read the existing article. + +**Ollama prompt:** + +``` +You are compiling a wiki article for an MSP (managed service provider) client. +Produce a structured Markdown article using the template and source data provided. +Be concise, factual, and technical. No filler phrases. No emojis. Past tense for history. +Mark unknown fields as (verify). + +--- +CLIENT SLUG: +DISPLAY NAME: +COMPILE MODE: + +SYNCRO LIVE DATA: +Customer ID: +Prepaid Hours: +Asset Count: +Open Tickets (): + +Contacts: + +Billing pattern from invoices: + +SESSION LOG EXCERPTS: + + + +EXISTING ARTICLE (preserve Patterns and History, update everything else): + + + +--- +TEMPLATE STRUCTURE TO FOLLOW: + + +--- +RULES: +1. BILLING — Syncro is the authoritative source for ALL billing-related fields. Never use session log values for these: + - Hours remaining: use live `prepay_hours` from Syncro customer record + - Contract type: if `prepay_hours > 0` → "Prepaid hour block"; if recent invoices show per-ticket billing → "Break-fix"; if large flat invoices → "Project" + - Billing rate: use the `price_retail` from the most recent non-zero labor line item in recent invoices; if no invoices yet, write "(verify — check Syncro invoices)" + - Customer ID: use Syncro `id` exactly as returned + - Managed device count: use Syncro asset count +2. Infrastructure: derive from session logs; keep Syncro asset count in Profile, not in infrastructure tables +3. Patterns & Known Issues: synthesize from session logs; for full recompile preserve existing patterns verbatim unless session logs show they are resolved +4. Active Work: use Syncro open ticket list as the primary source +5. History Highlights: chronological, from session logs only, one-line entries with dates +6. Access: vault paths and IPs from session logs; never invent vault paths +7. For fields with no source data: write "(verify)" not placeholder text +8. Backlinks: list any wiki article slugs (clients/projects/systems) that this client is cross-referenced with +``` + +If Ollama is unreachable: Claude writes the article directly using the same rules and all collected data. + +--- + +## Phase 5 — Write Article + Update Index + +**Write the article:** +- Seed: write `wiki/clients/.md` from generated content +- Full: overwrite `wiki/clients/.md` +- Refresh: edits already applied in Phase 4 + +**Update `wiki/index.md`:** +- Check if `wiki/clients/.md` is listed in the Clients table +- If **not listed**: insert a new row in the Clients table: + ``` + | [](clients/.md) | | | + ``` +- If **listed**: update the `Last Compiled` date and summary +- Update the `Last updated` header date + +--- + +## Phase 6 — Commit + +```bash +cd "$CLAUDETOOLS_ROOT" +git add "wiki/clients/${SLUG}.md" wiki/index.md +git commit -m "wiki: compile ${SLUG} (${MODE})" +git push origin main +``` + +Emit: +``` +[SUCCESS] wiki/clients/.md committed and pushed + +Mode: +Sources: session logs + Syncro (customer ) +Article: wiki/clients/.md +Index: wiki/index.md updated +``` + +--- + +## "all" Mode + +When invoked as `/wiki-compile all`: + +1. Run the same checks as `/wiki-lint` Steps 1–2 to find missing and stale articles +2. For each missing article: run seed mode +3. For stale articles (compiled > 90 days ago with newer logs): run refresh mode +4. For each: pause after Syncro ambiguity check if needed — do not bulk-skip +5. After all articles processed: single commit with message `wiki: bulk compile (N articles)` + +--- + +## Hard Rules + +- **This skill is read-only against Syncro.** No POST, PUT, PATCH, or DELETE. GET requests only. +- **Syncro is authoritative for all billing fields.** Hours remaining, billing rate, contract type, customer ID, and asset count always come from Syncro live data — never from session logs. Session logs may be stale; Syncro is not. +- **Never invent vault paths.** If a credential is not mentioned in session logs, write "(verify)" in the Access section. +- **Never populate Infrastructure tables with placeholder rows.** Only include servers/services that appear in session logs or Syncro assets. +- **Syncro contacts are ground truth for the Profile section.** Do not override with session log guesses if the contact name differs. +- **Refresh mode never touches Patterns or History.** Those sections require human review or `--full`. diff --git a/.claude/commands/wiki-lint.md b/.claude/commands/wiki-lint.md index 2881de1..bf79984 100644 --- a/.claude/commands/wiki-lint.md +++ b/.claude/commands/wiki-lint.md @@ -89,6 +89,86 @@ Read the `## Compilation Queue` section in `wiki/index.md`. For each entry: --- +## Step 6 — Syncro Live-Check (Client Articles, Auto-Fix) + +For every client wiki article that contains a `Syncro customer ID` line, pull live billing data from Syncro and auto-fix stale values in place. This step runs silently — no user confirmation needed. Fixes are committed at the end. + +### Setup + +```bash +BASE="https://computerguru.syncromsp.com/api/v1" +USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json") +case "$USER_ID" in + mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;; + howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;; + *) echo "[SYNCRO] No API key for user '$USER_ID' — skipping Step 6" ; exit 0 ;; +esac +``` + +### For Each Client Article + +```bash +for ARTICLE in wiki/clients/*.md; do + SLUG=$(basename "$ARTICLE" .md) + + # Extract Syncro customer ID — skip if not documented + CUST_ID=$(grep -oP '(?<=\*\*Syncro customer ID:\*\* )\d+' "$ARTICLE" 2>/dev/null) + [ -z "$CUST_ID" ] && continue + + # Pull live customer data + CUST=$(curl -s "$BASE/customers/${CUST_ID}?api_key=$API_KEY" | jq '.customer') + [ -z "$CUST" ] && echo "[SYNCRO][WARNING] $SLUG — API returned empty for customer $CUST_ID" && continue + + LIVE_HOURS=$(echo "$CUST" | jq -r '.prepay_hours // "0"') + TODAY=$(date +%Y-%m-%d) + + # --- Fix 1: Hours remaining --- + # Extract current wiki value (match pattern: "X.X hrs as of") + WIKI_HOURS=$(grep -oP '[\d.]+ hrs as of' "$ARTICLE" | grep -oP '[\d.]+' | head -1) + + if [ -n "$WIKI_HOURS" ] && [ "$WIKI_HOURS" != "$LIVE_HOURS" ]; then + # Replace the Hours remaining line with live value + sed -i "s/\*\*Hours remaining[^:]*:\*\*[^\n]*/\*\*Hours remaining (if prepaid):\*\* ${LIVE_HOURS} hrs as of ${TODAY} (Syncro live)/" "$ARTICLE" + echo "[SYNCRO][FIXED] $SLUG — hours: wiki=${WIKI_HOURS} → Syncro=${LIVE_HOURS}" + SYNCRO_FIXES+=("$SLUG: hours ${WIKI_HOURS} → ${LIVE_HOURS}") + fi + + # --- Check 2: Open ticket count (flag only, no auto-fix) --- + OPEN_COUNT=$(curl -s "$BASE/tickets?customer_id=${CUST_ID}&status=New,In+Progress,Scheduled,Waiting+on+Customer&per_page=1&api_key=$API_KEY" | jq '.meta.total_count // (.tickets | length)') + # Flag if article was compiled > 7 days ago and open ticket count changed + COMPILED=$(grep -oP '(?<=last_compiled: )[\d-]+' "$ARTICLE") + COMPILED_AGE=$(( ( $(date +%s) - $(date -d "$COMPILED" +%s 2>/dev/null || echo 0) ) / 86400 )) + if [ "$COMPILED_AGE" -gt 7 ] && [ "$OPEN_COUNT" -gt 0 ]; then + SYNCRO_TICKET_FLAGS+=("[TICKETS_CHECK] $SLUG — $OPEN_COUNT open ticket(s) in Syncro; article compiled ${COMPILED_AGE}d ago") + fi +done +``` + +### Commit Fixes (if any) + +```bash +if [ ${#SYNCRO_FIXES[@]} -gt 0 ]; then + cd "$CLAUDETOOLS_ROOT" + git add wiki/clients/*.md + git commit -m "wiki-lint: Syncro live-check auto-fix (${#SYNCRO_FIXES[@]} article(s))" + git push origin main +fi +``` + +### Add to Lint Report + +Append to the report output: + +``` +### Syncro Live-Check (N auto-fixed, M flagged) +[SYNCRO][FIXED] cascades-tucson — hours: 37.5 → 31.0 (Syncro live as of YYYY-MM-DD) +[TICKETS_CHECK] valleywide — 2 open ticket(s) in Syncro; article compiled 14d ago +``` + +If API key is missing or Syncro is unreachable, emit `[SYNCRO] Skipped (API unavailable)` and continue. + +--- + ## Output Format Emit a clean lint report: @@ -116,11 +196,17 @@ Emit a clean lint report: [QUEUE_STALE] client:birthbiologic — wiki/clients/birth-biologic.md exists; remove from queue ... +### Syncro Live-Check (N auto-fixed, M flagged) +[SYNCRO][FIXED] cascades-tucson — hours: 37.5 → 31.0 (Syncro live) +[TICKETS_CHECK] valleywide — 2 open ticket(s); article compiled 14d ago +... + ### Summary - N missing articles → run /wiki-compile for each - N stale articles → run /wiki-compile to refresh - N broken links → fix manually or after recompile - N index gaps → update wiki/index.md +- N Syncro hours auto-fixed, M ticket flags for review ``` After the report, ask: "Run /wiki-compile for any of the missing articles now?"