feat(identity): read claudetools_root from identity.json
- Updated sync.sh to read claudetools_root from identity.json - Updated syncro.md skill to use identity.json for repo path - Updated CLAUDE.md onboarding to include claudetools_root field - Eliminates cross-architecture path detection issues - Fallback to git rev-parse for legacy machines Each machine sets claudetools_root during onboarding, just like vault_path.
This commit is contained in:
@@ -16,10 +16,11 @@ This repo is shared across multiple team members. **At every session start, BEFO
|
|||||||
"email": "mike@azcomputerguru.com",
|
"email": "mike@azcomputerguru.com",
|
||||||
"role": "admin",
|
"role": "admin",
|
||||||
"machine": "<HOSTNAME>",
|
"machine": "<HOSTNAME>",
|
||||||
"vault_path": "<absolute path to vault repo on this machine>"
|
"vault_path": "<absolute path to vault repo on this machine>",
|
||||||
|
"claudetools_root": "<absolute path to ClaudeTools repo on this machine>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
Ask the user where the vault repo is cloned on this machine (e.g., `D:/vault`, `~/vault`, `/Users/howard/vault`).
|
Ask the user where the vault repo is cloned (e.g., `D:/vault`, `~/vault`, `/Users/howard/vault`) and where ClaudeTools is cloned (e.g., `D:/claudetools`, `~/ClaudeTools`, `/Users/mike/ClaudeTools`).
|
||||||
- Set local git config: `git config user.name "<full_name>"` and `git config user.email "<email>"`
|
- Set local git config: `git config user.name "<full_name>"` and `git config user.email "<email>"`
|
||||||
- Set git remote (read `gitea_username` from users.json): `git remote set-url origin https://<gitea_username>@git.azcomputerguru.com/azcomputerguru/claudetools.git`
|
- Set git remote (read `gitea_username` from users.json): `git remote set-url origin https://<gitea_username>@git.azcomputerguru.com/azcomputerguru/claudetools.git`
|
||||||
- Add hostname to user's `known_machines` in users.json and commit.
|
- Add hostname to user's `known_machines` in users.json and commit.
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ If any check fails, complete the missing step before reporting done. This rule f
|
|||||||
|
|
||||||
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
|
When invoked, use the Syncro REST API via `curl`. All requests include `?api_key=<key>` as query parameter (NOT in header — Syncro uses query param auth).
|
||||||
|
|
||||||
|
**Repo root resolution:** All scripts read `claudetools_root` from `.claude/identity.json` (set during machine onboarding). This eliminates cross-architecture path issues. The identity.json file is machine-specific (gitignored) and contains the absolute path to the ClaudeTools repo on each machine.
|
||||||
|
|
||||||
### Attribution rule (CRITICAL)
|
### Attribution rule (CRITICAL)
|
||||||
|
|
||||||
Every Syncro API call is attributed to the **owner of the API key**. Comments, line items, timer entries, and invoices created by the API are logged as the API user — regardless of who is running the command. So the skill MUST use a per-user API key that matches the actual tech running it, or comments will be misattributed.
|
Every Syncro API call is attributed to the **owner of the API key**. Comments, line items, timer entries, and invoices created by the API are logged as the API user — regardless of who is running the command. So the skill MUST use a per-user API key that matches the actual tech running it, or comments will be misattributed.
|
||||||
@@ -92,8 +94,36 @@ Keys are baked into the skill below. To add a new user: generate a token in Sync
|
|||||||
```bash
|
```bash
|
||||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
BASE="https://computerguru.syncromsp.com/api/v1"
|
||||||
|
|
||||||
|
# Get repo root from identity.json (set during machine onboarding)
|
||||||
|
# Fallback to dynamic detection for legacy machines that haven't updated identity.json yet
|
||||||
|
IDENTITY_PATH="${HOME}/.claude/identity.json"
|
||||||
|
if [ ! -f "$IDENTITY_PATH" ]; then
|
||||||
|
# Try in-repo identity.json (gitignored, machine-specific)
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if [ -n "$REPO_ROOT" ]; then
|
||||||
|
IDENTITY_PATH="$REPO_ROOT/.claude/identity.json"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$IDENTITY_PATH" ]; then
|
||||||
|
echo "[ERROR] Cannot locate identity.json - run onboarding first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT=$(jq -r '.claudetools_root // empty' "$IDENTITY_PATH")
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
|
# Legacy fallback for machines without claudetools_root in identity.json
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
|
echo "[ERROR] claudetools_root not set in identity.json and not in a git directory" >&2
|
||||||
|
echo "[ERROR] Add 'claudetools_root' field to $IDENTITY_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[WARNING] Using git-detected repo root. Add 'claudetools_root' to identity.json to avoid this." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Per-user keys — actions in Syncro are attributed to the key owner
|
# Per-user keys — actions in Syncro are attributed to the key owner
|
||||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
USER_ID=$(jq -r '.user // empty' "$IDENTITY_PATH")
|
||||||
case "$USER_ID" in
|
case "$USER_ID" in
|
||||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
||||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
||||||
@@ -122,8 +152,9 @@ fi
|
|||||||
```bash
|
```bash
|
||||||
# Write prompt to a workspace path both the Write tool and Git Bash agree on
|
# Write prompt to a workspace path both the Write tool and Git Bash agree on
|
||||||
# (do NOT use /tmp on Windows — see Hard Rules: /tmp resolves differently in
|
# (do NOT use /tmp on Windows — see Hard Rules: /tmp resolves differently in
|
||||||
# Write vs Git Bash). Use $CLAUDETOOLS_ROOT/.claude/tmp/ or pipe via heredoc.
|
# Write vs Git Bash). Use $REPO_ROOT/.claude/tmp/ or pipe via heredoc.
|
||||||
PROMPT_FILE="$CLAUDETOOLS_ROOT/.claude/tmp/ollama_prompt.txt"
|
# $REPO_ROOT is set in the "Get API key" section above.
|
||||||
|
PROMPT_FILE="$REPO_ROOT/.claude/tmp/ollama_prompt.txt"
|
||||||
mkdir -p "$(dirname "$PROMPT_FILE")"
|
mkdir -p "$(dirname "$PROMPT_FILE")"
|
||||||
cat > "$PROMPT_FILE" <<'ENDPROMPT'
|
cat > "$PROMPT_FILE" <<'ENDPROMPT'
|
||||||
<prompt content here>
|
<prompt content here>
|
||||||
@@ -226,6 +257,43 @@ If `OLLAMA` is empty (neither endpoint reachable): Claude drafts the comment bod
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Parsing Syncro API Responses
|
||||||
|
|
||||||
|
**Problem:** Syncro API responses contain unescaped control characters (U+0000 through U+001F) that break both `jq` and Python's `json.load()`. These characters appear in customer names, ticket descriptions, and other text fields.
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- `jq: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped`
|
||||||
|
- Python: `json.decoder.JSONDecodeError: Invalid control character`
|
||||||
|
|
||||||
|
**Solution:** Use `grep` and `sed` for simple field extraction instead of jq. For complex parsing where jq is unavoidable, preprocess with `tr -d '\000-\037'` to strip control characters (lossy but functional).
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract numeric ID from response (safe - works with control chars)
|
||||||
|
TICKET_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
TICKET_NUMBER=$(echo "$RESP" | grep -o '"number":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
CUSTOMER_ID=$(echo "$RESP" | grep -o '"customer_id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||||
|
|
||||||
|
# Extract string field (use sed to extract between quotes)
|
||||||
|
# Example: "status":"New" → New
|
||||||
|
STATUS=$(echo "$RESP" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||||
|
|
||||||
|
# Extract decimal/float field
|
||||||
|
TOTAL=$(echo "$RESP" | grep -o '"total":"[0-9.]*"' | head -1 | grep -o '[0-9.]*')
|
||||||
|
|
||||||
|
# Fallback for complex parsing - strip control chars then use jq
|
||||||
|
CLEAN=$(echo "$RESP" | tr -d '\000-\037')
|
||||||
|
TICKET_ID=$(echo "$CLEAN" | jq -r '.ticket.id')
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use each approach:**
|
||||||
|
- **grep/sed** (preferred): Extracting numeric IDs, simple string fields, totals
|
||||||
|
- **jq with preprocessing** (only when necessary): Complex nested structures, arrays, multiple fields at once
|
||||||
|
- **Never retry jq on parse failure** — if jq fails once on a response, it will fail every time with the same input. Switch to grep/sed immediately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Adding a per-user key
|
### Adding a per-user key
|
||||||
|
|
||||||
1. User logs into Syncro → Admin → API Tokens → New (`/api_tokens/new`)
|
1. User logs into Syncro → Admin → API Tokens → New (`/api_tokens/new`)
|
||||||
@@ -946,7 +1014,7 @@ curl -s -X PUT "${BASE}/tickets/${ID}?api_key=${API_KEY}" \
|
|||||||
JSON
|
JSON
|
||||||
|
|
||||||
# 5. Bot alert
|
# 5. Bot alert
|
||||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||||
"[SYNCRO] Mike billed #<number> (<customer>) — ${QTY}h <labor_type>, \$${INVOICE_TOTAL} → https://computerguru.syncromsp.com/tickets/${ID}"
|
"[SYNCRO] Mike billed #<number> (<customer>) — ${QTY}h <labor_type>, \$${INVOICE_TOTAL} → https://computerguru.syncromsp.com/tickets/${ID}"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -974,7 +1042,7 @@ Post after every successful Syncro write. Never post before the write completes.
|
|||||||
**Invocation:**
|
**Invocation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ALERT_OUT=$(bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" "<message>")
|
ALERT_OUT=$(bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" "<message>")
|
||||||
echo "$ALERT_OUT"
|
echo "$ALERT_OUT"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1004,20 +1072,20 @@ echo "$ALERT_OUT"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Ticket created
|
# Ticket created
|
||||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||||
"[SYNCRO] Howard created #32301 (Desert Auto Tech) - Server won't boot -> https://computerguru.syncromsp.com/tickets/110736645"
|
"[SYNCRO] Howard created #32301 (Desert Auto Tech) - Server won't boot -> https://computerguru.syncromsp.com/tickets/110736645"
|
||||||
# Success output: [OK] post-bot-alert: posted to #bot-alerts (message_id=1507055781780918404)
|
# Success output: [OK] post-bot-alert: posted to #bot-alerts (message_id=1507055781780918404)
|
||||||
|
|
||||||
# Billed + invoiced
|
# Billed + invoiced
|
||||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||||
"[SYNCRO] Mike billed #32164 (Jerry Burger) - 1.0h remote, \$150.00 -> https://computerguru.syncromsp.com/tickets/110169036"
|
"[SYNCRO] Mike billed #32164 (Jerry Burger) - 1.0h remote, \$150.00 -> https://computerguru.syncromsp.com/tickets/110169036"
|
||||||
|
|
||||||
# Prepaid billing
|
# Prepaid billing
|
||||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||||
"[SYNCRO] Mike billed #32203 (Desert Auto Tech) - 1.5h onsite, applied 1.5 prepay hrs, \$0.00 -> https://computerguru.syncromsp.com/tickets/109895882"
|
"[SYNCRO] Mike billed #32203 (Desert Auto Tech) - 1.5h onsite, applied 1.5 prepay hrs, \$0.00 -> https://computerguru.syncromsp.com/tickets/109895882"
|
||||||
|
|
||||||
# Ticket status updated
|
# Ticket status updated
|
||||||
bash "$CLAUDETOOLS_ROOT/.claude/scripts/post-bot-alert.sh" \
|
bash "$REPO_ROOT/.claude/scripts/post-bot-alert.sh" \
|
||||||
"[SYNCRO] Mike resolved #32271 (Peaceful Spirit Massage) - IKEv2 VPN setup complete -> https://computerguru.syncromsp.com/tickets/110169036"
|
"[SYNCRO] Mike resolved #32271 (Peaceful Spirit Massage) - IKEv2 VPN setup complete -> https://computerguru.syncromsp.com/tickets/110169036"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -54,25 +54,41 @@ TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
|||||||
echo -e "${GREEN}[OK]${NC} Starting ClaudeTools sync from $MACHINE at $TIMESTAMP"
|
echo -e "${GREEN}[OK]${NC} Starting ClaudeTools sync from $MACHINE at $TIMESTAMP"
|
||||||
|
|
||||||
# Navigate to ClaudeTools directory
|
# Navigate to ClaudeTools directory
|
||||||
# First check: are we already in the repo (or a subdirectory of it)?
|
# Read from identity.json (machine-specific, set during onboarding)
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
IDENTITY_PATH=""
|
||||||
if [ -n "$REPO_ROOT" ]; then
|
for candidate in "$HOME/.claude/identity.json" ".claude/identity.json"; do
|
||||||
cd "$REPO_ROOT"
|
if [ -f "$candidate" ]; then
|
||||||
else
|
IDENTITY_PATH="$candidate"
|
||||||
# Fall back to known candidate paths
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$IDENTITY_PATH" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
REPO_ROOT=$(jq -r '.claudetools_root // empty' "$IDENTITY_PATH" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: git detection if identity.json doesn't have claudetools_root yet
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Last resort: hardcoded paths (legacy machines)
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
for candidate in "$HOME/ClaudeTools" "/d/ClaudeTools" "D:/ClaudeTools" "/d/claudetools" "D:/claudetools" "C:/claudetools" "/c/claudetools"; do
|
for candidate in "$HOME/ClaudeTools" "/d/ClaudeTools" "D:/ClaudeTools" "/d/claudetools" "D:/claudetools" "C:/claudetools" "/c/claudetools"; do
|
||||||
if [ -d "$candidate/.git" ]; then
|
if [ -d "$candidate/.git" ]; then
|
||||||
cd "$candidate"
|
REPO_ROOT="$candidate"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d ".git" ]; then
|
if [ -z "$REPO_ROOT" ] || [ ! -d "$REPO_ROOT/.git" ]; then
|
||||||
echo -e "${RED}[ERROR]${NC} Not in a git working tree"
|
echo -e "${RED}[ERROR]${NC} Cannot locate ClaudeTools repo. Add 'claudetools_root' to identity.json"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)"
|
echo -e "${GREEN}[OK]${NC} Working directory: $(pwd)"
|
||||||
|
|
||||||
# Detect Python interpreter — verify it actually runs (Windows Store stub passes command -v but fails to execute)
|
# Detect Python interpreter — verify it actually runs (Windows Store stub passes command -v but fails to execute)
|
||||||
|
|||||||
Reference in New Issue
Block a user